Skip to content

Instantly share code, notes, and snippets.

@Tantalus13A98B5F
Last active January 8, 2020 08:37
Show Gist options
  • Save Tantalus13A98B5F/0399cebd902adcd0ed05bf0207d8fe78 to your computer and use it in GitHub Desktop.
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.
#!/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