add support to restore normal files

This commit is contained in:
2021-09-09 18:51:22 +08:00
parent 40b73ee929
commit 493af568b5
5 changed files with 206 additions and 3 deletions

View File

@@ -11,6 +11,7 @@ from os.path import exists, join, isdir
from os import mkdir, remove from os import mkdir, remove
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
class BackupTask(Thread): class BackupTask(Thread):
@@ -114,6 +115,10 @@ class Backuper:
t.start() t.start()
elif self.opts.action == OptAction.LIST: elif self.opts.action == OptAction.LIST:
print(prog.name) print(prog.name)
elif self.opts.action == OptAction.RESTORE:
t = RestoreTask(prog, self.db, self.conf)
self.tasks.append(t)
t.start()
def run(self): def run(self):
if self.opts.action == OptAction.LIST_LEVELDB_KEY: if self.opts.action == OptAction.LIST_LEVELDB_KEY:

View File

@@ -83,6 +83,79 @@ class NFBasicOption:
del v del v
# pylint: disable=unsupported-membership-test, unsubscriptable-object
class BasicConfig:
def __repr__(self) -> str:
data = getattr(self, "data", None)
return f"{self.__class__.__name__}<{data}>"
@cached_property
def name(self) -> str:
data = getattr(self, "data", None)
if isinstance(data, dict) and 'name' in data:
v = data['name']
if isinstance(v, str) and len(v) > 0:
return v
@cached_property
def path(self) -> str:
data = getattr(self, "data", None)
if isinstance(data, dict) and 'path' in data:
v = data['path']
if isinstance(v, str) and len(v) > 0:
return v
raise ValueError('Path not found.')
@cached_property
def type(self) -> str:
data = getattr(self, "data", None)
if isinstance(data, dict) and 'type' in data:
v = data['type']
if isinstance(v, str) and len(v) > 0:
return v
raise ValueError('Type not found.')
# pylint: enable=unsupported-membership-test, unsubscriptable-object
class ConfigPath(BasicOption, NFBasicOption, BasicConfig):
def __init__(self, data, cfg, prog):
NFBasicOption.__init__(self, cfg, prog)
if isinstance(data, str):
self.data = {"path": data, "type": "path"}
elif isinstance(data, dict):
self.data = data
else:
raise TypeError('Must be str or dict.')
self.parse_all()
self.parse_all_nf()
class ConfigOLeveldb(BasicOption, NFBasicOption, BasicConfig):
def __init__(self, data, cfg, prog):
NFBasicOption.__init__(self, cfg, prog)
if isinstance(data, dict):
self.data = data
else:
raise TypeError('Must be dict.')
self.parse_all()
self.parse_all_nf()
@cached_property
def ignore_hidden_files(self):
True
@cached_property
def domains(self) -> List[str]:
if 'domains' in self.data:
if isinstance(self.data['domains'], list):
dms = []
for i in self.data['domains']:
if isinstance(i, str) and len(i) > 0:
dms.append(i)
if len(dms) > 0:
return dms
def namedtuple_bo(typename, field_names): def namedtuple_bo(typename, field_names):
a = namedtuple(typename, field_names) a = namedtuple(typename, field_names)
return type(typename, (a, BasicOption), {}) return type(typename, (a, BasicOption), {})
@@ -91,6 +164,7 @@ def namedtuple_bo(typename, field_names):
ConfigNormalFile = namedtuple_bo('ConfigNormalFile', ['name', 'full_path']) ConfigNormalFile = namedtuple_bo('ConfigNormalFile', ['name', 'full_path'])
ConfigLeveldb = namedtuple_bo('ConfigLeveldb', ['name', 'full_path', 'domains']) # noqa: E501 ConfigLeveldb = namedtuple_bo('ConfigLeveldb', ['name', 'full_path', 'domains']) # noqa: E501
ConfigResult = Union[ConfigNormalFile, ConfigLeveldb] ConfigResult = Union[ConfigNormalFile, ConfigLeveldb]
ConfigOriginResult = Union[ConfigPath, ConfigOLeveldb]
class Program(BasicOption, NFBasicOption): class Program(BasicOption, NFBasicOption):
@@ -101,6 +175,20 @@ class Program(BasicOption, NFBasicOption):
self.parse_all() self.parse_all()
self.parse_all_nf() self.parse_all_nf()
@cached_property
def all_configs(self) -> List[ConfigOriginResult]:
r = []
for i in self.data['files']:
if isinstance(i, str):
r.append(ConfigPath(i, self._cfg, self))
elif isinstance(i, dict):
t = i['type']
if t == 'path':
r.append(ConfigPath(i, self._cfg, self))
elif t == 'leveldb':
r.append(ConfigOLeveldb(i, self._cfg, self))
return r
@cached_property @cached_property
def base(self) -> str: def base(self) -> str:
if 'base' in self.data: if 'base' in self.data:
@@ -203,6 +291,24 @@ class Program(BasicOption, NFBasicOption):
i._prog = self i._prog = self
return r return r
def get_config(self, name: str) -> ConfigOriginResult:
for i in self.data['files']:
if isinstance(i, str):
if not relpath(name, i).startswith('..'):
return ConfigPath(i, self._cfg, self)
elif isinstance(i, dict):
t = i['type']
if t == 'path':
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 not relpath(name, n).startswith('..'):
return ConfigPath(i, self._cfg, self)
@cached_property @cached_property
def name(self) -> str: def name(self) -> str:
if 'name' in self.data: if 'name' in self.data:

View File

@@ -1,9 +1,10 @@
from collections import namedtuple from collections import namedtuple
from os.path import exists, dirname, abspath, isfile, isdir, join from os.path import exists, dirname, abspath, isfile, isdir, join, isabs
from os import stat, makedirs, listdir from os import stat, makedirs, listdir
from game_backuper.hashl import sha512 from game_backuper.hashl import sha512
from shutil import copy2 from shutil import copy2
from game_backuper.filetype import FileType from game_backuper.filetype import FileType
from os import remove
File = namedtuple('File', ['id', 'file', 'size', 'program', 'hash', 'type']) File = namedtuple('File', ['id', 'file', 'size', 'program', 'hash', 'type'])
@@ -36,9 +37,40 @@ def listdirs(loc: str, ignore_hidden_files: bool = True):
return r return r
def list_all_paths(base: str, cli):
from game_backuper.config import ConfigPath, ConfigOLeveldb
r = []
for c in cli:
if isinstance(c, ConfigPath):
if isabs(c.path):
bp = c.path
else:
bp = join(base, c.path)
if isfile(bp):
r.append(bp)
elif isdir(bp):
r += listdirs(bp, c.ignore_hidden_files)
elif isinstance(c, ConfigOLeveldb):
r.append(c.path if isabs(c.path) else join(base, c.path))
return r
def new_file(loc: str, name: str, prog: str, type: FileType = None) -> File: def new_file(loc: str, name: str, prog: str, type: FileType = None) -> File:
if exists(loc): if exists(loc):
fs = stat(loc).st_size fs = stat(loc).st_size
with open(loc, 'rb') as f: with open(loc, 'rb') as f:
hs = sha512(f) hs = sha512(f)
return File(None, name, fs, prog, hs, type) return File(None, name, fs, prog, hs, type)
def remove_dirs(loc: str):
bl = listdirs(loc, False)
for i in bl:
if isfile(i):
remove(i)
elif isdir(i):
try:
remove_dirs(i)
except Exception:
remove_dirs(i)
remove(loc)

56
game_backuper/restorer.py Normal file
View File

@@ -0,0 +1,56 @@
from threading import Thread
from game_backuper.config import Config, Program, ConfigPath
from game_backuper.db import Db
from os.path import join, relpath, isabs, isfile, isdir, exists
from game_backuper.file import (
list_all_paths,
copy_file,
remove_dirs,
new_file,
)
from os import remove
class RestoreTask(Thread):
def __init__(self, prog: Program, db: Db, cfg: Config):
Thread.__init__(self, name=f"Restore_{prog.name}")
self.cfg = cfg
self.prog = prog
self.db = db
def run(self):
b = self.prog.base
prog = self.prog.name
li = self.db.get_file_list(prog)
cli = self.prog.all_configs
pl = list_all_paths(b, cli)
for fn in li:
f = self.db.get_file(prog, fn)
r = self.prog.get_config(fn)
if isinstance(r, ConfigPath):
if f.type is not None:
raise ValueError('Type dismatched.')
nam = r.name if r.name else r.path
src = join(self.cfg.dest, prog, fn)
tmp = relpath(fn, nam)
if isabs(r.path):
dest = r.path
else:
dest = join(b, r.path)
if not tmp.startswith('.'):
dest = join(dest, tmp)
if dest in pl:
pl.remove(dest)
if exists(dest):
tf = new_file(dest, nam, prog)
if tf.size == f.size and tf.hash == f.hash:
print(f'{prog}: Skip {f.file}')
continue
copy_file(src, dest, nam, prog)
for i in pl:
if isfile(i):
remove(i)
print(f'{prog}: Removed {i}')
elif isdir(i):
remove_dirs(i)
print(f'{prog}: Removed {i}')

View File

@@ -1,7 +1,7 @@
# flake8: noqa # flake8: noqa
import sys import sys
from game_backuper import __version__ from game_backuper import __version__
if len(sys.argv) == 2 and sys.argv[1] == "py2exe": if "py2exe" in sys.argv:
from distutils.core import setup from distutils.core import setup
import py2exe import py2exe
params = { params = {
@@ -29,7 +29,11 @@ else:
"install_requires": ["pyyaml"], "install_requires": ["pyyaml"],
'entry_points': { 'entry_points': {
'console_scripts': ['game-backuper = game_backuper:start'] 'console_scripts': ['game-backuper = game_backuper:start']
} },
"extras_require": {
"leveldb": "plyvel"
},
"python_requires": ">=3.6"
} }
setup( setup(
name="game-backuper", name="game-backuper",