Last active
January 8, 2020 08:37
-
-
Save Tantalus13A98B5F/0399cebd902adcd0ed05bf0207d8fe78 to your computer and use it in GitHub Desktop.
A simple backup script for Windows, based on `backup.json`, generating a tarball file. Useful if you want to pack up your `AppData\Roaming` files.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from contextlib import contextmanager | |
from fnmatch import fnmatch | |
from glob import glob | |
import subprocess as subp | |
import datetime as dt | |
import logging | |
import tarfile | |
import json | |
import sys | |
import os | |
import io | |
'''SAMPLE backup.json | |
{ | |
"current": "myprofile", | |
"profiles": { | |
"myprofile": { | |
"includeWSL": true, | |
"includePATH": true, | |
"includeCodeExt": true, | |
"files": [ | |
{"exclude": true, "path": "/path/to/exclude"}, | |
"/path/to/backup", | |
["/item/to/ignore"] | |
], | |
"wslfiles": [ "..." ] | |
} | |
} | |
} | |
''' | |
CURDIR, FNAME = os.path.split(os.path.abspath(__file__)) | |
FSROOT = os.path.splitdrive(CURDIR)[0] + '/' | |
os.chdir(CURDIR) | |
logging.basicConfig(level=logging.INFO) | |
logging.info('chdir %r', CURDIR) | |
@contextmanager | |
def binary_stdout(): | |
fd = sys.stdout.fileno() | |
with os.fdopen(fd, 'wb', closefd=False) as fp: | |
yield fp | |
def get_timestr(): | |
return dt.datetime.now().strftime('%Y.%m.%d-%H.%M.%S') | |
def tar_add_virtual_file(tar, name, byts): | |
ti = tarfile.TarInfo(name) | |
ti.size = len(byts) | |
fp = io.BytesIO(byts) | |
tar.addfile(ti, fp) | |
def get_output(args, **kwargs): | |
return subp.run(args, **kwargs, stdout=subp.PIPE).stdout | |
def load_json_file(fn): | |
with open(fn) as fp: | |
return json.load(fp) | |
class BackupRunner: | |
configuration = 'backup.json' | |
def __init__(self): | |
self._exclude_list = [] | |
def _is_exclude(self, ti): | |
for pat in self._exclude_list: | |
if fnmatch(FSROOT + ti.name, pat): | |
logging.info('excluding %r due to pattern %r', ti.name, pat) | |
return None | |
return ti | |
def _add_tar_item(self, tar, item): | |
tar.add(item, filter=self._is_exclude) | |
def _iter_items(self, filelist): | |
for item in filelist: | |
# {"exclude": true, "path": "/path/to/exclude"} | |
if hasattr(item, 'get') and \ | |
item.get('exclude') and item.get('path'): | |
tmp = os.path.expanduser(item['path']) | |
self._exclude_list.append(tmp) | |
# "/path/to/backup" | |
elif isinstance(item, str): | |
tmp = glob(os.path.expanduser(item)) | |
if not tmp: | |
logging.info('found no matching file %r', item) | |
yield from tmp | |
# ["/item/to/ignore"] | |
else: | |
logging.info('ignoring illegal item %r', item) | |
def wslmode(self, profile): | |
tmpfp = io.BytesIO() | |
with tarfile.open(None, 'w', tmpfp) as tar: | |
for item in self._iter_items(profile['wslfiles']): | |
self._add_tar_item(tar, item) | |
# tarfile must be seekable, thus cannot use stdout directly | |
with binary_stdout() as fp: | |
fp.write(tmpfp.getvalue()) | |
def mainmode(self, filename, profile): | |
with tarfile.open(filename, 'w:gz') as tar: | |
for item in self._iter_items(profile['files']): | |
self._add_tar_item(tar, item) | |
tar.add(self.configuration) | |
tar.add(FNAME) | |
if profile.get('includeWSL'): | |
byts = get_output(['wsl', 'python3', FNAME, 'wslmode']) | |
tar_add_virtual_file(tar, 'wsl.tar', byts) | |
if profile.get('includeCodeExt'): | |
byts = get_output(['code', '--list-extensions'], shell=True) | |
tar_add_virtual_file(tar, 'vscode_extensions.txt', byts) | |
if profile.get('includePATH'): | |
byts = os.environ['PATH'].replace(os.pathsep, '\n').encode() | |
tar_add_virtual_file(tar, 'path.txt', byts) | |
@classmethod | |
def main(cls): | |
self = cls() | |
data = load_json_file(cls.configuration) | |
pname = data['current'] | |
profile = data['profiles'][pname] | |
if tuple(sys.argv[1:]) == ('wslmode',): | |
self.wslmode(profile) | |
else: | |
filename = 'backup-%s-%s.tar.gz' % (pname, get_timestr()) | |
self.mainmode(filename, profile) | |
if __name__ == '__main__': | |
BackupRunner.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment