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 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_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_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. 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: programs:
- name: Your program name # This name is used to identify different application. - name: Your program name # This name is used to identify different application.
base: /path/to/save/path # Must be absoulte path. base: /path/to/save/path # Must be absoulte path.
@@ -18,6 +21,8 @@ programs:
compress_method: null # Optional. compress_method: null # Optional.
compress_level: null # Optional. compress_level: null # Optional.
encrypt_files: false # Optional encrypt_files: false # Optional
protect_filename: false # Optional
unpin_file: false # Optional.
files: files:
- BGI.gdb # path to a file/folder. All subfolders will include if it is a folder. Must be relative path. - BGI.gdb # path to a file/folder. All subfolders will include if it is a folder. Must be relative path.
- type: path - type: path
@@ -29,6 +34,8 @@ programs:
compress_method: null # Optional. compress_method: null # Optional.
compress_level: null # Optional. compress_level: null # Optional.
encrypt_files: false # 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. excludes: # Optional. Exculde some files. Only effected when path is a folder.
- data.db # Releative path - data.db # Releative path
- /path/to/data.db # Absolute path - /path/to/data.db # Absolute path
@@ -51,5 +58,7 @@ programs:
compress_method: null # Optional. compress_method: null # Optional.
compress_level: null # Optional. compress_level: null # Optional.
encrypt_files: false # 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. domains: # optional. Just backup minor domains in localstorage database. Only chromium is tested.
- some domain - some domain

View File

@@ -1,9 +1,23 @@
__version__ = "1.0.0" __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 from game_backuper.main import main
def start(): def start():
import sys
try: try:
sys.exit(main()) sys.exit(main())
except Exception: except Exception:

View File

@@ -8,13 +8,15 @@ from game_backuper.config import (
from game_backuper.cml import Opts, OptAction from game_backuper.cml import Opts, OptAction
from threading import Thread from threading import Thread
from os.path import exists, join, isdir 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.file import new_file, copy_file, File, mkdir_for_file
from game_backuper.filetype import FileType from game_backuper.filetype import FileType
from game_backuper.restorer import RestoreTask from game_backuper.restorer import RestoreTask
from game_backuper.file import remove_compress_files, remove_unencryped_files from game_backuper.file import remove_compress_files, remove_unencryped_files
from game_backuper.compress import compress from game_backuper.compress import compress
from game_backuper.enc import encrypt_file from game_backuper.enc import encrypt_file
from game_backuper.file import unpin_file_if_needed
from tempfile import mkstemp from tempfile import mkstemp
@@ -30,8 +32,7 @@ class BackupTask(Thread):
prog = self.prog.name prog = self.prog.name
bp = join(self.cfg.dest, prog) bp = join(self.cfg.dest, prog)
ebp = join(self.cfg.dest, '.encrypt', prog) ebp = join(self.cfg.dest, '.encrypt', prog)
if not exists(bp): ebpi = join(self.cfg.dest, '.encrypt', '.id')
mkdir(bp)
fl = self.db.get_file_list(prog) fl = self.db.get_file_list(prog)
for f in self.prog.files: for f in self.prog.files:
if isinstance(f, ConfigNormalFile): if isinstance(f, ConfigNormalFile):
@@ -45,26 +46,48 @@ class BackupTask(Thread):
continue continue
de = join(ebp if f.encrypt_files else bp, f[0]) de = join(ebp if f.encrypt_files else bp, f[0])
if ori is not None: 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 ori.size == nf.size and ori.hash == nf.hash:
if c is None: if c is None:
if exists(de) and not f.encrypt_files: if exists(de) and not f.encrypt_files:
print(f'{prog}: Skip {f[0]}.') print(f'{prog}: Skip {f[0]}.')
remove_compress_files(de, prog, f.name) 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(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 continue
elif exists(de) and f.encrypt_files and not ori.compressed: # noqa: E501 elif exists(de) and f.encrypt_files and not ori.compressed: # noqa: E501
print(f'{prog}: Skip {f[0]}.') print(f'{prog}: Skip {f[0]}.')
remove_unencryped_files(join(bp, f[0]), prog, f.name) # noqa: E501 remove_unencryped_files(join(bp, f[0]), prog, f.name) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
continue continue
else: else:
if not f.encrypt_files and exists(de + c.ext): if not f.encrypt_files and exists(de + c.ext):
print(f'{prog}: Skip {f.name}.') print(f'{prog}: Skip {f.name}.')
remove_compress_files(de, prog, f.name, c.ext) # noqa: E501 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(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 continue
elif f.encrypt_files and ori.compressed_type == c.method: # noqa: E501 elif f.encrypt_files and ori.compressed_type == c.method: # noqa: E501
print(f'{prog}: Skip {f.name}.') print(f'{prog}: Skip {f.name}.')
remove_unencryped_files(join(bp, f.name), prog, f.name) # noqa: E501 remove_unencryped_files(join(bp, f.name), prog, f.name) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
continue continue
stats = None stats = None
if f.encrypt_files: if f.encrypt_files:
@@ -74,12 +97,20 @@ class BackupTask(Thread):
copy_file(f[1], de, f[0], prog) copy_file(f[1], de, f[0], prog)
remove_compress_files(de, prog, f.name) 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(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: else:
compress(f[1], de, c, f.name, prog) 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(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(ori.id, nf.size, nf.hash)
self.db.set_file_encrypt_information(ori.id, stats) self.db.set_file_encrypt_information(ori.id, stats)
if f.unpin_file:
unpin_file_if_needed(de)
else: 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: if f.encrypt_files:
s = encrypt_file(f[1], de, nf, f.name, prog, c) s = encrypt_file(f[1], de, nf, f.name, prog, c)
nf = File.from_encrypt_stats(s, nf) nf = File.from_encrypt_stats(s, nf)
@@ -91,7 +122,12 @@ class BackupTask(Thread):
else: else:
compress(f[1], de, c, f.name, prog) 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(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): elif isinstance(f, ConfigLeveldb):
from game_backuper.leveldb import have_leveldb from game_backuper.leveldb import have_leveldb
if not have_leveldb: if not have_leveldb:
@@ -111,12 +147,25 @@ class BackupTask(Thread):
c = f.compress_config c = f.compress_config
de = join(ebp if f.encrypt_files else bp, f.name + ".db") de = join(ebp if f.encrypt_files else bp, f.name + ".db")
if ori is not None: 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: if ori.type is None or ori.type != FileType.LEVELDB:
pp = join(bp, ori.file) pp = join(bp, ori.file)
if exists(pp): if exists(pp):
remove(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.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) self.db.remove_file(ori)
ori = None ori = None
if ori is not None: if ori is not None:
@@ -126,28 +175,46 @@ class BackupTask(Thread):
print(f'{prog}: Skip {f[0]}.') print(f'{prog}: Skip {f[0]}.')
remove_compress_files(de, prog, f.name) 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(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 continue
elif exists(de) and f.encrypt_files and not ori.compressed: # noqa: E501 elif exists(de) and f.encrypt_files and not ori.compressed: # noqa: E501
print(f'{prog}: Skip {f[0]}.') print(f'{prog}: Skip {f[0]}.')
remove_unencryped_files(join(bp, f[0] + '.db'), prog, f.name) # noqa: E501 remove_unencryped_files(join(bp, f[0] + '.db'), prog, f.name) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
continue continue
else: else:
if not f.encrypt_files and exists(de + c.ext): if not f.encrypt_files and exists(de + c.ext):
print(f'{prog}: Skip {f.name}.') print(f'{prog}: Skip {f.name}.')
remove_compress_files(de, prog, f.name, c.ext) 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(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 continue
elif f.encrypt_files and ori.compressed_type == c.method: # noqa: E501 elif f.encrypt_files and ori.compressed_type == c.method: # noqa: E501
print(f'{prog}: Skip {f.name}.') print(f'{prog}: Skip {f.name}.')
remove_unencryped_files(join(bp, f.name + '.db'), prog, f.name) # noqa: E501 remove_unencryped_files(join(bp, f.name + '.db'), prog, f.name) # noqa: E501
if f.unpin_file:
unpin_file_if_needed(de)
continue 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) mkdir_for_file(de)
st = None st = None
if c is None: if c is None and not f.encrypt_files:
leveldb_to_sqlite(f.full_path, de, ent) leveldb_to_sqlite(f.full_path, de, ent)
print(f'{prog}: Covert leveldb done. {f.full_path}({f.name}) -> {de}') # noqa: E501 print(f'{prog}: Covert leveldb done. {f.full_path}({f.name}) -> {de}') # noqa: E501
remove_compress_files(de, prog, f.name) 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(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: else:
tmp = mkstemp() tmp = mkstemp()
close(tmp[0]) close(tmp[0])
@@ -160,6 +227,7 @@ class BackupTask(Thread):
else: else:
compress(tmp, de, c, f.name, prog) 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(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) remove(tmp)
print(f'{prog}: Removed tempfile {tmp}') print(f'{prog}: Removed tempfile {tmp}')
if ori is None: if ori is None:
@@ -171,6 +239,8 @@ class BackupTask(Thread):
else: else:
self.db.set_file(ori.id, stats.size, stats.hash) self.db.set_file(ori.id, stats.size, stats.hash)
self.db.set_file_encrypt_information(ori.id, st) self.db.set_file_encrypt_information(ori.id, st)
if f.unpin_file:
unpin_file_if_needed(de)
for fn in fl: for fn in fl:
f = self.db.get_file(prog, fn) f = self.db.get_file(prog, fn)
if f.type is None: if f.type is None:
@@ -180,6 +250,7 @@ class BackupTask(Thread):
print(f'{prog}: Remove {de}({fn})') print(f'{prog}: Remove {de}({fn})')
remove_compress_files(de, prog, fn) remove_compress_files(de, prog, fn)
self.remove_encrypted_file(join(ebp, fn), prog, fn, f) 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) self.db.remove_file(f)
if f.type == FileType.LEVELDB: if f.type == FileType.LEVELDB:
de = join(bp, fn + '.db') de = join(bp, fn + '.db')
@@ -188,6 +259,7 @@ class BackupTask(Thread):
print(f'{prog}: Remove {de}({fn})') print(f'{prog}: Remove {de}({fn})')
remove_compress_files(de, prog, fn + '.db') remove_compress_files(de, prog, fn + '.db')
self.remove_encrypted_file(join(ebp, fn + '.db'), prog, fn, f) 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) self.db.remove_file(f)
def remove_encrypted_file(self, loc: str, prog: str, name: str, f: File): 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 import HRESULT, POINTER, Structure, Union, byref, c_uint, sizeof, windll # noqa: E501
from ctypes.wintypes import ( from ctypes.wintypes import BYTE, DWORD, HANDLE, LARGE_INTEGER, LPCWSTR, LPVOID, PDWORD, PHANDLE, ULONG, WPARAM # noqa: E501
DWORD, HANDLE, LARGE_INTEGER, LPCWSTR, LPVOID, PHANDLE
)
ULONG_PTR = WPARAM
PVOID = LPVOID
dll = windll.CldApi 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_NONE = 0
CF_OPEN_FILE_FLAG_EXCLUSIVE = 1 CF_OPEN_FILE_FLAG_EXCLUSIVE = 1
CF_OPEN_FILE_FLAG_WRITE_ACCESS = 2 CF_OPEN_FILE_FLAG_WRITE_ACCESS = 2
CF_OPEN_FILE_FLAG_DELETE_ACCESS = 3 CF_OPEN_FILE_FLAG_DELETE_ACCESS = 3
CF_OPEN_FILE_FLAG_FOREGROUND = 4 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 = dll.CfOpenFileWithOplock
CfOpenFileWithOplock.argtypes = [LPCWSTR, c_uint, PHANDLE] CfOpenFileWithOplock.argtypes = [LPCWSTR, c_uint, PHANDLE]
CfOpenFileWithOplock.restype = HRESULT CfOpenFileWithOplock.restype = HRESULT
@@ -18,7 +72,14 @@ CfHydratePlaceholder.argtypes = [HANDLE, LARGE_INTEGER, LARGE_INTEGER, c_uint, L
CfHydratePlaceholder.restype = HRESULT CfHydratePlaceholder.restype = HRESULT
CfCloseHandle = dll.CfCloseHandle CfCloseHandle = dll.CfCloseHandle
CfCloseHandle.argtypes = [HANDLE] 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_INVALID_FUNCTION = 1
ERROR_MORE_DATA = 234
GetLastError = windll.Kernel32.GetLastError GetLastError = windll.Kernel32.GetLastError
GetLastError.restype = DWORD GetLastError.restype = DWORD
@@ -28,6 +89,41 @@ def hydrate_file(s: str):
try: try:
CfOpenFileWithOplock(s, CF_OPEN_FILE_FLAG_NONE, byref(h)) CfOpenFileWithOplock(s, CF_OPEN_FILE_FLAG_NONE, byref(h))
CfHydratePlaceholder(h, 0, -1, 0, LPVOID()) 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: except OSError as e:
if GetLastError() != ERROR_INVALID_FUNCTION: if GetLastError() != ERROR_INVALID_FUNCTION:
CfCloseHandle(h) CfCloseHandle(h)

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ from game_backuper.filetype import FileType
from platform import system from platform import system
if system() == "Windows": if system() == "Windows":
try: try:
from game_backuper.cfapi import hydrate_file from game_backuper.cfapi import hydrate_file, unpin_file
have_cfapi = True have_cfapi = True
except Exception: except Exception:
have_cfapi = False have_cfapi = False
@@ -150,3 +150,10 @@ def remove_unencryped_files(loc: str, prog: str, name: str):
remove(loc) remove(loc)
print(f'{prog}: Removed {loc}({name})') print(f'{prog}: Removed {loc}({name})')
remove_compress_files(loc, prog, 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.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.db import Db
from game_backuper.backuper import Backuper from game_backuper.backuper import Backuper
from os import makedirs from os import makedirs
@@ -11,6 +11,28 @@ def main(cm=None):
import sys import sys
cm = sys.argv[1:] cm = sys.argv[1:]
cml = Opts(cm) 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) cfg = Config(cml.config_file)
if not exists(cfg.dest): if not exists(cfg.dest):
makedirs(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(i, f"\\{i}")
s = s.replace("*", ".*") s = s.replace("*", ".*")
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 nam = r.real_name
if f.encrypted: if f.encrypted:
src = join(self.cfg.dest, '.encrypt', prog, fn) 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: else:
src = join(self.cfg.dest, prog, fn) src = join(self.cfg.dest, prog, fn)
c = r.compress_config c = r.compress_config
@@ -62,6 +64,7 @@ class RestoreTask(Thread):
if tf.size == f.size and tf.hash == f.hash: if tf.size == f.size and tf.hash == f.hash:
print(f'{prog}: Skip {fn}') print(f'{prog}: Skip {fn}')
continue continue
mkdir_for_file(dest)
if f.encrypted: if f.encrypted:
decrypt_file(src, dest, f, fn, prog, CompressConfig(f.compressed_type.to_str()) if f.compressed else None) # noqa: E501 decrypt_file(src, dest, f, fn, prog, CompressConfig(f.compressed_type.to_str()) if f.compressed else None) # noqa: E501
elif c is None: elif c is None:
@@ -78,6 +81,8 @@ class RestoreTask(Thread):
nam = r.real_name nam = r.real_name
if f.encrypted: if f.encrypted:
src = join(self.cfg.dest, '.encrypt', prog, fn + '.db') 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: else:
src = join(self.cfg.dest, prog, fn + '.db') src = join(self.cfg.dest, prog, fn + '.db')
c = r.compress_config c = r.compress_config

View File

@@ -1,7 +1,7 @@
# flake8: noqa # flake8: noqa
import sys import sys
from version import version, dversion from version import version
from setuptools import Extension from setuptools import setup, Extension
try: try:
from Cython.Build import cythonize from Cython.Build import cythonize
except ImportError: except ImportError:
@@ -18,44 +18,6 @@ if '--without-zstd' in sys.argv:
else: else:
ext_modules.append(Extension("game_backuper._zstd", ["game_backuper/_zstd.pyx"], libraries=["zstd"])) 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( setup(
name="game-backuper", name="game-backuper",
version=version, version=version,
@@ -72,5 +34,15 @@ setup(
keywords="backup", keywords="backup",
packages=["game_backuper"], packages=["game_backuper"],
ext_modules=cythonize(ext_modules, compiler_directives={'language_level': "3"}), 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"
) )