15 Commits

14 changed files with 371 additions and 68 deletions

24
build_exe.py Normal file
View File

@@ -0,0 +1,24 @@
from version import version, dversion
from py2exe import freeze
freeze(
console=[{
'script': "game_backuper/__main__.py",
"dest_base": 'game-backuper',
'version_info': {
'version': version,
'product_name': 'game-backuper',
'product_version': dversion,
'company_name': 'lifegpc',
'description': 'A game backuper',
'copyright': 'Copyright (C) 2021-2024 lifegpc'
},
}],
options={
"optimize": 2,
"compressed": 1,
"excludes": ["doctest", "pydoc", "unittest"],
"includes": ["cryptography.utils", "_cffi_backend", "sqlite3.dump"]
},
zipfile=None,
)

View File

@@ -8,7 +8,10 @@ compress_method: "bzip2" # Optional. Default value: null. Supported value: "bzi
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.
db_password: "Password" # Specify the password of the encryped database.
db_path: /path/to/db/path # Optional. Default value: $dest/data.db. The path to the database.
encrypt_files: false # Optional. Default value: false. Encrypt backup files. The key information will stored in database.
protect_filename: false # Optional. Default value: false. Use id in database as file name. Only works when encrypt_files is true.
unpin_file: false # Optional. Default value: false. Notifiy sync provider to dehydrate file data.
programs:
- name: Your program name # This name is used to identify different application.
base: /path/to/save/path # Must be absoulte path.
@@ -18,6 +21,8 @@ programs:
compress_method: null # Optional.
compress_level: null # Optional.
encrypt_files: false # Optional
protect_filename: false # Optional
unpin_file: 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
@@ -29,6 +34,8 @@ programs:
compress_method: null # Optional.
compress_level: null # Optional.
encrypt_files: false # Optional.
protect_filename: false # Optional
unpin_file: false # Optional.
excludes: # Optional. Exculde some files. Only effected when path is a folder.
- data.db # Releative path
- /path/to/data.db # Absolute path
@@ -51,5 +58,7 @@ programs:
compress_method: null # Optional.
compress_level: null # Optional.
encrypt_files: false # Optional.
protect_filename: false # Optional
unpin_file: false # Optional.
domains: # optional. Just backup minor domains in localstorage database. Only chromium is tested.
- some domain

View File

@@ -1,9 +1,23 @@
__version__ = "1.0.0"
import sys
from platform import system
if system() == 'Windows' and sys.version_info.minor > 7:
from os import add_dll_directory, environ, getcwd
from os.path import dirname, isdir
add_dll_directory(dirname(sys.executable))
add_dll_directory(getcwd())
for i in environ['PATH'].split(";"):
if isdir(i):
add_dll_directory(i)
for i in sys.path:
if isdir(i):
add_dll_directory(i)
from game_backuper.main import main
def start():
import sys
try:
sys.exit(main())
except Exception:

View File

@@ -8,13 +8,15 @@ from game_backuper.config import (
from game_backuper.cml import Opts, OptAction
from threading import Thread
from os.path import exists, join, isdir
from os import mkdir, remove, close
from os import remove, close
from shutil import move
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, remove_unencryped_files
from game_backuper.compress import compress
from game_backuper.enc import encrypt_file
from game_backuper.file import unpin_file_if_needed
from tempfile import mkstemp
@@ -30,8 +32,7 @@ class BackupTask(Thread):
prog = self.prog.name
bp = join(self.cfg.dest, prog)
ebp = join(self.cfg.dest, '.encrypt', prog)
if not exists(bp):
mkdir(bp)
ebpi = join(self.cfg.dest, '.encrypt', '.id')
fl = self.db.get_file_list(prog)
for f in self.prog.files:
if isinstance(f, ConfigNormalFile):
@@ -45,26 +46,48 @@ class BackupTask(Thread):
continue
de = join(ebp if f.encrypt_files else bp, f[0])
if ori is not None:
de2 = join(ebpi if f.encrypt_files else bp, str(ori.id)) # noqa: E501
if f.protect_filename:
if not exists(de2) and exists(de):
mkdir_for_file(de2)
move(de, de2)
print(f'{prog}: Renamed {de} -> {de2}.')
de = de2
else:
if not exists(de) and exists(de2):
mkdir_for_file(de)
move(de2, de)
print(f'{prog}: Renamed {de2} -> {de}.')
if ori.size == nf.size and ori.hash == nf.hash:
if c is None:
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
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
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
if f.unpin_file:
unpin_file_if_needed(de)
continue
else:
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
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de + c.ext)
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), prog, f.name) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
continue
stats = None
if f.encrypt_files:
@@ -74,12 +97,20 @@ class BackupTask(Thread):
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
self.remove_encrypted_file(join(ebpi, str(ori.id)), 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.remove_encrypted_file(join(ebpi, str(ori.id)), 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)
if f.unpin_file:
unpin_file_if_needed(de)
else:
if f.protect_filename:
self.db.add_file(nf, False)
tmpori = self.db.get_file(prog, f[0])
de = join(ebpi if f.encrypt_files else bp, str(tmpori.id)) # noqa: E501
if f.encrypt_files:
s = encrypt_file(f[1], de, nf, f.name, prog, c)
nf = File.from_encrypt_stats(s, nf)
@@ -91,7 +122,12 @@ class BackupTask(Thread):
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)
if f.protect_filename:
self.db.set_file_encrypt_information(tmpori.id, s)
else:
self.db.add_file(nf)
if f.unpin_file:
unpin_file_if_needed(de)
elif isinstance(f, ConfigLeveldb):
from game_backuper.leveldb import have_leveldb
if not have_leveldb:
@@ -111,12 +147,25 @@ class BackupTask(Thread):
c = f.compress_config
de = join(ebp if f.encrypt_files else bp, f.name + ".db")
if ori is not None:
de2 = join(ebpi if f.encrypt_files else bp, str(ori.id)) # noqa: E501
if f.protect_filename:
if not exists(de2) and exists(de):
mkdir_for_file(de2)
move(de, de2)
print(f'{prog}: Renamed {de} -> {de2}.')
de = de2
else:
if not exists(de) and exists(de2):
mkdir_for_file(de)
move(de2, de)
print(f'{prog}: Renamed {de2} -> {de}.')
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)
self.remove_encrypted_file(join(ebp, ori.file), prog, f.name, ori) # noqa: E501
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
self.db.remove_file(ori)
ori = None
if ori is not None:
@@ -126,28 +175,46 @@ class BackupTask(Thread):
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
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
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
if f.unpin_file:
unpin_file_if_needed(de)
continue
else:
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
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de + c.ext)
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
if f.unpin_file:
unpin_file_if_needed(de)
continue
if f.protect_filename:
ori = self.db.get_file(prog, f[0])
if ori is None:
nf = File(None, f.name, 0, prog, None, FileType.LEVELDB, None, None, None, None, None) # noqa: E501
self.db.add_file(nf, False)
ori = self.db.get_file(prog, f[0])
de = join(ebpi if f.encrypt_files else bp, str(ori.id)) # noqa: E501
mkdir_for_file(de)
st = None
if c is None:
if c is None and not f.encrypt_files:
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
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
else:
tmp = mkstemp()
close(tmp[0])
@@ -160,6 +227,7 @@ class BackupTask(Thread):
else:
compress(tmp, de, c, f.name, prog)
self.remove_encrypted_file(join(ebp, f[0] + '.db'), prog, f.name, ori) # noqa: E501
self.remove_encrypted_file(join(ebpi, str(ori.id)), prog, f.name, ori) # noqa: E501
remove(tmp)
print(f'{prog}: Removed tempfile {tmp}')
if ori is None:
@@ -171,6 +239,8 @@ class BackupTask(Thread):
else:
self.db.set_file(ori.id, stats.size, stats.hash)
self.db.set_file_encrypt_information(ori.id, st)
if f.unpin_file:
unpin_file_if_needed(de)
for fn in fl:
f = self.db.get_file(prog, fn)
if f.type is None:
@@ -180,6 +250,7 @@ class BackupTask(Thread):
print(f'{prog}: Remove {de}({fn})')
remove_compress_files(de, prog, fn)
self.remove_encrypted_file(join(ebp, fn), prog, fn, f)
self.remove_encrypted_file(join(ebpi, str(f.id)), prog, fn, f)
self.db.remove_file(f)
if f.type == FileType.LEVELDB:
de = join(bp, fn + '.db')
@@ -188,6 +259,7 @@ class BackupTask(Thread):
print(f'{prog}: Remove {de}({fn})')
remove_compress_files(de, prog, fn + '.db')
self.remove_encrypted_file(join(ebp, fn + '.db'), prog, fn, f)
self.remove_encrypted_file(join(ebpi, str(f.id)), prog, fn, ori) # noqa: E501
self.db.remove_file(f)
def remove_encrypted_file(self, loc: str, prog: str, name: str, f: File):

View File

@@ -1,15 +1,69 @@
from ctypes import HRESULT, byref, c_uint, windll
from ctypes.wintypes import (
DWORD, HANDLE, LARGE_INTEGER, LPCWSTR, LPVOID, PHANDLE
)
from ctypes import HRESULT, POINTER, Structure, Union, byref, c_uint, sizeof, windll # noqa: E501
from ctypes.wintypes import BYTE, DWORD, HANDLE, LARGE_INTEGER, LPCWSTR, LPVOID, PDWORD, PHANDLE, ULONG, WPARAM # noqa: E501
ULONG_PTR = WPARAM
PVOID = LPVOID
dll = windll.CldApi
CF_HYDRATE_FLAG_NONE = 0
CF_IN_SYNC_STATE_NOT_IN_SYNC = 0
CF_IN_SYNC_STATE_IN_SYNC = 1
CF_OPEN_FILE_FLAG_NONE = 0
CF_OPEN_FILE_FLAG_EXCLUSIVE = 1
CF_OPEN_FILE_FLAG_WRITE_ACCESS = 2
CF_OPEN_FILE_FLAG_DELETE_ACCESS = 3
CF_OPEN_FILE_FLAG_FOREGROUND = 4
CF_PIN_STATE_UNSPECIFIED = 0
CF_PIN_STATE_PINNED = 1
CF_PIN_STATE_UNPINNED = 2
CF_PIN_STATE_EXCLUDED = 3
CF_PIN_STATE_INHERIT = 4
CF_PLACEHOLDER_INFO_BASIC = 0
CF_PLACEHOLDER_INFO_STANDARD = 1
CF_SET_PIN_FLAG_NONE = 0x00000000
CF_SET_PIN_FLAG_RECURSE = 0x00000001
CF_SET_PIN_FLAG_RECURSE_ONLY = 0x00000002
CF_SET_PIN_FLAG_RECURSE_STOP_ON_ERROR = 0x00000004
class CF_PLACEHOLDER_BASIC_INFO(Structure):
_fields_ = [("PinState", c_uint),
("InSyncState", c_uint),
("FileId", LARGE_INTEGER),
("SyncRootFileId", LARGE_INTEGER),
("FileIdentityLength", ULONG),
("FileIdentity", BYTE)]
class CF_PLACEHOLDER_STANDARD_INFO(Structure):
_fields_ = [("OnDiskDataSize", LARGE_INTEGER),
("ValidatedDataSize", LARGE_INTEGER),
("ModifiedDataSize", LARGE_INTEGER),
("PropertiesSize", LARGE_INTEGER),
("PinState", c_uint),
("InSyncState", c_uint),
("FileId", LARGE_INTEGER),
("SyncRootFileId", LARGE_INTEGER),
("FileIdentityLength", ULONG),
("FileIdentity", BYTE)]
class DUMMYSTRUCTNAME(Structure):
_fields_ = [("Offset", DWORD), ("OffsetHigh", DWORD)]
class DUMMYUNIONNAME(Union):
_fields_ = [("DUMMYSTRUCTNAME", DUMMYSTRUCTNAME), ("Pointer", PVOID)]
class OVERLAPPED(Structure):
_fields_ = [("Internal", ULONG_PTR),
("InternalHigh", ULONG_PTR),
("DUMMYUNIONNAME", DUMMYUNIONNAME),
("hEvent", HANDLE)]
LPOVERLAPPED = POINTER(OVERLAPPED)
CfOpenFileWithOplock = dll.CfOpenFileWithOplock
CfOpenFileWithOplock.argtypes = [LPCWSTR, c_uint, PHANDLE]
CfOpenFileWithOplock.restype = HRESULT
@@ -18,7 +72,14 @@ CfHydratePlaceholder.argtypes = [HANDLE, LARGE_INTEGER, LARGE_INTEGER, c_uint, L
CfHydratePlaceholder.restype = HRESULT
CfCloseHandle = dll.CfCloseHandle
CfCloseHandle.argtypes = [HANDLE]
CfGetPlaceholderInfo = dll.CfGetPlaceholderInfo
CfGetPlaceholderInfo.argtypes = [HANDLE, c_uint, PVOID, DWORD, PDWORD] # noqa: E501
CfGetPlaceholderInfo.restype = HRESULT
CfSetPinState = dll.CfSetPinState
CfSetPinState.argtypes = [HANDLE, c_uint, c_uint, LPOVERLAPPED]
CfSetPinState.restype = HRESULT
ERROR_INVALID_FUNCTION = 1
ERROR_MORE_DATA = 234
GetLastError = windll.Kernel32.GetLastError
GetLastError.restype = DWORD
@@ -28,6 +89,41 @@ def hydrate_file(s: str):
try:
CfOpenFileWithOplock(s, CF_OPEN_FILE_FLAG_NONE, byref(h))
CfHydratePlaceholder(h, 0, -1, 0, LPVOID())
except OSError as e:
if GetLastError() != ERROR_INVALID_FUNCTION:
# File is not cloud file
if e.winerror != -2147024520:
CfCloseHandle(h)
raise e
CfCloseHandle(h)
def get_info(s: str, standard: bool = False):
h = HANDLE()
i = CF_PLACEHOLDER_STANDARD_INFO() if standard else CF_PLACEHOLDER_BASIC_INFO() # noqa: E501
t = CF_PLACEHOLDER_INFO_STANDARD if standard else CF_PLACEHOLDER_INFO_BASIC
le = DWORD()
si = DWORD(sizeof(i))
try:
CfOpenFileWithOplock(s, CF_OPEN_FILE_FLAG_FOREGROUND, byref(h))
CfGetPlaceholderInfo(h, t, byref(i), si, byref(le))
except OSError as e:
ee = GetLastError()
if ee == ERROR_MORE_DATA:
CfCloseHandle(h)
return i
if ee != ERROR_INVALID_FUNCTION:
CfCloseHandle(h)
raise e
CfCloseHandle(h)
return i
def unpin_file(s: str):
h = HANDLE()
try:
CfOpenFileWithOplock(s, CF_OPEN_FILE_FLAG_FOREGROUND, byref(h))
CfSetPinState(h, CF_PIN_STATE_UNPINNED, CF_SET_PIN_FLAG_NONE, None)
except OSError as e:
if GetLastError() != ERROR_INVALID_FUNCTION:
CfCloseHandle(h)

View File

@@ -15,6 +15,7 @@ class OptAction(IntEnum):
RESTORE = 1
LIST = 2
LIST_LEVELDB_KEY = 3
VERSION = 4
@staticmethod
def from_str(v: str) -> IntEnum:
@@ -28,6 +29,8 @@ class OptAction(IntEnum):
return OptAction.LIST
elif t == 'list_leveldb_key':
return OptAction.LIST_LEVELDB_KEY
elif t == "version":
return OptAction.VERSION
else:
raise TypeError('Must be str.')
@@ -59,7 +62,7 @@ class Opts:
re = OptAction.from_str(cm[0])
if re is not None:
self.action = re
if re == OptAction.LIST:
if re == OptAction.LIST or re == OptAction.VERSION:
return
elif re == OptAction.LIST_LEVELDB_KEY:
if len(cm) == 1:
@@ -79,6 +82,7 @@ class Opts:
print('''game-backuper [options] [backup|restore] [<game names> [...]]
game-backuper [options] list
game-backuper [options] list_leveldb_key [<db_path> [...]]
game-backuper version Print library support message.
Options:
-h, --help Print help message.
-c, --config <path> Set config file.

View File

@@ -71,6 +71,8 @@ if have_gzip:
self._f = None
def close(self):
if self._f is None:
return
self._f.close()
def compress(self, data: bytes) -> bytes:
@@ -90,6 +92,8 @@ if have_gzip:
self._f = None
def close(self):
if self._f is None:
return
self._f.close()
def read(self, len: int) -> bytes:

View File

@@ -21,9 +21,14 @@ class BasicOption:
_enable_pcre2 = None
_encrypt_files = None
_compress_config = None
_disable_compress = False
_protect_filename = None
_unpin_file = None
@property
def compress_config(self) -> CompressConfig:
if self._disable_compress:
return None
if self._compress_config is not None:
return self._compress_config
prog = getattr(self, "_prog", None)
@@ -64,6 +69,20 @@ class BasicOption:
return cfg._encrypt_files
return False
@cached_property
def protect_filename(self) -> bool:
if self._protect_filename is not None:
return self._protect_filename and self.encrypt_files
prog = getattr(self, "_prog", None)
if prog is not None:
if prog._protect_filename is not None:
return prog._protect_filename and self.encrypt_files
cfg = getattr(self, "_cfg", None)
if cfg is not None:
if cfg._protect_filename is not None:
return cfg._protect_filename and self.encrypt_files
return False
@cached_property
def remove_old_files(self) -> bool:
if self._remove_old_files is not None:
@@ -78,11 +97,27 @@ class BasicOption:
return cfg._remove_old_files
return True
@cached_property
def unpin_file(self) -> bool:
if self._unpin_file is not None:
return self._unpin_file
prog = getattr(self, "_prog", None)
if prog is not None:
if prog._unpin_file is not None:
return prog._unpin_file
cfg = getattr(self, "_cfg", None)
if cfg is not None:
if cfg._unpin_file is not None:
return cfg._unpin_file
return False
def parse_all(self, data=None):
self.parse_compress_config(data)
self.parse_remove_old_files(data)
self.parse_enable_pcre2(data)
self.parse_encrypt_files(data)
self.parse_protect_filename(data)
self.parse_unpin_file(data)
def parse_compress_config(self, data=None):
if data is None:
@@ -93,6 +128,8 @@ class BasicOption:
self._compress_config = CompressConfig(v, data.get("compress_level")) # noqa: E501
elif v is not None:
raise TypeError('compress_method option should be str or None.') # noqa: E501
else:
self._disable_compress = True
def parse_enable_pcre2(self, data=None):
if data is None:
@@ -116,6 +153,17 @@ class BasicOption:
raise TypeError('encrypt_files option must be a boolean.')
del v
def parse_protect_filename(self, data=None):
if data is None:
data = getattr(self, 'data')
if 'protect_filename' in data:
v = data['protect_filename']
if isinstance(v, bool):
self._protect_filename = v
else:
raise TypeError('protect_filename option must be a boolean.')
del v
def parse_remove_old_files(self, data=None):
if data is None:
data = getattr(self, 'data')
@@ -127,6 +175,17 @@ class BasicOption:
raise TypeError('remove_old_files option must be a boolean.')
del v
def parse_unpin_file(self, data=None):
if data is None:
data = getattr(self, 'data')
if 'unpin_file' in data:
v = data['unpin_file']
if isinstance(v, bool):
self._unpin_file = v
else:
raise TypeError('unpin_file option must be a boolean.')
del v
class NFBasicOption:
"""Basic options which is included in config, program."""
@@ -438,7 +497,14 @@ class Program(BasicOption, NFBasicOption):
continue
return r
elif t == 'leveldb':
if relpath(i['name'], name) == '.':
if isabs(i['path']):
n = i['name']
else:
n = i['path']
if 'name' in i and isinstance(i['name'], str):
if i['name'] != '':
n = i['name']
if relpath(n, name) == '.':
return ConfigOLeveldb(i, self._cfg, self)
@cached_property
@@ -453,6 +519,7 @@ class Config(BasicOption, NFBasicOption):
dest = ''
encrypt_db = False
db_password = None
db_path = None
progs = []
progs_name = []
@@ -476,6 +543,10 @@ class Config(BasicOption, NFBasicOption):
if not isinstance(t['db_password'], str):
raise ValueError('db_password should be a string.')
self.db_password = t['db_password']
if 'db_path' in t:
if not isinstance(t['db_path'], str):
raise ValueError('db_path should be a string.')
self.db_path = t['db_path']
if 'programs' not in t:
raise ValueError("No programs found.")
self.parse_all(t)

View File

@@ -53,6 +53,7 @@ def getpass(prompt, cfg: Config) -> str:
class Db:
VERSION = [1, 0, 0, 2]
fn = None
def __check_database(self) -> bool:
self.__updateExistsTable()
@@ -85,9 +86,10 @@ class Db:
def __init__(self, config: Config, opts: Opts):
self._cfg = config
self._opt = opts
fn = join(config.dest, "data.db")
hydrate_file_if_needed(fn)
self.db = connect(fn, check_same_thread=False)
self.fn = config.db_path if config.db_path else join(
config.dest, "data.db")
hydrate_file_if_needed(self.fn)
self.db = connect(self.fn, check_same_thread=False)
if config.encrypt_db:
passpharse = getpass('Please input the password of the database:', config) # noqa: E501
if not self.encrypted:
@@ -100,8 +102,8 @@ class Db:
db.execute(q)
self.db.close()
db.close()
move(tfn, fn)
self.db = connect(fn, check_same_thread=False)
move(tfn, self.fn)
self.db = connect(self.fn, check_same_thread=False)
elif opts.change_key:
self.__set_encrypt_key(passpharse)
passpharse = getpass('Please input new password of the database:', config) # noqa: E501
@@ -114,8 +116,8 @@ class Db:
db.execute(q)
self.db.close()
db.close()
move(tfn, fn)
self.db = connect(fn, check_same_thread=False)
move(tfn, self.fn)
self.db = connect(self.fn, check_same_thread=False)
self.__set_encrypt_key(passpharse)
else:
if self.encrypted:
@@ -129,8 +131,8 @@ class Db:
db.execute(q)
self.db.close()
db.close()
move(tfn, fn)
self.db = connect(fn, check_same_thread=False)
move(tfn, self.fn)
self.db = connect(self.fn, check_same_thread=False)
if opts.optimize_db:
self.db.execute('VACUUM;')
self.db.commit()
@@ -171,7 +173,7 @@ class Db:
tuple(self.VERSION))
self.db.commit()
def add_file(self, f: File):
def add_file(self, f: File, commited: bool = True):
with self._lock:
self.db.execute('INSERT INTO files (file, size, program, hash) VALUES (?, ?, ?, ?);', # noqa: E501
(f.file, f.size, f.program, f.hash))
@@ -188,12 +190,13 @@ class Db:
(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()
if commited:
self.db.commit()
@property
def encrypted(self):
try:
con = connect(join(self._cfg.dest, 'data.db'))
con = connect(self.fn)
con.execute('SELECT count(*) FROM sqlite_master;')
con.close()
return False

View File

@@ -7,7 +7,7 @@ from game_backuper.filetype import FileType
from platform import system
if system() == "Windows":
try:
from game_backuper.cfapi import hydrate_file
from game_backuper.cfapi import hydrate_file, unpin_file
have_cfapi = True
except Exception:
have_cfapi = False
@@ -150,3 +150,10 @@ def remove_unencryped_files(loc: str, prog: str, name: str):
remove(loc)
print(f'{prog}: Removed {loc}({name})')
remove_compress_files(loc, prog, name)
def unpin_file_if_needed(fn: str):
if not have_cfapi:
return
if exists(fn):
unpin_file(fn)

View File

@@ -1,5 +1,5 @@
from game_backuper.config import Config
from game_backuper.cml import Opts
from game_backuper.cml import Opts, OptAction
from game_backuper.db import Db
from game_backuper.backuper import Backuper
from os import makedirs
@@ -11,6 +11,28 @@ def main(cm=None):
import sys
cm = sys.argv[1:]
cml = Opts(cm)
if cml.action == OptAction.VERSION:
from game_backuper.compress import (
have_brotli,
have_bz2,
have_gzip,
have_lzip,
have_lzma,
have_snappy,
have_zstd,
)
from game_backuper.leveldb import have_leveldb
from game_backuper.regexp import have_pcre2
print("Brotli support:", have_brotli)
print("BZip2 support:", have_bz2)
print("GZip support:", have_gzip)
print("LZip support:", have_lzip)
print("LZMA support:", have_lzma)
print("Snappy support:", have_snappy)
print("ZSTD support:", have_zstd)
print("LevelDB support:", have_leveldb)
print("PCRE2 support:", have_pcre2)
return 0
cfg = Config(cml.config_file)
if not exists(cfg.dest):
makedirs(cfg.dest)

View File

@@ -48,4 +48,4 @@ def wildcards_to_regex(s: str, **k):
s = s.replace(i, f"\\{i}")
s = s.replace("*", ".*")
s = s.replace("?", ".")
return Regex(s, **k)
return Regex(f"^{s}$", **k)

View File

@@ -39,6 +39,8 @@ class RestoreTask(Thread):
nam = r.real_name
if f.encrypted:
src = join(self.cfg.dest, '.encrypt', prog, fn)
if not exists(src):
src = join(self.cfg.dest, '.encrypt', '.id', str(f.id)) # noqa: E501
else:
src = join(self.cfg.dest, prog, fn)
c = r.compress_config
@@ -62,6 +64,7 @@ class RestoreTask(Thread):
if tf.size == f.size and tf.hash == f.hash:
print(f'{prog}: Skip {fn}')
continue
mkdir_for_file(dest)
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:
@@ -78,6 +81,8 @@ class RestoreTask(Thread):
nam = r.real_name
if f.encrypted:
src = join(self.cfg.dest, '.encrypt', prog, fn + '.db')
if not exists(src):
src = join(self.cfg.dest, '.encrypt', '.id', str(f.id)) # noqa: E501
else:
src = join(self.cfg.dest, prog, fn + '.db')
c = r.compress_config

View File

@@ -1,7 +1,7 @@
# flake8: noqa
import sys
from version import version, dversion
from setuptools import Extension
from version import version
from setuptools import setup, Extension
try:
from Cython.Build import cythonize
except ImportError:
@@ -18,44 +18,6 @@ if '--without-zstd' in sys.argv:
else:
ext_modules.append(Extension("game_backuper._zstd", ["game_backuper/_zstd.pyx"], libraries=["zstd"]))
if "py2exe" in sys.argv:
from distutils.core import setup
import py2exe
params = {
"console": [{
'script': "game_backuper/__main__.py",
"dest_base": 'game-backuper',
'version': version,
'product_name': 'game-backuper',
'product_version': dversion,
'company_name': 'lifegpc',
'description': 'A game backuper',
}],
"options": {
"py2exe": {
"optimize": 2,
"compressed": 1,
"excludes": ["pydoc", "unittest"],
"includes": ["cryptography.utils", "_cffi_backend", "sqlite3.dump"]
}
},
"zipfile": None,
}
else:
from setuptools import setup
params = {
"install_requires": ["pyyaml"],
'entry_points': {
'console_scripts': ['game-backuper = game_backuper:start']
},
"extras_require": {
"leveldb": "plyvel",
"lzip": "lzip",
"snappy": "python-snappy",
"brotli": "brotli",
},
"python_requires": ">=3.6"
}
setup(
name="game-backuper",
version=version,
@@ -72,5 +34,15 @@ setup(
keywords="backup",
packages=["game_backuper"],
ext_modules=cythonize(ext_modules, compiler_directives={'language_level': "3"}),
**params
install_requires=["pyyaml"],
entry_points={
'console_scripts': ['game-backuper = game_backuper:start']
},
extras_require={
"leveldb": "plyvel",
"lzip": "lzip",
"snappy": "python-snappy",
"brotli": "brotli",
},
python_requires=">=3.6"
)