diff --git a/.github/workflows/test-with-custom-root.yml b/.github/workflows/test-with-custom-root.yml new file mode 100644 index 0000000..40b7b2b --- /dev/null +++ b/.github/workflows/test-with-custom-root.yml @@ -0,0 +1,66 @@ +name: Test with custom root + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + ext2: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7.7 + - name: Install e2fsprogs + run: sudo apt-get install e2fsprogs + - name: Setup filesystem + run: sudo ./test.sh setup ext2 + - name: Test + run: ./test.sh run /test/dir/ + - name: Clear filesystem + run: sudo ./test.sh clear + + ext3: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7.7 + - name: Install e2fsprogs + run: sudo apt-get install e2fsprogs + - name: Setup filesystem + run: sudo ./test.sh setup ext3 + - name: Test + run: ./test.sh run /test/dir/ + - name: Clear filesystem + run: sudo ./test.sh clear + + ext4: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7.7 + - name: Install e2fsprogs + run: sudo apt-get install e2fsprogs + - name: Setup filesystem + run: sudo ./test.sh setup ext4 + - name: Test + run: ./test.sh run /test/dir/ + - name: Clear filesystem + run: sudo ./test.sh clear diff --git a/README.md b/README.md index f9d82e0..c9e1e28 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ optional arguments: ``` ## Example -**Create snapshot.** It creates snapshot of filesystem image `data_fs.img` and saves to `data_fs.img.snapshot.out` (if not otherwise specified, using `-o`). +**Create snapshot.** It creates snapshot of filesystem image `data_fs.img` and saves to `data_fs.img.snapshot.out` (if not otherwise specified, using `-o`). You can specify your own root directory, using `-root /custom`. ``` $ ext4-backup-pointers create -i data_fs.img @@ -83,7 +83,8 @@ from src.utils import generate_snapshot, recover_file generate_snapshot( fs="data_fs.img", # Filesystem image file snapshot_file="snapshot.out", # Path, where will be snapshot metadata file created - dirs_max_depth=100 # Max directory traversal depth + root_path="/", # Root directory of backup + dirs_max_depth=100 # Max directory traversal depth from root ) recover_file( fs="data_fs.img", # Filesystem image file diff --git a/src/console.py b/src/console.py index 42a16f8..ee48354 100644 --- a/src/console.py +++ b/src/console.py @@ -15,6 +15,7 @@ def create(args): snapshot = generate_snapshot( fs=args.input, snapshot_file=snapshot_file, + root_path=args.root_path, dirs_max_depth=args.dirs_max_depth ) @@ -65,6 +66,7 @@ def start(): parser_create = subparsers.add_parser('create', help='create metadata snapshot') parser_create.add_argument('-i', '--input', type=str, help='image of file system', required=True) parser_create.add_argument('-o', '--output', type=str, help='metadata snapshot output file', required=False) + parser_create.add_argument('-root', '--root-path', type=str, help='root directory of backup', required=False, default='/') parser_create.add_argument('-depth', '--dirs-max-depth', type=int, help='maximum depth of directory traversal', required=False, default=100) parser_create.set_defaults(func=create) diff --git a/src/utils.py b/src/utils.py index 398b823..b30b0f6 100644 --- a/src/utils.py +++ b/src/utils.py @@ -148,34 +148,6 @@ def parse_bitmap(data, total_items=None): return indexes -# [ 'inode_id': 'physical_addr', ... ] -def get_inodes_addresses(fs, sb=None): - # get blocks - bgs = get_block_groups(fs) - - # get inodes with their physical address - inodes = {} - for bg in bgs: - # only initialized inodes - if test_block_groups_flag(sb, 'INODE_UNINIT', bg['group']): - continue - - # get & parse bitmap - bitmap = read_blocks(fs, sb['Block size'], bg['ibitmap']) - indexes = parse_bitmap(bitmap, sb['Inodes per group']) - - # get base address for inode table in this bg - base_address = bg['itable'] * sb['Block size'] - - # loop through used inodes in this bg - for index in indexes: - inode_id = index + (sb['Inodes per group'] * bg['group']) + 1 - raw_address = (index * sb['Inode size']) + base_address - - inodes[inode_id] = raw_address - - return inodes - # # INODE # @@ -253,6 +225,23 @@ def inode_parse(data): 'size': join_int32(i_size_high, i_size_lo) } +# get inode by inode id +def get_inode_by_inode_id(fs, sb, inode_id): + # get blocks + bgs = get_block_groups(fs) + + # get bg where inode is + bg_index = (inode_id - 1) // sb['Inodes per group'] + bg = bgs[bg_index] + + # get offet and addr in itable + index = (inode_id - 1) % sb['Inodes per group'] + inode_addr = (index * sb['Inode size']) + bg['itable'] * sb['Block size'] + + # get and parse inode data + inode_data = read_blocks(fs, 1, inode_addr, sb['Inode size']) + return inode_parse(inode_data) + # # EXTENT TREE STRUCTS # @@ -583,6 +572,43 @@ def readdir_from_chunks(fs, sb, chunks, size=None): fh.close() return entries +# read dir by inode_id +def readdir(fs, sb, inode_id): + inode = get_inode_by_inode_id(fs, sb, inode_id) + + # read all chunks and get dir list + size, chunks = inode_to_chunks(fs, sb, inode) + dir_list = readdir_from_chunks(fs, sb, chunks, size) + + return dir_list + +# get inode id of given path +def get_inode_id_of_path(fs, sb, dir_path): + # split dir to names + names = dir_path.split("/") + # remove empty strings + names = [x for x in names if x] + + inode_id = 2 + for name in names: + entries = readdir(fs, sb, inode_id) + + # find name in entries + found_entry = None + for entry in entries: + if entry['name'] == name: + found_entry = entry + break + + if found_entry is None: + raise Exception('Directory `' + name + '` was not found.') + + # ext3 compatibility + assert entry['filetype'] is None or entry['filetype'] == 'S_IFDIR' + inode_id = entry['inode'] + + return inode_id + # # CHECKSUM # @@ -734,31 +760,21 @@ def load_snapshot(file_path): return snapshot # generate snapshot from fs -def generate_snapshot(fs, snapshot_file, dirs_max_depth=100): +def generate_snapshot(fs, snapshot_file, dirs_max_depth=100, root_path='/'): sb = get_super(fs) - inodes = get_inodes_addresses(fs, sb) + # normalize root path + if root_path != '/': + if not root_path.endswith('/'): + root_path += '/' + if not root_path.startswith('/'): + root_path = '/' + root_path + + # start indexing from root inode + root_inode = get_inode_id_of_path(fs, sb, root_path) + inodes = [ { 'prefix': root_path, 'inode': root_inode } ] + dir_entries = {} files_chunks = {} - dirs_chunks = {} - for inode_id, inode_addr in inodes.items(): - inode_data = read_blocks(fs, 1, inode_addr, sb['Inode size']) - inode = inode_parse(inode_data) - - # only regular files - if inode['filetype'] == 'S_IFREG' and inode_id > 11: - size, chunks = inode_to_chunks(fs, sb, inode) - files_chunks[inode_id] = size, chunks - continue - - # save directories - if inode['filetype'] == 'S_IFDIR': - size, chunks = inode_to_chunks(fs, sb, inode) - dirs_chunks[inode_id] = size, chunks - continue - - # root inode is 2 - inodes = [ { 'prefix': '/', 'inode': 2 } ] - entries = {} # loop through dirs for depth in range(dirs_max_depth): @@ -768,22 +784,29 @@ def generate_snapshot(fs, snapshot_file, dirs_max_depth=100): last_inodes, inodes = inodes, [] for entry in last_inodes: prefix = entry['prefix'] - inode = entry['inode'] + inode_id = entry['inode'] - if not inode in dirs_chunks: - continue + # get inode + inode = get_inode_by_inode_id(fs, sb, inode_id) - # pop from dict - size, chunks = dirs_chunks[inode] - del dirs_chunks[inode] + # we are looping only through direcotories + assert inode['filetype'] == 'S_IFDIR' + # get chunks + size, chunks = inode_to_chunks(fs, sb, inode) for entry in readdir_from_chunks(fs, sb, chunks, size): if entry['name'] == '.' or entry['name'] == '..': continue # only regular files if entry['filetype'] == 'S_IFREG': - entries[prefix + entry['name']] = entry['inode'] + # save dir entry + dir_entries[prefix + entry['name']] = entry['inode'] + + # save files chunks + inode = get_inode_by_inode_id(fs, sb, entry['inode']) + size, chunks = inode_to_chunks(fs, sb, inode) + files_chunks[entry['inode']] = size, chunks continue # add dir to next iteration @@ -794,7 +817,7 @@ def generate_snapshot(fs, snapshot_file, dirs_max_depth=100): }) save_snapshot(snapshot_file, { - 'dirs': entries, + 'dirs': dir_entries, 'inodes': files_chunks }) diff --git a/test.sh b/test.sh index 05384aa..4fe25b2 100755 --- a/test.sh +++ b/test.sh @@ -163,8 +163,15 @@ remove_file() { # CREATE SNAPSHOT # snapshot() { - ext4-backup-pointers create -i data_fs.img.test -o snapshot.out.test - CATCH "[OK] Created snapshot." + # set custom root + if [ -z "$1" ]; then + ROOT="/" + else + ROOT="$1" + fi + + ext4-backup-pointers create -i data_fs.img.test -o snapshot.out.test -root "$ROOT" + CATCH "[OK] Created snapshot with root: $ROOT" } # @@ -223,7 +230,7 @@ case $1 in fi install create_file - snapshot + snapshot "$2" remove_file restore ;; @@ -253,7 +260,7 @@ case $1 in echo '' echo './test.sh setup [ext4] # setup test filesystem. (run as root)' echo ' # - optional: (ext2, ext3, ext4)' - echo './test.sh run # run rests.' + echo './test.sh run [/] # run rests with FS root path.' echo './test.sh clear # clear test filesystem. (run as root)' echo './test.sh full [ext4] # create & test & clear. (run as root)' echo ' # - optional: (ext2, ext3, ext4)' @@ -262,7 +269,7 @@ case $1 in echo '' echo './test.sh install # python install src.' echo './test.sh create_file # create test file.' - echo './test.sh snapshot # create snapshot.' + echo './test.sh snapshot [/] # create snapshot, specify FS root path.' echo './test.sh remove_file # remove test file.' echo './test.sh restore # restore removed file from snapshot.' echo ''