-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.py
More file actions
executable file
·133 lines (108 loc) · 4.15 KB
/
backup.py
File metadata and controls
executable file
·133 lines (108 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env python3
import sys
import os
import codecs
import time
import logging
import yaml
import coloredlogs
from sh import rsync, wget
logger = logging.getLogger("backup")
coloredlogs.install(logger=logger)
logger.setLevel("DEBUG")
class Backup:
def load_config(self, file="config.yaml"):
with open("config.yaml") as f:
config = yaml.safe_load(f)
self.UUID_LIST = config.get("drives", [])
self.PATHS = config.get("paths", [])
self.EXCLUDES = config.get("excludes", [])
# /mount/path/SNAP/ is base path
# /mount/path/SNAP/LAST is latest snapshot
# /mount/path/SNAP/YYYY-MM-DD snapshot with timestamp
self.SNAP = config.get("base-dir", "snapshots")
self.LAST = config.get("last-dir", "last")
# rsync options
self.OPT = "-vvaPh"
self.EXCLUDES_URL = "https://raw.githubusercontent.com/rubo77/rsync-homedir-excludes/master/rsync-homedir-excludes.txt"
self.EXCLUDES_FILE = "excludes.txt"
self.DIR_MODE = int("0o755", 8)
def download_excludes(self):
wget("-O", self.EXCLUDES_FILE, self.EXCLUDES_URL)
with open(self.EXCLUDES_FILE, "a") as f:
f.write("\n\n#config.yaml:\n" + "\n".join(self.EXCLUDES) + "\n")
def get_mounts(self):
mounts = {}
with open("/proc/mounts") as f:
for l in f:
if l.startswith("/") and " " in l:
k, v = l.split()[:2]
mounts[k] = codecs.unicode_escape_decode(v)[0]
return mounts
def get_snapshot_drives(self, uuids=None):
if not uuids:
uuids = self.UUID_LIST
mounts = self.get_mounts()
drives = []
for uuid in uuids:
dev = os.path.realpath(os.path.join("/dev/disk/by-uuid", uuid))
try:
snapshot_path = os.path.join(mounts[dev], self.SNAP)
except KeyError:
drives.append((uuid, None))
continue
drives.append((uuid, (dev, snapshot_path)))
return drives
def create_all_snapshots(self, uuids=None):
drives = self.get_snapshot_drives(uuids)
for drive in drives:
if drive[1]:
self.create_snapshot(drive)
else:
logger.warning("[%s] not mounted", drive[0])
def create_snapshot(self, drive):
uuid, (dev, path) = drive
if os.path.exists(path):
logger.info("[%s] found snapshot dir at '%s'", uuid, path)
else:
os.makedirs(path, mode=self.DIR_MODE, exist_ok=True)
logger.info("[%s] created '%s'", uuid, path)
last_path = os.path.join(path, self.LAST)
snapshot_path = self.get_snapshot_path(path)
for src in self.PATHS:
dest = os.path.join(snapshot_path, src.strip("/"))
os.makedirs(os.path.dirname(dest), mode=self.DIR_MODE, exist_ok=True)
segment_count = 1 + len([x for x in src.split("/") if x])
link_dest = os.path.join("../"*segment_count, self.LAST, src.strip("/"))
self.rsync(src, dest, link_dest)
self.recreate_symlink(snapshot_path, last_path)
os.sync()
def rsync(self, src, dest, link_dest):
rsync(
self.OPT, src + "/", dest,
link_dest=link_dest,
exclude_from=self.EXCLUDES_FILE,
_out=sys.stdout.buffer
)
def recreate_symlink(self, snapshot_path, last_path):
try:
os.remove("-f", last_path, _out=sys.stdout.buffer)
except OSError:
pass
os.symlink(os.path.relpath(snapshot_path, os.path.dirname(last_path)), last_path)
def get_snapshot_path(self, path):
date = time.strftime("%Y-%m-%d")
snapshot_path = os.path.join(path, date)
i = 1
while os.path.exists(snapshot_path):
snapshot_path = os.path.join(path, "%s_%d" % (date, i))
i += 1
return snapshot_path
if __name__ == "__main__":
if os.getuid() != 0:
print("you must be root")
sys.exit(1)
backup = Backup()
backup.load_config()
backup.download_excludes()
backup.create_all_snapshots()