diff --git a/example.yaml b/example.yaml index 7279c42..ab5ca46 100644 --- a/example.yaml +++ b/example.yaml @@ -7,6 +7,7 @@ compress_method: "bzip2" # Optional. Default value: null. Supported value: "bzi # zstd support 0-22 (Default: 3). brotli support 0-11 (Default: unset). compress_level: 6 encrypt_db: false # Optional. Default value: false. Encrypt the database. Warning: The default python sqlite library don't support encrypt, it just ignore encrypt phases. +encrypt_files: false # Optional. Default value: false. Encrypt backup files. The key information will stored in database. programs: - name: Your program name # This name is used to identify different application. base: /path/to/save/path # Must be absoulte path. @@ -15,6 +16,7 @@ programs: ignore_hidden_files: true # Optional. compress_method: null # Optional. compress_level: null # Optional. + encrypt_files: false # Optional files: - BGI.gdb # path to a file/folder. All subfolders will include if it is a folder. Must be relative path. - type: path @@ -25,6 +27,7 @@ programs: ignore_hidden_files: true # Optional. compress_method: null # Optional. compress_level: null # Optional. + encrypt_files: false # Optional. excludes: # Optional. Exculde some files. Only effected when path is a folder. - data.db # Releative path - /path/to/data.db # Absolute path @@ -46,5 +49,6 @@ programs: remove_old_files: true # Optional. compress_method: null # Optional. compress_level: null # Optional. + encrypt_files: false # Optional. domains: # optional. Just backup minor domains in localstorage database. Only chromium is tested. - some domain diff --git a/game_backuper/backuper.py b/game_backuper/backuper.py index aca6ae4..16e6c2a 100644 --- a/game_backuper/backuper.py +++ b/game_backuper/backuper.py @@ -12,8 +12,9 @@ from os import mkdir, remove, close from game_backuper.file import new_file, copy_file, File, mkdir_for_file from game_backuper.filetype import FileType from game_backuper.restorer import RestoreTask -from game_backuper.file import remove_compress_files +from game_backuper.file import remove_compress_files, remove_unencryped_files from game_backuper.compress import compress +from game_backuper.enc import encrypt_file from tempfile import mkstemp @@ -28,6 +29,7 @@ class BackupTask(Thread): self.prog.clear_cache() prog = self.prog.name bp = join(self.cfg.dest, prog) + ebp = join(self.cfg.dest, '.encrypt', prog) if not exists(bp): mkdir(bp) fl = self.db.get_file_list(prog) @@ -41,31 +43,54 @@ class BackupTask(Thread): nf = new_file(f[1], f[0], prog) if nf is None: continue - de = join(bp, f[0]) + de = join(ebp if f.encrypt_files else bp, f[0]) if ori is not None: if ori.size == nf.size and ori.hash == nf.hash: if c is None: - if exists(de): + if exists(de) and not f.encrypt_files: print(f'{prog}: Skip {f[0]}.') remove_compress_files(de, prog, f.name) + self.remove_encrypted_file(join(ebp, f[0]), prog, f.name, ori) # noqa: E501 + continue + elif exists(de) and f.encrypt_files and not ori.compressed: # noqa: E501 + print(f'{prog}: Skip {f[0]}.') + remove_unencryped_files(join(bp, f[0]), prog, f.name) # noqa: E501 continue else: - if exists(de + c.ext): + if not f.encrypt_files and exists(de + c.ext): print(f'{prog}: Skip {f.name}.') remove_compress_files(de, prog, f.name, c.ext) # noqa: E501 + self.remove_encrypted_file(join(ebp, f[0]), prog, f.name, ori) # noqa: E501 continue - if c is None: + elif f.encrypt_files and ori.compressed_type == c.method: # noqa: E501 + print(f'{prog}: Skip {f.name}.') + remove_unencryped_files(join(bp, f.name), prog, f.name) # noqa: E501 + continue + stats = None + if f.encrypt_files: + stats = encrypt_file(f[1], de, nf, f.name, prog, c) + remove_unencryped_files(join(bp, f[0]), prog, f.name) # noqa: E501 + elif c is None: copy_file(f[1], de, f[0], prog) remove_compress_files(de, prog, f.name) + self.remove_encrypted_file(join(ebp, f[0]), prog, f.name, ori) # noqa: E501 else: compress(f[1], de, c, f.name, prog) + self.remove_encrypted_file(join(ebp, f[0]), prog, f.name, ori) # noqa: E501 self.db.set_file(ori.id, nf.size, nf.hash) + self.db.set_file_encrypt_information(ori.id, stats) else: - if c is None: + if f.encrypt_files: + s = encrypt_file(f[1], de, nf, f.name, prog, c) + nf = File.from_encrypt_stats(s, nf) + remove_unencryped_files(join(bp, f[0]), prog, f.name) # noqa: E501 + elif c is None: copy_file(f[1], de, f[0], prog) remove_compress_files(de, prog, f.name) + self.remove_encrypted_file(join(ebp, f[0]), prog, f.name, ori) # noqa: E501 else: compress(f[1], de, c, f.name, prog) + self.remove_encrypted_file(join(ebp, f[0]), prog, f.name, ori) # noqa: E501 self.db.add_file(nf) elif isinstance(f, ConfigLeveldb): from game_backuper.leveldb import have_leveldb @@ -84,47 +109,68 @@ class BackupTask(Thread): stats = leveldb_stats(f.full_path, ent) ori = self.db.get_file(prog, f.name) c = f.compress_config - de = join(bp, f.name + ".db") + de = join(ebp if f.encrypt_files else bp, f.name + ".db") if ori is not None: if ori.type is None or ori.type != FileType.LEVELDB: pp = join(bp, ori.file) if exists(pp): remove(pp) - remove_compress_files(pp, prog, f.name) + remove_compress_files(pp, prog, f.name) + self.remove_encrypted_file(join(ebp, ori.file), prog, f.name, ori) # noqa: E501 self.db.remove_file(ori) ori = None if ori is not None: if ori.size == stats.size and ori.hash == stats.hash: if c is None: - if exists(de): - print(f'{prog}: Skip {f[0]}') + if exists(de) and not f.encrypt_files: + print(f'{prog}: Skip {f[0]}.') remove_compress_files(de, prog, f.name) + self.remove_encrypted_file(join(ebp, f[0] + '.db'), prog, f.name, ori) # noqa: E501 + continue + elif exists(de) and f.encrypt_files and not ori.compressed: # noqa: E501 + print(f'{prog}: Skip {f[0]}.') + remove_unencryped_files(join(bp, f[0] + '.db'), prog, f.name) # noqa: E501 continue else: - if exists(de + c.ext): - print(f'{prog}: Skip {f.name}') + if not f.encrypt_files and exists(de + c.ext): + print(f'{prog}: Skip {f.name}.') remove_compress_files(de, prog, f.name, c.ext) + self.remove_encrypted_file(join(ebp, f[0] + '.db'), prog, f.name, ori) # noqa: E501 + continue + elif f.encrypt_files and ori.compressed_type == c.method: # noqa: E501 + print(f'{prog}: Skip {f.name}.') + remove_unencryped_files(join(bp, f.name + '.db'), prog, f.name) # noqa: E501 continue mkdir_for_file(de) + st = None if c is None: leveldb_to_sqlite(f.full_path, de, ent) print(f'{prog}: Covert leveldb done. {f.full_path}({f.name}) -> {de}') # noqa: E501 remove_compress_files(de, prog, f.name) + self.remove_encrypted_file(join(ebp, f[0] + '.db'), prog, f.name, ori) # noqa: E501 else: tmp = mkstemp() close(tmp[0]) tmp = tmp[1] leveldb_to_sqlite(f.full_path, tmp, ent) print(f'{prog}: Covert leveldb done. {f.full_path}({f.name}) -> {tmp}') # noqa: E501 - compress(tmp, de, c, f.name, prog) + if f.encrypt_files: + st = encrypt_file(tmp, de, File.from_leveldb_stats(stats), f.name, prog, c) # noqa: E501 + remove_unencryped_files(join(bp, f[0] + '.db'), prog, f.name) # noqa: E501 + else: + compress(tmp, de, c, f.name, prog) + self.remove_encrypted_file(join(ebp, f[0] + '.db'), prog, f.name, ori) # noqa: E501 remove(tmp) print(f'{prog}: Removed tempfile {tmp}') if ori is None: nf = File(None, f.name, stats.size, prog, stats.hash, - FileType.LEVELDB) + FileType.LEVELDB, None, None, None, None, None) + if st: + nf = File.from_encrypt_stats(st, nf) self.db.add_file(nf) else: self.db.set_file(ori.id, stats.size, stats.hash) + self.db.set_file_encrypt_information(ori.id, st) for fn in fl: f = self.db.get_file(prog, fn) if f.type is None: @@ -140,6 +186,11 @@ class BackupTask(Thread): print(f'{prog}: Remove {de}({fn})') self.db.remove_file(f) + def remove_encrypted_file(self, loc: str, prog: str, name: str, f: File): + if exists(loc): + remove(loc) + print(f'{prog}: Removed {loc}({name})') + class Backuper: def __init__(self, db: Db, config: Config, opts: Opts): diff --git a/game_backuper/compress.py b/game_backuper/compress.py index dad95f8..9f0ef62 100644 --- a/game_backuper/compress.py +++ b/game_backuper/compress.py @@ -174,6 +174,17 @@ class CompressMethod(IntEnum): else: raise TypeError('Must be str.') + def to_str(self) -> str: + return { + CompressMethod.BZIP2: 'bzip2', + CompressMethod.GZIP: 'gzip', + CompressMethod.LZMA: 'lzma', + CompressMethod.LZIP: 'lzip', + CompressMethod.ZSTD: 'zstd', + CompressMethod.SNAPPY: 'snappy', + CompressMethod.BROTLI: 'brotli', + }[self] + class CompressConfig: def __init__(self, method: str, level: int = None): diff --git a/game_backuper/config.py b/game_backuper/config.py index cb3cde0..92e3695 100644 --- a/game_backuper/config.py +++ b/game_backuper/config.py @@ -19,6 +19,7 @@ class BasicOption: '''Basic options which is included in config, program and files.''' _remove_old_files = None _enable_pcre2 = None + _encrypt_files = None _compress_config = None @property @@ -49,6 +50,20 @@ class BasicOption: return cfg._enable_pcre2 return False + @cached_property + def encrypt_files(self) -> bool: + if self._encrypt_files is not None: + return self._encrypt_files + prog = getattr(self, "_prog", None) + if prog is not None: + if prog._encrypt_files is not None: + return prog._encrypt_files + cfg = getattr(self, "_cfg", None) + if cfg is not None: + if cfg._encrypt_files is not None: + return cfg._encrypt_files + return False + @cached_property def remove_old_files(self) -> bool: if self._remove_old_files is not None: @@ -67,6 +82,7 @@ class BasicOption: self.parse_compress_config(data) self.parse_remove_old_files(data) self.parse_enable_pcre2(data) + self.parse_encrypt_files(data) def parse_compress_config(self, data=None): if data is None: @@ -89,6 +105,17 @@ class BasicOption: raise TypeError('enable_pcre2 option must be a boolean.') del v + def parse_encrypt_files(self, data=None): + if data is None: + data = getattr(self, 'data') + if 'encrypt_files' in data: + v = data['encrypt_files'] + if isinstance(v, bool): + self._encrypt_files = v + else: + raise TypeError('encrypt_files option must be a boolean.') + del v + def parse_remove_old_files(self, data=None): if data is None: data = getattr(self, 'data') @@ -403,7 +430,12 @@ class Program(BasicOption, NFBasicOption): if i['name'] != '': n = i['name'] if not relpath(name, n).startswith('..'): - return ConfigPath(i, self._cfg, self) + r = ConfigPath(i, self._cfg, self) + if r.is_exclude(n, name): + continue + if not r.is_include(n, name): + continue + return r elif t == 'leveldb': if relpath(i['name'], name) == '.': return ConfigOLeveldb(i, self._cfg, self) diff --git a/game_backuper/db.py b/game_backuper/db.py index 068f616..00fe873 100644 --- a/game_backuper/db.py +++ b/game_backuper/db.py @@ -8,6 +8,7 @@ from threading import Lock from typing import List, Union from game_backuper.cml import Opts from game_backuper.config import Config +from game_backuper.enc import EncryptStats from game_backuper.file import File, hydrate_file_if_needed from game_backuper.filetype import FileType @@ -33,10 +34,19 @@ id INT, type INT, PRIMARY KEY(id) );''' +ENCRYPTED_FILES_TABLE = '''CREATE TABLE encrypted_files ( +id INTEGER, +key TEXT, +iv TEXT, +crc32 TEXT, +compressed INT, +compressed_size INT, +PRIMARY KEY(id) +);''' class Db: - VERSION = [1, 0, 0, 1] + VERSION = [1, 0, 0, 2] def __check_database(self) -> bool: self.__updateExistsTable() @@ -46,6 +56,8 @@ class Db: if v < self.VERSION: if v < [1, 0, 0, 1]: self.db.execute(FILETYPE_TABLE) + if v < [1, 0, 0, 2]: + self.db.execute(ENCRYPTED_FILES_TABLE) self.__write_version() if v > self.VERSION: raise ValueError( @@ -60,6 +72,8 @@ class Db: self.db.execute(FILES_TABLE) if 'filetype' not in self._exist_table: self.db.execute(FILETYPE_TABLE) + if 'encrypted_files' not in self._exist_table: + self.db.execute(ENCRYPTED_FILES_TABLE) self.db.commit() def __init__(self, config: Config, opts: Opts): @@ -162,6 +176,12 @@ class Db: for i in cur: self.db.execute('INSERT INTO filetype VALUES (?, ?);', (i[0], f.type)) + if f.encrypted: + cur = self.db.execute( + 'SELECT * FROM files WHERE program=? AND file=?;', + (f.program, f.file)) + for i in cur: + self.db.execute('INSERT INTO encrypted_files VALUES (?, ?, ?, ?, ?, ?);', (i[0], f.key, f.iv, f.crc32, f.x_compress_type, f.compressed_size)) # noqa: E501 self.db.commit() @property @@ -178,7 +198,7 @@ class Db: def get_file(self, prog: str, file: str) -> File: with self._lock: cur = self.db.execute( - 'SELECT files.*, filetype.type FROM files LEFT JOIN filetype ON files.id=filetype.id WHERE program=? AND file=?;', # noqa: E501 + 'SELECT files.*, filetype.type, encrypted_files.key, encrypted_files.iv, encrypted_files.crc32, encrypted_files.compressed, encrypted_files.compressed_size FROM files LEFT JOIN filetype ON files.id=filetype.id LEFT JOIN encrypted_files ON files.id=encrypted_files.id WHERE program=? AND file=?;', # noqa: E501 (prog, file)) for i in cur: return File(*i) @@ -207,6 +227,10 @@ class Db: self.db.execute('DELETE FROM files WHERE id=?;', (iid,)) if ft is not None: self.db.execute('DELETE FROM filetype WHERE id=?;', (iid,)) + cur = self.db.execute('SELECT * FROM encrypted_files WHERE id=?;', (iid,)) # noqa: E501 + for i in cur: + self.db.execute('DELETE FROM encrypted_files WHERE id=?;', (iid,)) # noqa: E501 + break self.db.commit() def set_file(self, id: int, size: int, hash: str): @@ -214,3 +238,20 @@ class Db: self.db.execute('UPDATE files SET size=?, hash=? WHERE id=?;', (size, hash, id)) self.db.commit() + + def set_file_encrypt_information(self, id: int, stats: EncryptStats): + if stats is not None and not isinstance(stats, EncryptStats): + raise TypeError(f"Expected EncryptStats, got {type(stats)}") + with self._lock: + if stats is None: + self.db.execute('DELETE FROM encrypted_files WHERE id=?;', (id,)) # noqa: E501 + else: + cur = self.db.execute('SELECT * FROM encrypted_files WHERE id=?;', (id,)) # noqa: E501 + have_data = False + for _ in cur: + have_data = True + if have_data: + self.db.execute('UPDATE encrypted_files SET key=?, iv=?, crc32=?, compressed=?, compressed_size=? WHERE id=?;', (stats.key, stats.iv, stats.crc32, stats.compress_type.value if stats.compressed else None, stats.compressed_size if stats.compressed else None, id)) # noqa: E501 + else: + self.db.execute('INSERT INTO encrypted_files VALUES (?, ?, ?, ?, ?, ?);', (id, stats.key, stats.iv, stats.crc32, stats.compress_type.value if stats.compressed else None, stats.compressed_size if stats.compressed else None)) # noqa: E501 + self.db.commit() diff --git a/game_backuper/enc.py b/game_backuper/enc.py index 13ddc38..95954c2 100644 --- a/game_backuper/enc.py +++ b/game_backuper/enc.py @@ -1,15 +1,33 @@ -from base64 import b85decode +from base64 import b85decode, b85encode +from collections import namedtuple from io import RawIOBase -from os import PathLike, urandom +from os import PathLike, remove, urandom +from os.path import exists, getsize from typing import Union from zlib import crc32 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from game_backuper.compress import CompressConfig +from game_backuper.compress import ( + CompressConfig, + CompressMethod, + compress_info, +) +from game_backuper.file import File, hydrate_file_if_needed, mkdir_for_file _MODE_CLOSED = 0 _MODE_READ = 1 _MODE_WRITE = 3 +_EncrpytStats = namedtuple('EncryptStats', ['key', 'iv', 'crc32', 'x_compress_type', 'compressed_size']) # noqa: E501 + + +class EncryptStats(_EncrpytStats): + @property + def compressed(self): + return self.x_compress_type is not None + + @property + def compress_type(self): + return CompressMethod(self.x_compress_type) class DecryptException(Exception): @@ -307,3 +325,46 @@ class EncFile(RawIOBase): @property def iv(self): return self._iv + + +def encrypt_file(src: str, dest: str, f: File, name: str, prog: str, c: CompressConfig = None): # noqa: E501 + if exists(dest): + remove(dest) + mkdir_for_file(dest) + cs = 4096 if c is None else c.chunk_size + with open(src, 'rb') as s: + with EncFile(dest, 'wb', f.hash, compress=c) as t: + a = s.read(cs) + while a != b'': + t.write(a) + a = s.read(cs) + del a + stats = EncryptStats(b85encode(t.key).decode(), b85encode(t.iv).decode(), t.crc32, c._method.value if c else None, t.tell() if c else None) # noqa: E501 + i = compress_info(f.size, getsize(dest)) + if c is None: + print(f'{prog}: Encrypted {src}({name}) -> {dest} ({i})') + else: + print(f'{prog}: Compressed and encrypted {src}({name}) -> {dest} ({i})') # noqa: E501 + return stats + + +def decrypt_file(src: str, dest: str, f: File, name: str, prog: str, c: CompressConfig = None): # noqa: E501 + if not f.encrypted: + raise ValueError('File is not encrypted.') + hydrate_file_if_needed(src) + if exists(dest): + remove(dest) + mkdir_for_file(dest) + cs = 4096 if c is None else c.chunk_size + with EncFile(src, 'rb', f.hash, f.key, f.iv, f.encrypt_file_size, f.crc32, c) as s: # noqa: E501 + with open(dest, 'wb') as t: + a = s.read(cs) + while a != b'': + t.write(a) + a = s.read(cs) + del a + i = compress_info(f.size, getsize(src)) + if c is None: + print(f'{prog}: Decrypted {src}({name}) -> {dest} ({i})') + else: + print(f'{prog}: Decrypted and decompressed {src}({name}) -> {dest} ({i})') # noqa: E501 diff --git a/game_backuper/file.py b/game_backuper/file.py index e6edde3..7934a7c 100644 --- a/game_backuper/file.py +++ b/game_backuper/file.py @@ -15,11 +15,42 @@ else: have_cfapi = False -_File = namedtuple('File', ['id', 'file', 'size', 'program', 'hash', 'type']) +_File = namedtuple('File', ['id', 'file', 'size', 'program', 'hash', 'type', 'key', 'iv', 'crc32', 'x_compress_type', 'compressed_size']) # noqa: E501 class File(_File): - pass + @property + def encrypted(self): + return bool(self.key and self.iv and self.crc32) + + @property + def compressed(self): + return self.x_compress_type is not None + + @property + def compressed_type(self): + from game_backuper.compress import CompressMethod + return CompressMethod(self.x_compress_type) if self.compressed else None # noqa: E501 + + @property + def encrypt_file_size(self): + return self.compressed_size if self.compressed_size else self.size + + @classmethod + def from_encrypt_stats(cls, stats, file): + from game_backuper.enc import EncryptStats + if not isinstance(stats, EncryptStats): + raise TypeError(f'Expected EncryptStats, got {type(stats)}') + if not isinstance(file, (File, _File)): + raise TypeError(f'Expected File, got {type(file)}') + return cls(file.id, file.file, file.size, file.program, file.hash, file.type, stats.key, stats.iv, stats.crc32, stats.x_compress_type, stats.compressed_size) # noqa: E501 + + @classmethod + def from_leveldb_stats(cls, stats): + from game_backuper.leveldb import LeveldbStats + if not isinstance(stats, LeveldbStats): + raise TypeError(f'Expected LeveldbStats, got {type(stats)}') + return cls(None, None, stats.size, None, stats.hash, None, None, None, None, None, None) # noqa: E501 def mkdir_for_file(p: str): @@ -78,7 +109,7 @@ def new_file(loc: str, name: str, prog: str, type: FileType = None) -> File: fs = stat(loc).st_size with open(loc, 'rb') as f: hs = sha512(f) - return File(None, name, fs, prog, hs, type) + return File(None, name, fs, prog, hs, type, None, None, None, None, None) # noqa: E501 def remove_dirs(loc: str): @@ -112,3 +143,10 @@ def hydrate_file_if_needed(fn: str): return if exists(fn): hydrate_file(fn) + + +def remove_unencryped_files(loc: str, prog: str, name: str): + if exists(loc): + remove(loc) + print(f'{prog}: Removed {loc}({name})') + remove_compress_files(loc, prog, name) diff --git a/game_backuper/restorer.py b/game_backuper/restorer.py index 34a116c..f7dd36f 100644 --- a/game_backuper/restorer.py +++ b/game_backuper/restorer.py @@ -12,7 +12,8 @@ from game_backuper.file import ( ) from os import remove, close from game_backuper.filetype import FileType -from game_backuper.compress import decompress +from game_backuper.compress import CompressConfig, decompress +from game_backuper.enc import decrypt_file from tempfile import mkstemp @@ -36,7 +37,10 @@ class RestoreTask(Thread): if f.type is not None: raise ValueError('Type dismatched.') nam = r.real_name - src = join(self.cfg.dest, prog, fn) + if f.encrypted: + src = join(self.cfg.dest, '.encrypt', prog, fn) + else: + src = join(self.cfg.dest, prog, fn) c = r.compress_config tmp = relpath(fn, nam) if isabs(r.path): @@ -47,19 +51,24 @@ class RestoreTask(Thread): dest = join(dest, tmp) if dest in pl: pl.remove(dest) - if (c is None and not exists(src)) or (c is not None and not exists(src + c.ext)): # noqa: E501 + if not f.encrypted and ((c is None and not exists(src)) or (c is not None and not exists(src + c.ext))): # noqa: E501 + print(f'{prog}: Warn: Can not find backup files: "{src}"({fn})') # noqa: E501 + continue + elif f.encrypted and not exists(src): print(f'{prog}: Warn: Can not find backup files: "{src}"({fn})') # noqa: E501 continue if exists(dest): - tf = new_file(dest, nam, prog) + tf = new_file(dest, fn, prog) if tf.size == f.size and tf.hash == f.hash: print(f'{prog}: Skip {fn}') continue - if c is None: + if f.encrypted: + decrypt_file(src, dest, f, fn, prog, CompressConfig(f.compressed_type.to_str()) if f.compressed else None) # noqa: E501 + elif c is None: hydrate_file_if_needed(src) - copy_file(src, dest, nam, prog) + copy_file(src, dest, fn, prog) else: - decompress(src, dest, c, nam, prog) + decompress(src, dest, c, fn, prog) elif isinstance(r, ConfigOLeveldb): from game_backuper.leveldb import have_leveldb if not have_leveldb: @@ -67,7 +76,10 @@ class RestoreTask(Thread): if f.type != FileType.LEVELDB: raise ValueError('Type dismatched.') nam = r.real_name - src = join(self.cfg.dest, prog, fn + '.db') + if f.encrypted: + src = join(self.cfg.dest, '.encrypt', prog, fn + '.db') + else: + src = join(self.cfg.dest, prog, fn + '.db') c = r.compress_config if isabs(r.path): dest = r.path @@ -75,7 +87,10 @@ class RestoreTask(Thread): dest = join(b, r.path) if dest in pl: pl.remove(dest) - if (c is None and not exists(src)) or (c is not None and not exists(src + c.ext)): # noqa: E501 + if not f.encrypted and ((c is None and not exists(src)) or (c is not None and not exists(src + c.ext))): # noqa: E501 + print(f'{prog}: Warn: Can not find backup files: "{src}"({fn})') # noqa: E501 + continue + elif f.encrypted and not exists(src): print(f'{prog}: Warn: Can not find backup files: "{src}"({fn})') # noqa: E501 continue from game_backuper.leveldb import ( @@ -90,17 +105,26 @@ class RestoreTask(Thread): print(f'{prog}: Skip {fn}') continue mkdir_for_file(dest) - if c is None: + if f.encrypted: + tmp = mkstemp() + close(tmp[0]) + tmp = tmp[1] + decrypt_file(src, tmp, f, fn, prog, CompressConfig(f.compressed_type.to_str()) if f.compressed else None) # noqa: E501 + sqlite_to_leveldb(tmp, dest, r.domains) + print(f'{prog}: Convert leveldb done. {tmp}({fn}) -> {dest}') # noqa: E501 + remove(tmp) + print(f'{prog}: Removed temp file {tmp}') + elif c is None: hydrate_file_if_needed(src) sqlite_to_leveldb(src, dest, r.domains) - print(f'{prog}: Covert leveldb done. {src}({fn}) -> {dest}') # noqa: E501 + print(f'{prog}: Convert leveldb done. {src}({fn}) -> {dest}') # noqa: E501 else: tmp = mkstemp() close(tmp[0]) tmp = tmp[1] decompress(src, tmp, c, fn, prog) sqlite_to_leveldb(tmp, dest, r.domains) - print(f'{prog}: Covert leveldb done. {tmp}({fn}) -> {dest}') # noqa: E501 + print(f'{prog}: Convert leveldb done. {tmp}({fn}) -> {dest}') # noqa: E501 remove(tmp) print(f'{prog}: Removed tempfile {tmp}') for i in pl: diff --git a/requirements.txt b/requirements.txt index d9fd094..09d45e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +cryptography pyyaml plyvel[leveldb] lzip[lzip]