Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b8a4d9884 | |||
| 4e5342fc83 | |||
| 75e6d4bd39 | |||
| 4e816cbf61 | |||
| b5bc5cbf39 | |||
| aabe935933 | |||
| b1016b029e | |||
| a45215497f | |||
| 6290fc1dae | |||
| 4ff3b8a414 | |||
| d4dedcb316 | |||
| 7d82acb488 | |||
| 48c2295a49 | |||
| ea645a8ec9 | |||
| a779ca4b8d |
24
build_exe.py
Normal file
24
build_exe.py
Normal 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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
54
setup.py
54
setup.py
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user