Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/test-with-custom-root.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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)

Expand Down
139 changes: 81 additions & 58 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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
})

Expand Down
17 changes: 12 additions & 5 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

#
Expand Down Expand Up @@ -223,7 +230,7 @@ case $1 in
fi
install
create_file
snapshot
snapshot "$2"
remove_file
restore
;;
Expand Down Expand Up @@ -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)'
Expand All @@ -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 ''
Expand Down