Coverage for /home/agp/Documents/me/code/brotation/brotation/brotation.py : 97%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Main module.
3Rotation Policy
4================
5Rotation policy is also supported:
7- 7-daily (mon-sun) : e.g. flowmonitor.git.d6.7z
8- 1-per-week of the month: e.g. flowmonitor.git.w0.7z
9- 1 per month: e.g. flowmonitor.git.m9.7z
10- 1 per year: e.g. flowmonitor.git.y17.7z
12So we need 7 + 5 + 12 = 24 files to cover a whole year.
14In each backup the dairy backup is generated and then is cloned
15modifying the rotation names, so the compressed file only is sent once
16and hence all files correspond with the last update.
18As soon a new day comes, some of the previous backups files will be left
19behind creating the rotation sequence.
21'womanly_speech.d2.xz',
22'womanly_speech.d5.xz',
23'womanly_speech.m01.xz',
24'womanly_speech.m11.7z',
25'womanly_speech.m11.xz',
26'womanly_speech.m12.7z',
27'womanly_speech.m12.xz',
28'womanly_speech.m3.7z',
29'womanly_speech.m5.7z',
30'womanly_speech.m6.xz',
31'womanly_speech.m8.xz',
32'womanly_speech.w0.xz',
33'womanly_speech.w13.xz',
34'womanly_speech.w17.7z',
35'womanly_speech.w17.xz',
36'womanly_speech.w2.7z',
37'womanly_speech.w26.7z',
38'womanly_speech.w39.7z',
39'womanly_speech.w9.7z',
40'womanly_speech.y11.7z',
41'womanly_speech.y13.xz',
42'womanly_speech.y14.7z',
43'womanly_speech.y15.xz',
44'womanly_speech.y17.7z',
45'womanly_speech.y20.xz'
47# rules:
491. get day, week, month numbers
502. if month goes to 1, then last month became last year.
513. if week goes back to 0, then last week became last month.
524. if days goes back to 0, then last day became last week.
54# steps:
561. create the dictionary of existing rotate files.
572. compte the current day.
583. chech above rules in order.
594. try to get the last
62d{n} --> d{}
64"""
66import re
67import os
68import tarfile
69from subprocess import Popen, PIPE
72from gutools.tools import expandpath, soft_update, fileiter
73from gutools.ushift import Rotator, FQItem
75# --------------------------------------------------
76# logger
77# --------------------------------------------------
78from gutools.loggers import logger, \
79 trace, exception, debug, info, warn, error
80log = logger(__name__)
83rotate_match = re.compile(r'(?P<key>.+?)\.(?P<rot>(d[0-6]|w[0-9]|w[0-4][0-9]|w5[1-3]|\d|m[0-9]|m1[0-2]?|m0[0-9]|y\d{2}))\.(?P<ext>7z|xz)$', re.DOTALL).match
86def extract_info(path):
87 """*Extract rotate info from path.*
88 """
89 m = rotate_match(path)
90 if m:
91 return FQItem(m.groupdict())
94def parse_config_file(path):
95 config = dict()
96 path = expandpath(path)
97 if os.path.exists(path):
98 reg = re.compile(r'^(?P<key>\w+)\s*="?(?P<value>.*?)"?$')
99 reg2 = re.compile(r'([^\s]+)\s*')
100 for line in open(path, 'rt'):
101 m = reg.match(line)
102 if m:
103 key, value = m.groups()
104 value2 = [x.group() for x in reg2.finditer(value)]
105 if len(value2) > 1:
106 value = value2
107 config[key] = value
109 return config
112class BackupRotator(Rotator):
113 RANGES = [
114 ('d', 0, 6),
115 ('w', 0, 4),
116 ('m', 1, 12),
117 ('y', 10, None),
118 ]
120 FLAGS = {
121 'xz': ['cJf', '--threads=0'],
122 'gz': ['cxf'],
123 }
124 """A class ...
126 """
128 def __init__(self, root, config=None, config_file=None, *args, **kw):
129 super().__init__(self.RANGES)
130 self.root = root
131 self.config_file = config_file or '~/.config/backups.conf'
132 self.config = config
133 debug(f"root: {root}")
135 def _parse(self, element):
136 item = extract_info(element)
137 if item:
138 item['rot'], value = item['rot'][0], int(item['rot'][1:])
139 item[item['rot']] = value
140 return item
142 def _rebuild(self, item):
143 """*Reconstruct an element from parsed data.*"""
144 return "{key}.{rot}{value}.{ext}".format(**item)
145 # return f"{item['rot']}{item[item['rot']]}" + "[{d}.{w}.{m}.{y}]".format(**item)
147 def apply(self, items):
148 """*xxx*
150 - load config if not already downloaded.
151 - xax
154 """
155 if self.config is None:
156 self.config = parse_config_file(self.config_file)
158 root = '/tmp/kk'
159 root = '~/Documents/me/code/gutools'
160 where = '/tmp/'
162 firstone = None
163 root = expandpath(root)
164 for path in items:
165 if not firstone:
166 firstone = path
167 output = os.path.join(where, repr(path))
168 self.compress(output, root)
169 foo = 1
170 foo = 1
172 def compress(self, output, root, cwd='/'):
173 """*Compress folder using tar and xz utitlities.*
174 - 'output': the compressed tar file name.
175 - 'root': folder to compress.
176 - 'cwd' is a relative path from where the files would be included
178 The approach is to use system call directly for faster execution.
179 Using tarfile library implies python intervention, that will be
180 slower than system call, specially running in a raspberry.
181 """
182 ext = os.path.splitext(output)[-1][1:]
183 # with tarfile.open(output, f"w:{ext}") as tar:
184 #tar.add(root, arcname=os.path.basename(root))
186 root = root.lstrip(cwd)
187 name = root.replace('/', '.')
188 cmd = ['tar']
189 cmd.extend(self.FLAGS[ext])
190 cmd.extend([output, root])
191 with Popen(cmd, stdout=PIPE, cwd=cwd) as proc:
192 print(proc.stdout.read())
193 foo = 1
194 foo = 1
196 def load_config(self, config):
197 """
199 """
201 foo = 1