├── .gitignore ├── README.rst ├── burpfs ├── FileSystem.py ├── LogFile.py ├── __init__.py └── burpfs └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | BurpFS 3 | ====== 4 | 5 | BurpFS_ - Exposes the Burp_ backup storage as a Filesystem in 6 | USErspace (FUSE_). 7 | 8 | .. _BurpFS: https://github.com/ZungBang/burpfs 9 | .. _Burp: http://burp.grke.net/ 10 | .. _FUSE: https://github.com/libfuse/libfuse 11 | 12 | Copyright |(C)| 2012-2020 Avi Rozen 13 | 14 | .. contents:: 15 | 16 | Introduction 17 | ------------ 18 | 19 | **BurpFS** is a tool, developed independently of Burp, that represents 20 | the Burp backup storage as a read-only filesystem in userspace. 21 | 22 | **BurpFS** is specifically designed to cater for the following 23 | use-cases: 24 | 25 | - maintaining a remote snapshot of the files in the backup storage 26 | using `rsync`_ 27 | - auditing the contents of backup jobs at the client side 28 | - comparing backup jobs (using several mount points) 29 | - easier restore from backup 30 | 31 | .. _rsync: https://rsync.samba.org 32 | 33 | 34 | Installation 35 | ------------ 36 | 37 | There's no official release at this point, so please clone the public 38 | **BurpFS** Git repository_ and run ``burpfs`` as ``root`` from the 39 | ``burpfs`` directory. 40 | 41 | **BurpFS** requires Burp 1.3.22 and up, Python_ 2.7 and up, FUSE_ 2.x 42 | (note that Python FUSE bindings_ 1.0.0 and up is *reuqired* for Python 43 | 3.x). 44 | 45 | You must set the client machine that you mount the filesystem on as a 46 | ``restore-client`` at the server side. 47 | 48 | With Burp 2.x **BurpFS** uses the burp monitor to browse the backup 49 | manifest, so you need to be able to run ``burp -a m`` on the same 50 | machine you try to mount the filesystem on. 51 | 52 | 53 | .. _repository: https://github.com/ZungBang/burpfs.git 54 | .. _Python: https://www.python.org 55 | .. _FUSE: https://github.com/libfuse/libfuse 56 | .. _bindings: https://github.com/libfuse/python-fuse 57 | 58 | 59 | Usage Examples 60 | -------------- 61 | 62 | Mount the most recent backup snapshot for the local machine: 63 | 64 | :: 65 | 66 | burpfs /path/to/mount/point 67 | 68 | Mount the contents of the backup #50 for client ``dumbo`` (the local 69 | client must be configured as a ``restore_client`` for ``dumbo`` at the 70 | ``burp`` server side): 71 | 72 | :: 73 | 74 | burpfs -o backup=50,client=dumbo /path/to/mount/point 75 | 76 | Mount only the *modified* contents of the backup #50 for client 77 | ``dumbo``, compared with the backup preceding it: 78 | 79 | :: 80 | 81 | burpfs -o diff,backup=50,client=dumbo /path/to/mount/point 82 | 83 | Mount the contents of the backup job before the specified date/time: 84 | 85 | :: 86 | 87 | burpfs -o datetime='2012-08-23 00:00:00' /path/to/mount/point 88 | 89 | Allow other users to access filesystem, set logging level to ``debug`` 90 | and stay in foreground, so that Burp messages may be examined: 91 | 92 | :: 93 | 94 | burpfs -f -o allow_other,logging=debug /path/to/mount/point 95 | 96 | 97 | Limitations 98 | ----------- 99 | **BurpFS** is in its rough-around-the-edges alpha stage. Expect 100 | breakage. Please report Bugs_. 101 | 102 | **BurpFS** queries the Burp server for the files list only once, when 103 | the filesystem is initialized. There's nothing to prevent the backup 104 | being represented from being deleted at the server side while the 105 | filesystem is mounted. **BurpFS** is liable to fail in interesting 106 | ways in this case. 107 | 108 | 109 | Changelog 110 | --------- 111 | **Version 0.3.8 (2020-09-14)** 112 | 113 | - do not try to open backup in 'working' state 114 | 115 | **Version 0.3.7 (2020-04-19)** 116 | 117 | - added support for burp 2.2.12 and up 118 | 119 | **Version 0.3.6 (2019-11-16)** 120 | 121 | - fixed handling of new lines in file names 122 | 123 | **Version 0.3.5 (2019-11-16)** 124 | 125 | - fixed path regex sanitation code 126 | 127 | **Version 0.3.4 (2019-11-12)** 128 | 129 | - Python 3 transition 130 | 131 | **Version 0.3.3 (2018-10-25)** 132 | 133 | - workaround: access to files with back-quotes in their name 134 | 135 | **Version 0.3.2 (2016-09-13)** 136 | 137 | - tweaked VSS headers parser 138 | 139 | **Version 0.3.1 (2016-09-11)** 140 | 141 | - auto strip VSS headers 142 | 143 | **Version 0.3.0 (2016-09-08)** 144 | 145 | - added support for Burp 2.x 146 | 147 | **Version 0.2.4 (2016-09-05)** 148 | 149 | - issue error when trying to mount Burp 2.x backup 150 | 151 | **Version 0.2.3 (2015-12-14)** 152 | 153 | - added new diff mode to mount modified/added files only 154 | - fixed restore of files with very long path string 155 | 156 | **Version 0.2.2 (2013-11-11)** 157 | 158 | - added support for Burp 1.4.x 159 | 160 | **Version 0.2.1 (2013-01-13)** 161 | 162 | - fixed **BurpFS** version 163 | 164 | **Version 0.2.0 (2013-01-13)** 165 | 166 | - implemented LRU (Least Recently Used) cache policy 167 | - workaround: access to files with single quotes in their name 168 | - provide options to specify the path to the Burp executable 169 | ``-o burp`` and a path to the Burp client configuration file 170 | ``-o conf`` 171 | 172 | **Version 0.1.0 (2013-01-03)** 173 | 174 | - switched to burp JSON long listing format (requires Burp 1.3.22 and 175 | up): 176 | 177 | + fixed ``-o use_ino`` so that files can have their original inode 178 | numbers 179 | + fixed file timestamps 180 | + fixed handling of hardlinks 181 | 182 | - fixed handling of Windows paths 183 | - fixed handling of empty directories 184 | - several stability workarounds 185 | 186 | **Version 0.0.1 (2012-12-21-End of The World Release)** 187 | 188 | - initial public release 189 | 190 | Source Code 191 | ----------- 192 | 193 | **BurpFS** development source code may be cloned from its public Git 194 | repository at ``_ 195 | 196 | 197 | Bugs 198 | ---- 199 | 200 | Please report problems via the **BurpFS** issue tracking system: 201 | ``_ 202 | 203 | 204 | License 205 | ------- 206 | 207 | **BurpFS** is free software: you can redistribute it and/or modify 208 | it under the terms of the GNU General Public License as published by 209 | the Free Software Foundation, either version 3 of the License, or (at 210 | your option) any later version. 211 | 212 | This program is distributed in the hope that it will be useful, but 213 | WITHOUT ANY WARRANTY; without even the implied warranty of 214 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 215 | General Public License for more details. 216 | 217 | You should have received a copy of the GNU General Public License 218 | along with this program. If not, see 219 | ``_. 220 | 221 | .. |(C)| unicode:: 0xA9 .. copyright sign 222 | 223 | -------------------------------------------------------------------------------- /burpfs/FileSystem.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # BurpFS - Burp Filesystem in USErspace 4 | # Copyright (C) 2012-2021 Avi Rozen 5 | # 6 | # This file is part of BurpFS. 7 | # 8 | # BurpFS is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | __version__ = '0.4.0' 22 | 23 | import os 24 | import sys 25 | import subprocess 26 | import stat 27 | import errno 28 | import copy 29 | import tempfile 30 | import shutil 31 | import threading 32 | import traceback 33 | import fcntl 34 | import time 35 | import re 36 | import json 37 | import struct 38 | import unicodedata 39 | from datetime import datetime 40 | from bisect import bisect_left, bisect_right 41 | 42 | # incantation to avoid import errors 43 | # https://stackoverflow.com/a/56120695 44 | try: 45 | from .LogFile import logging, LOGGING_LEVELS, LogFile 46 | except (ValueError, ImportError): 47 | from LogFile import logging, LOGGING_LEVELS, LogFile 48 | 49 | import fuse 50 | from fuse import Fuse, FuseOptParse 51 | 52 | if not hasattr(fuse, '__version__'): 53 | raise RuntimeError( 54 | "your fuse-py doesn't know of fuse.__version__, probably it's too old") 55 | 56 | fuse.fuse_python_api = (0, 2) 57 | 58 | fuse.feature_assert('stateful_files', 'has_init') 59 | 60 | 61 | def flag2mode(flags): 62 | ''' 63 | taken from python-fuse xmp.py example 64 | ''' 65 | md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} 66 | m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] 67 | 68 | if flags | os.O_APPEND: 69 | m = m.replace('w', 'a', 1) 70 | 71 | return m 72 | 73 | 74 | def makedirs(path): 75 | ''' 76 | create path like mkdir -p 77 | taken from: http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612 78 | ''' 79 | try: 80 | os.makedirs(path) 81 | except OSError as exc: 82 | if exc.errno == errno.EEXIST: 83 | pass 84 | else: 85 | raise 86 | 87 | 88 | def totimestamp(dt, epoch=datetime(1970, 1, 1)): 89 | ''' 90 | convert datetime to (UTC) timestamp 91 | adapted from: http://stackoverflow.com/a/8778548/27831 92 | ''' 93 | td = dt - epoch 94 | return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 1e6 95 | 96 | 97 | # _decode_list and _decode_dict below are used to convince the JSON 98 | # parser (json.loads) to avoid coercing everything into unicode under 99 | # Python 2.x 100 | # taken from: 101 | # http://stackoverflow.com/a/6633651 102 | 103 | def _decode_list(data): 104 | rv = [] 105 | for item in data: 106 | if isinstance(item, unicode): 107 | item = item.encode('utf-8') 108 | elif isinstance(item, list): 109 | item = _decode_list(item) 110 | elif isinstance(item, dict): 111 | item = _decode_dict(item) 112 | rv.append(item) 113 | return rv 114 | 115 | 116 | def _decode_dict(data): 117 | rv = {} 118 | for key, value in data.items(): 119 | if isinstance(key, unicode): 120 | key = key.encode('utf-8') 121 | if isinstance(value, unicode): 122 | value = value.encode('utf-8') 123 | elif isinstance(value, list): 124 | value = _decode_list(value) 125 | elif isinstance(value, dict): 126 | value = _decode_dict(value) 127 | rv[key] = value 128 | return rv 129 | 130 | 131 | # cut unicode string to specific byte string length 132 | # adapted from: 133 | # https://stackoverflow.com/a/13665637 134 | 135 | LENGTH_BY_PREFIX = [ 136 | (0xC0, 2), # first byte mask, total codepoint length 137 | (0xE0, 3), 138 | (0xF0, 4), 139 | (0xF8, 5), 140 | (0xFC, 6), 141 | ] 142 | 143 | 144 | def codepoint_length(first_byte): 145 | for mask, length in LENGTH_BY_PREFIX: 146 | if first_byte & mask == mask: 147 | return length 148 | return 1 # ascii or invalid byte 149 | 150 | 151 | def cut_unicode_to_bytes_length(unicode_text, byte_limit): 152 | utf8_bytes = unicode_text.encode('utf-8') 153 | cut_index = 0 154 | while cut_index < len(utf8_bytes): 155 | codepoint_1st = utf8_bytes[cut_index] 156 | if isinstance(codepoint_1st, str): 157 | codepoint_1st = ord(codepoint_1st) 158 | step = codepoint_length(codepoint_1st) 159 | if cut_index + step > byte_limit: 160 | # can't go a whole codepoint further, time to cut 161 | return utf8_bytes[:cut_index].decode('utf-8') 162 | else: 163 | cut_index += step 164 | # length limit is longer than our bytes strung, so no cutting 165 | return unicode_text 166 | 167 | 168 | class FileSystem(Fuse): 169 | 170 | datetime_format = '%Y-%m-%d %H:%M:%S' 171 | 172 | null_stat = fuse.Stat(st_mode=stat.S_IFDIR | 0o755, 173 | st_ino=0, 174 | st_dev=0, 175 | st_nlink=2, 176 | st_uid=0, 177 | st_gid=0, 178 | st_size=0, 179 | st_atime=0, 180 | st_mtime=0, 181 | st_ctime=0, 182 | st_blksize=0, 183 | st_rdev=0) 184 | 185 | fuse_stat_fields = [attr for attr in dir(null_stat) 186 | if attr.startswith('st_')] 187 | 188 | xattr_prefix = 'user.burpfs.' 189 | 190 | xattr_fields = [] 191 | 192 | xattr_fields_root = ['burp', 193 | 'conf', 194 | 'client', 195 | 'backup', 196 | 'datetime'] 197 | 198 | xattr_fields_burp = ['regex', 199 | 'state', 200 | 'pending'] 201 | 202 | xattr_fields_cache = ['prefix', 203 | 'num_files', 204 | 'max_num_files', 205 | 'total_size', 206 | 'max_total_size'] 207 | 208 | burp_done = {'regex': None, 209 | 'state': 'idle'} 210 | 211 | vss_header_format = '= 0) 406 | # are there any more references to this file? 407 | if self.fs.dirs[head][tail].refcount == 0: 408 | # ... no: add closed file to LRU queue 409 | head_tail_pair = (head, tail) 410 | # file may already be in the removal queue, so 411 | # we first attempt to remove it from the queue 412 | try: 413 | self.queue.remove(head_tail_pair) 414 | except ValueError: 415 | pass 416 | self.queue.append(head_tail_pair) 417 | self.fs.dirs[head][tail].queued_for_removal = True 418 | # evict closed files from cache until cache limits are met 419 | while (self.queue and 420 | (self.num_files > self.max_num_files or 421 | self.total_size > self.max_total_size)): 422 | rm_head, rm_tail = self.queue.pop(0) 423 | assert(self.fs.dirs[rm_head][rm_tail].queued_for_removal) 424 | self.fs.dirs[rm_head][rm_tail].queued_for_removal = False 425 | # can we evict this file from the cache? 426 | if self.fs.dirs[rm_head][rm_tail].refcount == 0: 427 | # ... yes: delete the file from disk 428 | realpath = os.path.normpath('/'.join([self.path, rm_head, rm_tail])) 429 | try: 430 | os.remove(realpath) 431 | except: 432 | pass 433 | # we update counters even if we failed to remove the file 434 | self.num_files -= 1 435 | self.total_size -= self.fs.dirs[rm_head][rm_tail].stat.st_size 436 | assert(self.num_files >= 0) 437 | assert(self.total_size >= 0) 438 | self.fs._extract_lock.release() 439 | 440 | def __init__(self, *args, **kw): 441 | ''' 442 | Initialize filesystem 443 | ''' 444 | 445 | self._extract_lock = threading.Lock() 446 | self._burp_status_lock = threading.Lock() 447 | self._initialized = False 448 | 449 | # default option values 450 | self.logging = 'info' 451 | self.syslog = False 452 | self.burp = 'burp' 453 | self.conf = '/etc/burp/burp.conf' 454 | self.client = '' 455 | self.backup = None 456 | self.datetime = None 457 | self.diff = False 458 | self.cache = None 459 | self.cache_num_files = 768 460 | self.cache_total_size = 100 461 | self.move_root = False 462 | self.use_ino = False 463 | self.max_ino = 0 464 | self.dirs = {'/': {'': FileSystem._Entry(self, '/')}} 465 | 466 | self._burp_status = copy.deepcopy(FileSystem.burp_done) 467 | self._burp_status['pending'] = 0 468 | 469 | class File (FileSystem._File): 470 | def __init__(self2, *a, **kw): 471 | FileSystem._File.__init__(self2, self, *a, **kw) 472 | 473 | self.file_class = File 474 | 475 | Fuse.__init__(self, *args, **kw) 476 | 477 | def _split(self, path): 478 | ''' 479 | os.path.split wrapper 480 | ''' 481 | head, tail = os.path.split(path) 482 | if head and not head.endswith('/'): 483 | head += '/' 484 | return head, tail 485 | 486 | def _add_parent_dirs(self, path): 487 | ''' 488 | add parent directories of path to dirs dictionary 489 | ''' 490 | head, tail = self._split(path[:-1]) 491 | if not head or head == path: 492 | return 493 | if head not in self.dirs: 494 | self.dirs[head] = {tail: FileSystem._Entry(self, path)} 495 | elif tail not in self.dirs[head]: 496 | self.dirs[head][tail] = FileSystem._Entry(self, path) 497 | self._add_parent_dirs(head) 498 | 499 | def _update_inodes(self, head): 500 | ''' 501 | generate unique st_ino for each missing st_ino 502 | ''' 503 | for tail in self.dirs[head]: 504 | if self.dirs[head][tail].stat.st_ino == 0: 505 | self.max_ino += 1 506 | self.dirs[head][tail].stat.st_ino = self.max_ino 507 | subdir = '%s%s/' % (head, tail) 508 | if subdir in self.dirs: 509 | self._update_inodes(subdir) 510 | 511 | def _vss_parse(self, realpath, path): 512 | ''' 513 | parse vss headers of realpath, cache results, 514 | return base offset and size overhead 515 | 516 | vss headers are win32 stream id structures 517 | https://msdn.microsoft.com/en-us/library/windows/desktop/aa362667.aspx 518 | ''' 519 | head, tail = self._split(path) 520 | if head not in self.dirs or tail not in self.dirs[head]: 521 | return 0, 0 522 | if (not self.dirs[head][tail].under_root and 523 | self.dirs[head][tail].vss_overhead == 0 and 524 | self.dirs[head][tail].vss_offset == 0 and 525 | os.path.exists(realpath)): 526 | s = os.lstat(realpath) 527 | bs = self.getattr(path) 528 | if (s.st_size >= FileSystem.vss_header_size and 529 | s.st_size > bs.st_size): 530 | self.logger.debug('parsing vss headers of "%s"' % path) 531 | with open(realpath, 'rb') as f: 532 | offset = 0 533 | overhead = 0 534 | while True: 535 | vss_header = f.read(FileSystem.vss_header_size) 536 | if len(vss_header) < FileSystem.vss_header_size: 537 | break 538 | offset += FileSystem.vss_header_size 539 | overhead += FileSystem.vss_header_size 540 | (sid, sattr, ssize, sname_size) = struct.unpack(FileSystem.vss_header_format, vss_header) 541 | self.logger.debug('vss header at offset %d: id=%d attr=%d size=%d, name size=%d' % 542 | (offset - FileSystem.vss_header_size, sid, sattr, ssize, sname_size)) 543 | offset += sname_size 544 | overhead += sname_size 545 | if (self.dirs[head][tail].vss_offset == 0 and 546 | sid == 1 and 547 | ssize == bs.st_size and 548 | offset + ssize <= s.st_size): 549 | self.logger.debug('setting offset of file data as %d' % offset) 550 | self.dirs[head][tail].vss_offset = offset 551 | overhead -= ssize 552 | offset += ssize 553 | overhead += ssize 554 | if offset <= 0 or offset > s.st_size: 555 | self.logger.debug('bad offset %d - bailing out' % offset) 556 | self.dirs[head][tail].vss_offset = 0 557 | break 558 | f.seek(offset) 559 | if self.dirs[head][tail].vss_offset > 0: 560 | if overhead + bs.st_size == s.st_size: 561 | self.logger.debug('setting vss header size overhead as %d' % overhead) 562 | self.dirs[head][tail].vss_overhead = overhead 563 | else: 564 | self.logger.debug('bad overhead %d - offset set to 0' % overhead) 565 | self.dirs[head][tail].vss_offset = 0 566 | return self.dirs[head][tail].vss_offset, self.dirs[head][tail].vss_overhead 567 | 568 | def _extract(self, path_list): 569 | ''' 570 | extract path list from storage, returns path list of extracted files 571 | ''' 572 | 573 | nitems = len(path_list) 574 | self._burp_increment_counter('pending', nitems) 575 | 576 | # serialize extractions 577 | self._extract_lock.acquire() 578 | 579 | items = [] 580 | realpath_list = [] 581 | 582 | for path in path_list: 583 | realpaths, path_regexs, found = self.cache.find(path) 584 | if not realpaths: 585 | continue 586 | realpath_list.extend(realpaths) 587 | self.cache.open(path) 588 | if not found: 589 | items.extend(path_regexs) 590 | 591 | if len(items) > 0: 592 | self._burp(items) 593 | 594 | self._extract_lock.release() 595 | self._burp_increment_counter('pending', -nitems) 596 | 597 | return realpath_list 598 | 599 | def _burp_set_status(self, status): 600 | ''' 601 | thread safe modification of burp status dict 602 | ''' 603 | self._burp_status_lock.acquire() 604 | for key in status: 605 | self._burp_status[key] = status[key] 606 | self._burp_status_lock.release() 607 | 608 | def _burp_increment_counter(self, counter, n): 609 | ''' 610 | thread safe modification of burp counters 611 | ''' 612 | self._burp_status_lock.acquire() 613 | self._burp_status[counter] += n 614 | self._burp_status_lock.release() 615 | 616 | def _burp_get_status(self): 617 | ''' 618 | thread safe access to burp status dict 619 | ''' 620 | self._burp_status_lock.acquire() 621 | status = copy.deepcopy(self._burp_status) 622 | self._burp_status_lock.release() 623 | return status 624 | 625 | def _burp_flock(self): 626 | ''' 627 | lock the storage daemon configuration file 628 | ''' 629 | # we allow locking to fail, so as to allow 630 | # at least a single instance of burpfs, 631 | # even if we can't lock the conf file 632 | try: 633 | f = open(self.conf, 'r') 634 | fcntl.flock(f, fcntl.LOCK_EX) 635 | return f 636 | except: 637 | self.logger.warning(traceback.format_exc()) 638 | return None 639 | 640 | def _burp_funlock(self, f): 641 | ''' 642 | unlock the file f 643 | ''' 644 | if not f: 645 | return 646 | try: 647 | fcntl.flock(f, fcntl.LOCK_UN) 648 | f.close() 649 | except: 650 | self.logger.warning(traceback.format_exc()) 651 | 652 | def _burp(self, items): 653 | ''' 654 | restore list of items from Burp backup 655 | ''' 656 | # we serialize calls to burp across instances of burpfs 657 | # by locking the configuration file 658 | # (note that this may not work over NFS) 659 | f = self._burp_flock() 660 | cmd_prefix = [self.burp, '-c', self.conf] 661 | if self.client: 662 | cmd_prefix += ['-C', self.client] 663 | cmd_prefix += ['-a', 'r', '-f', '-b', str(self.backup), '-d', self.cache.path, '-r'] 664 | for item in items: 665 | cmd = cmd_prefix + [item] 666 | self.logger.debug('$ %s' % ' '.join(cmd)) 667 | self._burp_set_status({'regex': item, 668 | 'state': 'run'}) 669 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 670 | stdout, stderr = p.communicate() 671 | if sys.version_info.major >= 3: 672 | stdout = stdout.decode('utf-8') 673 | stderr = stderr.decode('utf-8') 674 | self.logger.debug('%s%s' % (stdout, stderr)) 675 | self._burp_set_status(FileSystem.burp_done) 676 | # unlock the configuration file 677 | self._burp_funlock(f) 678 | 679 | def _setup_logging(self): 680 | ''' 681 | initialize logging facility 682 | ''' 683 | # log messages are sent to both console and syslog 684 | # use -o logging=level to set the log level 685 | # use -o syslog to enable logging to syslog 686 | self.logger = logging.getLogger('BurpFS') 687 | self.loglevel = LOGGING_LEVELS.get(self.logging, logging.NOTSET) 688 | self.logger.setLevel(self.loglevel) 689 | h = logging.StreamHandler() 690 | h.setLevel(self.loglevel) 691 | formatter = logging.Formatter("%(message)s") 692 | h.setFormatter(formatter) 693 | self.logger.addHandler(h) 694 | if self.syslog: 695 | try: 696 | h = logging.handlers.SysLogHandler('/dev/log') 697 | h.setLevel(self.loglevel) 698 | formatter = logging.Formatter( 699 | "%(name)s[%(process)d]: %(levelname)-8s - %(message)s") 700 | h.setFormatter(formatter) 701 | self.logger.addHandler(h) 702 | except: 703 | self.logger.warning(traceback.format_exc()) 704 | self.logfile = LogFile(self.logger, logging.DEBUG) 705 | 706 | def _list_files(self, client, backup, timespec): 707 | ''' 708 | get list of files in backup 709 | ''' 710 | cmd_prefix = [self.burp, '-c', self.conf] 711 | if client: 712 | cmd_prefix += ['-C', client] 713 | elif self.parser.burp_version() >= '2': 714 | # deduce client name 715 | with open(self.conf, 'r') as conf: 716 | for line in conf: 717 | match = re.search(r'^\s*cname\s*=\s*([^\s]*)', line) 718 | if match: 719 | client = '%s' % match.group(1) 720 | break 721 | 722 | cmd = cmd_prefix + ['-a', 'l'] 723 | self.logger.debug('Getting list of backups with: %s' % ' '.join(cmd)) 724 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 725 | stdout, stderr = p.communicate() 726 | if sys.version_info.major >= 3: 727 | stdout = stdout.decode('utf-8') 728 | stderr = stderr.decode('utf-8') 729 | self.logger.debug('%s' % stdout) 730 | matches = re.finditer((r'^Backup: ([0-9]{7}) ' + 731 | r'([0-9]{4})-([0-9]{2})-([0-9]{2}) ' + 732 | r'([0-9]{2}):([0-9]{2}):([0-9]{2})' + 733 | r'(?! .*\(working\))'), 734 | stdout, re.MULTILINE) 735 | if matches: 736 | available_backups = [ 737 | (int(match.group(1)), 738 | datetime.strptime( 739 | '%s-%s-%s %s:%s:%s' % (match.group(2), 740 | match.group(3), 741 | match.group(4), 742 | match.group(5), 743 | match.group(6), 744 | match.group(7)), 745 | FileSystem.datetime_format)) for match in matches 746 | ] 747 | 748 | if not matches or not available_backups: 749 | self.logger.error('%s%s' % (stdout, stderr)) 750 | raise RuntimeError('cannot determine list of available backups') 751 | 752 | backup_ids, backup_dates = list(zip(*available_backups)) 753 | ibackup = None 754 | nbackup = None 755 | 756 | if backup: 757 | ibackup = int(backup) 758 | nbackup = bisect_left(backup_ids, ibackup) 759 | if nbackup == len(backup_ids) or backup_ids[nbackup] != ibackup: 760 | raise ValueError('backup must be one of %s' % repr(backup_ids)) 761 | else: 762 | ibackup = backup_ids[-1] # latest (assume list is sorted by date) 763 | nbackup = -1 764 | if timespec: 765 | query_date = datetime.strptime(timespec, FileSystem.datetime_format) 766 | nbackup = bisect_right(backup_dates, query_date) 767 | if not nbackup: 768 | raise RuntimeError('no backup found upto %s' % query_date) 769 | nbackup -= 1 770 | ibackup = backup_ids[nbackup] 771 | 772 | if not ibackup: 773 | raise RuntimeError('could not determine backup number') 774 | 775 | backup_date = datetime.strftime(backup_dates[nbackup], FileSystem.datetime_format) 776 | self.logger.info('Backup: %07d %s' % (ibackup, backup_date)) 777 | 778 | if self.parser.burp_version() >= '2': 779 | cmd = cmd_prefix + ['-a', 'm'] 780 | self.logger.debug('Querying burp monitor for list of files in backup with: %s' % ' '.join(cmd)) 781 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 782 | inp = 'j:pretty-print-off' 783 | self.logger.debug(inp) 784 | p.stdin.write(('%s\n' % inp).encode('utf-8')) 785 | p.stdin.flush() 786 | ready = False 787 | while p.poll() is None and not ready: 788 | line = p.stdout.readline().decode('utf-8').rstrip('\n') 789 | self.logger.debug(line) 790 | ready = 'pretty print off' in line.lower() 791 | if not ready: 792 | raise RuntimeError('burp monitor terminated - please verify that the server is configured to allow remote status monitor (hint: "status_address=::")') 793 | inp = 'c:%s:b:%d:p:*' % (client, ibackup) 794 | self.logger.debug(inp) 795 | p.stdin.write(('%s\n' % inp).encode('utf-8')) 796 | p.stdin.flush() 797 | json_string = p.stdout.readline().decode('utf-8') 798 | p.stdin.close() 799 | else: 800 | cmd = cmd_prefix + ['-b', '%d' % ibackup, '-a', 'L', '-j'] 801 | self.logger.debug('Getting list of files in backup with: %s' % ' '.join(cmd)) 802 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 803 | stdout, stderr = p.communicate() 804 | json_string = '\n'.join( 805 | [line for line in stdout.splitlines() 806 | if not re.match(('^([0-9]{4})-([0-9]{2})-([0-9]{2}) ' + 807 | '([0-9]{2}):([0-9]{2}):([0-9]{2}):.*'), line)]) 808 | 809 | if sys.version_info.major < 3: 810 | backup = json.loads(json_string, object_hook=_decode_dict, strict=False) 811 | else: 812 | backup = json.loads(json_string, strict=False) 813 | if self.parser.burp_version() >= '2': 814 | # burp 2.x.x 815 | files = backup['clients'][0]['backups'][0]['browse']['entries'] 816 | elif 'backups' in backup: 817 | # burp 1.4.x 818 | files = backup['backups'][0]['items'] 819 | else: 820 | # burp 1.3.x 821 | files = backup['items'] 822 | 823 | diff_epoch = 0 824 | if nbackup != 0 and len(backup_dates) > 1: 825 | diff_epoch = time.mktime(backup_dates[nbackup - 1].timetuple()) 826 | 827 | return files, ibackup, backup_date, diff_epoch 828 | 829 | def _json_to_stat(self, item): 830 | ''' 831 | convert JSON file entry tokens into file stat structure 832 | ''' 833 | st = fuse.Stat(st_mode=item[self.stat_field_prefix + 'mode'], 834 | st_ino=item[self.stat_field_prefix + 'ino'], 835 | st_dev=item[self.stat_field_prefix + 'dev'], 836 | st_nlink=item[self.stat_field_prefix + 'nlink'], 837 | st_uid=item[self.stat_field_prefix + 'uid'], 838 | st_gid=item[self.stat_field_prefix + 'gid'], 839 | st_size=item[self.stat_field_prefix + 'size'], 840 | st_atime=item[self.stat_field_prefix + 'atime'], 841 | st_mtime=item[self.stat_field_prefix + 'mtime'], 842 | st_ctime=item[self.stat_field_prefix + 'ctime'], 843 | st_blksize=0, 844 | st_rdev=0) 845 | return st 846 | 847 | def _create_file_entry(self, item): 848 | ''' 849 | create file entry tuple from a JSON file entry 850 | also splits file path into head and tail 851 | ''' 852 | path = item['name'] 853 | if not path.startswith('/'): 854 | path = '/' + path 855 | under_root = False 856 | else: 857 | under_root = True 858 | head, tail = self._split(path) 859 | target = item['link'] if len(item['link']) > 0 else None 860 | hardlink = (target is not None) and (not stat.S_ISLNK(item[self.stat_field_prefix + 'mode'])) 861 | entry = FileSystem._Entry(self, 862 | path, 863 | stat=self._json_to_stat(item), 864 | link_target=target, 865 | under_root=under_root, 866 | hardlink=hardlink) 867 | return head, tail, entry 868 | 869 | def initialize(self): 870 | ''' 871 | initialize file list 872 | ''' 873 | 874 | self._setup_logging() 875 | 876 | self.logger.info('Populating file system ... ') 877 | self.cache = FileSystem._Cache(self) 878 | self.stat_field_prefix = '' if self.parser.burp_version() >= '2' else 'st_' 879 | 880 | # test access to burp conf file 881 | open(self.conf, 'r').close() 882 | 883 | # get list of files in backup 884 | files, backup, timespec, diff_epoch = self._list_files(self.client, 885 | self.backup, 886 | self.datetime) 887 | # validated values 888 | self.backup = backup 889 | self.datetime = timespec 890 | 891 | # are we using inode numbers 892 | self.use_ino = 'use_ino' in self.fuse_args.optlist 893 | 894 | # build dirs data structure 895 | num_entries = 0 896 | for file in files: 897 | head, tail, entry = self._create_file_entry(file) 898 | path = head + tail 899 | # find max st_ino 900 | if self.use_ino: 901 | if entry.stat.st_ino > self.max_ino: 902 | self.max_ino = entry.stat.st_ino 903 | # new directory 904 | if head not in self.dirs: 905 | self.dirs[head] = {} 906 | # is entry a directory itself? 907 | isdirectory = stat.S_ISDIR(entry.stat.st_mode) 908 | if (isdirectory and 909 | path + '/' not in self.dirs): 910 | self.dirs[path + '/'] = {} 911 | # add parent directories 912 | self._add_parent_dirs(head) 913 | # maybe skip unmodified files 914 | if (self.diff and 915 | not isdirectory and 916 | entry.stat.st_mtime < diff_epoch): 917 | continue 918 | # and finally 919 | self.dirs[head][tail] = entry 920 | if entry.stat.st_mtime >= diff_epoch: 921 | num_entries += 1 922 | 923 | # fix st_ino 924 | if self.use_ino: 925 | self._update_inodes('/') 926 | 927 | self.logger.debug('Cache directory is: %s' % self.cache.prefix) 928 | if not self.diff: 929 | self.logger.info('BurpFS ready (%d items).' % len(files)) 930 | else: 931 | self.logger.info('BurpFS ready (%d modified items out of %d).' % (num_entries, len(files))) 932 | 933 | self._initialized = True 934 | 935 | def shutdown(self): 936 | ''' 937 | remove cache directory if required 938 | ''' 939 | if self.cache: 940 | del self.cache 941 | 942 | def setxattr(self, path, name, value, flags): 943 | ''' 944 | set value of extended attribute 945 | ''' 946 | return -errno.EOPNOTSUPP 947 | 948 | def getxattr(self, path, name, size): 949 | ''' 950 | get value of extended attribute 951 | burpfs exposes some filesystem attributes for the root directory 952 | (e.g. backup number, cache prefix - see FileSystem.xattr_fields_root) 953 | and may also expose several other attributes for each file/directory 954 | in the future (see FileSystem.xattr_fields) 955 | ''' 956 | head, tail = self._split(path) 957 | val = None 958 | n = name.replace(FileSystem.xattr_prefix, '') 959 | if path == '/': 960 | if n in FileSystem.xattr_fields_root: 961 | val = str(getattr(self, n)) 962 | elif n.startswith('burp.'): 963 | n = n.replace('burp.', '') 964 | if n in FileSystem.xattr_fields_burp: 965 | val = str(self._burp_get_status()[n]) 966 | elif n.startswith('cache.'): 967 | n = n.replace('cache.', '') 968 | if n in FileSystem.xattr_fields_cache: 969 | val = str(getattr(self.cache, n)) 970 | if (not val and head in self.dirs and tail in self.dirs[head] and 971 | n in FileSystem.xattr_fields): 972 | # place holder in case we add xattrs 973 | pass 974 | # attribute not found 975 | if val is None: 976 | return -errno.ENODATA 977 | # We are asked for size of the value. 978 | if size == 0: 979 | return len(val) 980 | return val 981 | 982 | def listxattr(self, path, size): 983 | ''' 984 | list extended attributes 985 | ''' 986 | head, tail = self._split(path) 987 | xattrs = [] 988 | if path == '/': 989 | xattrs += [FileSystem.xattr_prefix + 990 | a for a in FileSystem.xattr_fields_root] 991 | xattrs += [FileSystem.xattr_prefix + 'burp.' + 992 | a for a in FileSystem.xattr_fields_burp] 993 | xattrs += [FileSystem.xattr_prefix + 'cache.' + 994 | a for a in FileSystem.xattr_fields_cache] 995 | if head in self.dirs and tail in self.dirs[head]: 996 | xattrs += [FileSystem.xattr_prefix + 997 | a for a in FileSystem.xattr_fields] 998 | # We are asked for size of the attr list, ie. joint size of attrs 999 | # plus null separators. 1000 | if size == 0: 1001 | return len("".join(xattrs)) + len(xattrs) 1002 | return xattrs 1003 | 1004 | def getattr(self, path): 1005 | ''' 1006 | Retrieve file attributes. 1007 | ''' 1008 | head, tail = self._split(path) 1009 | if head in self.dirs and tail in self.dirs[head]: 1010 | return self.dirs[head][tail].stat 1011 | else: 1012 | return -errno.ENOENT 1013 | 1014 | def readdir(self, path, offset): 1015 | ''' 1016 | read directory entries 1017 | ''' 1018 | path = path if path.endswith('/') else path + '/' 1019 | for key in ['.', '..']: 1020 | yield fuse.Direntry(key) 1021 | for key in list(self.dirs[path].keys()): 1022 | if len(key) > 0: 1023 | if self.use_ino: 1024 | bs = self.getattr(path + key) 1025 | ino = bs.st_ino 1026 | else: 1027 | ino = 0 1028 | yield fuse.Direntry(key, ino=ino) 1029 | 1030 | def readlink(self, path): 1031 | ''' 1032 | read link contents 1033 | ''' 1034 | head, tail = self._split(path) 1035 | link = self.dirs[head][tail].link_target 1036 | if link: 1037 | if self.move_root and link.startswith('/'): 1038 | link = os.path.normpath(self.fuse_args.mountpoint + link) 1039 | return link 1040 | return -errno.ENOENT 1041 | 1042 | class _File(object): 1043 | def __init__(self, fs, path, flags, *mode): 1044 | self.fs = fs 1045 | accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR 1046 | if (flags & accmode) != os.O_RDONLY: 1047 | raise IOError(errno.EACCES, '') 1048 | self.path = path 1049 | self.realpath = fs._extract([path])[0] 1050 | self.vss_offset, self.vss_overhead = fs._vss_parse(self.realpath, path) 1051 | self.file = os.fdopen(os.open(self.realpath, flags, *mode), 1052 | flag2mode(flags)+'b') 1053 | self.fd = self.file.fileno() 1054 | self.direct_io = False 1055 | self.keep_cache = True 1056 | 1057 | def read(self, length, offset): 1058 | self.file.seek(offset + self.vss_offset) 1059 | return self.file.read(length) 1060 | 1061 | def release(self, flags): 1062 | self.file.close() 1063 | self.fs.cache.close(self.path) 1064 | 1065 | 1066 | class BurpFuseOptParse(FuseOptParse): 1067 | ''' 1068 | We subclass FuseOptParse just so that we can honor the -o burp 1069 | command line option 1070 | ''' 1071 | def __init__(self, *args, **kw): 1072 | self._burp_version = None 1073 | FuseOptParse.__init__(self, *args, **kw) 1074 | 1075 | def get_version(self): 1076 | return ("BurpFS version: %s\nburp version: %s\n" 1077 | "Python FUSE version: %s" % 1078 | (__version__, self.burp_version(), fuse.__version__)) 1079 | 1080 | def burp_version(self): 1081 | ''' 1082 | return version string of burp, 1083 | return None if not runnable or version cannot be parsed 1084 | ''' 1085 | if not self._burp_version: 1086 | try: 1087 | # burp version command line option changed from -v to 1088 | # -V on 2.2.12 so we try both ... 1089 | for version_opt in ['v', 'V']: 1090 | cmd = [self.values.burp, '-c', self.values.conf, '-%s' % version_opt] 1091 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 1092 | stdout, stderr = p.communicate() 1093 | if sys.version_info.major >= 3: 1094 | stdout = stdout.decode('utf-8') 1095 | stderr = stderr.decode('utf-8') 1096 | match = re.search('burp-(.*)\n$', stdout) 1097 | if match: 1098 | self._burp_version = '%s' % match.group(1) 1099 | break 1100 | except: 1101 | # traceback.print_exc() 1102 | pass 1103 | return self._burp_version 1104 | 1105 | 1106 | def main(): 1107 | 1108 | usage = """ 1109 | BurpFS: exposes the Burp backup storage as a Filesystem in USErspace 1110 | 1111 | """ + Fuse.fusage 1112 | 1113 | # force -o sync_read 1114 | sys.argv.extend(['-o', 'sync_read']) 1115 | 1116 | server = FileSystem( 1117 | version=__version__, 1118 | parser_class=BurpFuseOptParse, 1119 | usage=usage) 1120 | 1121 | server.multithreaded = True 1122 | 1123 | server.parser.add_option(mountopt="burp", 1124 | metavar="PATH", 1125 | default=server.burp, 1126 | help=("path to burp executable " 1127 | "[default: %default]")) 1128 | server.parser.add_option(mountopt="conf", 1129 | metavar="PATH", 1130 | default=server.conf, 1131 | help=("client configuration file " 1132 | "[default: %default]")) 1133 | server.parser.add_option(mountopt="client", 1134 | metavar="CNAME", 1135 | default=server.client, 1136 | help=("client cname " 1137 | "[default: local client]")) 1138 | server.parser.add_option(mountopt="backup", 1139 | metavar="BACKUP", 1140 | default=server.backup, 1141 | help=("backup number " 1142 | "[default: the most recent backup]")) 1143 | server.parser.add_option(mountopt="datetime", 1144 | metavar="'YYYY-MM-DD hh:mm:ss'", 1145 | default=server.datetime, 1146 | help="backup snapshot date/time [default: now]") 1147 | server.parser.add_option(mountopt="diff", 1148 | action="store_true", 1149 | default=server.diff, 1150 | help=("populate file system only with " 1151 | "modified/new files [default: %default]")) 1152 | server.parser.add_option(mountopt="cache_num_files", 1153 | metavar="N", 1154 | default=server.cache_num_files, 1155 | help=("maximal number of files in cache " 1156 | "[default: %default]")) 1157 | server.parser.add_option(mountopt="cache_total_size", 1158 | metavar="MB", 1159 | default=server.cache_total_size, 1160 | help=("maximal total size (MB) of files in cache " 1161 | "[default: %default]")) 1162 | server.parser.add_option(mountopt="move_root", 1163 | action="store_true", 1164 | default=server.move_root, 1165 | help=("make absolute path symlinks point to path " 1166 | "under mount point [default: %default]")) 1167 | server.parser.add_option(mountopt="logging", 1168 | choices=list(LOGGING_LEVELS.keys()), 1169 | metavar='|'.join(list(LOGGING_LEVELS.keys())), 1170 | default=server.logging, 1171 | help="logging level [default: %default]") 1172 | server.parser.add_option(mountopt="syslog", 1173 | action="store_true", 1174 | default=server.syslog, 1175 | help=("log to both syslog and console [default: " 1176 | "%default]")) 1177 | 1178 | server.parse(values=server, errex=1) 1179 | 1180 | if server.fuse_args.mount_expected(): 1181 | if not server.parser.burp_version(): 1182 | raise RuntimeError('cannot determine burp version - ' 1183 | 'is it installed?') 1184 | else: 1185 | # we initialize before main (i.e. not in fsinit) so that 1186 | # any failure here aborts the mount 1187 | try: 1188 | server.initialize() 1189 | except: 1190 | server.shutdown() 1191 | raise 1192 | 1193 | server.main() 1194 | 1195 | # we shutdown after main, i.e. not in fsshutdown, because 1196 | # calling fsshutdown with multithreaded==True seems to cause 1197 | # the python fuse process to hang waiting for the python GIL 1198 | if server.fuse_args.mount_expected(): 1199 | server.shutdown() 1200 | -------------------------------------------------------------------------------- /burpfs/LogFile.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # BurpFS - Burp Filesystem in USErspace 4 | # Copyright (C) 2012-2021 Avi Rozen 5 | # 6 | # This file is part of BurpFS. 7 | # 8 | # BurpFS is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | import logging 22 | import logging.handlers 23 | 24 | LOGGING_LEVELS = {'debug': logging.DEBUG, 25 | 'info': logging.INFO, 26 | 'warning': logging.WARNING, 27 | 'error': logging.ERROR, 28 | 'critical': logging.CRITICAL} 29 | 30 | 31 | class LogFile: 32 | ''' 33 | file like object that wraps a logger object 34 | ''' 35 | def __init__(self, logger, level): 36 | self.logger = logger 37 | self.level = level 38 | self.tail = '' 39 | 40 | def write(self, message): 41 | lines = (self.tail + message).splitlines() 42 | for line in lines[:-1]: 43 | self.logger.log(self.level, line) 44 | if message.endswith('\n'): 45 | self.logger.log(self.level, lines[-1]) 46 | self.tail = '' 47 | elif len(lines) > 0: 48 | self.tail = lines[-1] 49 | else: 50 | self.tail = '' 51 | 52 | def flush(self, flush_tail=False): 53 | if flush_tail and self.tail: 54 | self.logger.log(self.level, self.tail) 55 | self.tail = '' 56 | for handler in self.logger.handlers: 57 | handler.flush() 58 | -------------------------------------------------------------------------------- /burpfs/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # BurpFS - Burp Filesystem in USErspace 4 | # Copyright (C) 2012-2021 Avi Rozen 5 | # 6 | # This file is part of BurpFS. 7 | # 8 | # BurpFS is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | """Expose the Burp backups as a FUSE file system. 22 | """ 23 | from .FileSystem import __version__, main 24 | 25 | __all__ = ["FileSystem", "LogFile"] 26 | -------------------------------------------------------------------------------- /burpfs/burpfs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # BurpFS - Burp Filesystem in USErspace 4 | # Copyright (C) 2012-2019 Avi Rozen 5 | # 6 | # This file is part of BurpFS. 7 | # 8 | # BurpFS is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | '''BurpFS mount script 22 | ''' 23 | 24 | from FileSystem import main 25 | 26 | if __name__ == '__main__': 27 | main() 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | # BurpFS - Burp Filesystem in USErspace 4 | # Copyright (C) 2012-2021 Avi Rozen 5 | # 6 | # This file is part of BurpFS. 7 | # 8 | # BurpFS is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program. If not, see . 20 | 21 | from setuptools import setup, find_packages 22 | from burpfs import __version__ 23 | 24 | author='Avi Rozen' 25 | author_email='avi.rozen@gmail.com' 26 | 27 | setup( 28 | name='BurpFS', 29 | version=__version__, 30 | description='Burp Filesystem in USErspace', 31 | long_description=open('README.rst').read(), 32 | author=author, 33 | author_email=author_email, 34 | maintainer=author, 35 | maintainer_email=author_email, 36 | url='https://github.com/ZungBang/burpfs', 37 | entry_points = { 'console_scripts': [ 'burpfs = burpfs:main' ] }, 38 | packages = find_packages(), 39 | license='GPL', 40 | platforms=['Linux'], 41 | install_requires=['fuse-python>=1.0.0'], 42 | classifiers = [ 43 | "Development Status :: 3 - Alpha", 44 | "Topic :: System :: Filesystems", 45 | "Topic :: System :: Archiving :: Backup", 46 | "Intended Audience :: System Administrators", 47 | "Environment :: No Input/Output (Daemon)", 48 | "License :: OSI Approved :: GNU General Public License (GPL)", 49 | "Operating System :: POSIX :: Linux", 50 | "Programming Language :: Python", 51 | ], 52 | ) 53 | --------------------------------------------------------------------------------