Skip to content

Commit ffaf8c3

Browse files
committed
allow to create backup from command line
1 parent d5ae1ab commit ffaf8c3

File tree

2 files changed

+128
-81
lines changed

2 files changed

+128
-81
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ Run the application.
2323
./backup.py
2424
```
2525

26+
## Run from Command Line
27+
28+
| Option | Type | Description |
29+
| ------ | ---- | ----------- |
30+
| -p, --project | str | Specify backup project file |
31+
| -t, --target | str | Run from command line and save backup to target directory |
32+
2633
## Install Development Dependencies
2734

2835
```bash

backup.py

Lines changed: 121 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
import shutil
88
import hashlib
99
from pathlib import Path
10+
from dataclasses import dataclass
11+
1012
import tkinter as tk
1113
from tkinter import ttk
1214
from tkfilebrowser import askopendirnames, askopenfilenames, askopenfilename, askopendirname
1315
from PIL import Image, ImageTk
1416
import yaml
15-
1617
import tk_async_execute as tae
1718

1819
DEFAULT_TITLE = 'Backup'
@@ -27,12 +28,100 @@ def load_image(filename):
2728
img2.paste(img)
2829
return ImageTk.PhotoImage(img2)
2930

31+
32+
@dataclass
33+
class BackupItem:
34+
"""Backup Item."""
35+
name: str
36+
itemtype: str
37+
path: str
38+
39+
@dataclass
40+
class BackupFile:
41+
"""Backup File."""
42+
source: str
43+
target: str
44+
45+
class BackupProject:
46+
"""Backup Project."""
47+
48+
filename: str
49+
items: list[BackupItem]
50+
51+
def __init__(self, filename: str):
52+
self.filename = filename
53+
self.items = []
54+
try:
55+
with open(self.filename, 'r', encoding='utf-8') as f:
56+
doc = yaml.load(f, Loader=yaml.SafeLoader)
57+
if doc['schema_version'] != 1:
58+
raise AssertionError('unknown schema')
59+
for item in doc['items']:
60+
name = item['name']
61+
itemtype = item['type']
62+
path = item['path']
63+
self.items.append(BackupItem(name, itemtype, path))
64+
# pylint: disable-next=broad-exception-caught
65+
except Exception as ex:
66+
print(f'warn: failed to load project \'{self.filename}\': {ex}')
67+
self.items = []
68+
69+
def save(self):
70+
"""Saves the backup project."""
71+
try:
72+
items = []
73+
for item in self.items:
74+
items.append({'name': item.name, 'type': item.itemtype, 'path': item.path})
75+
data = {'schema_version': 1, 'items': items}
76+
with open(self.filename, 'w', encoding='utf-8') as f:
77+
f.write(yaml.dump(data, sort_keys=False))
78+
# pylint: disable-next=broad-exception-caught
79+
except Exception as ex:
80+
print(f'failed to save \'{self.filename}\': {ex}')
81+
82+
def backup(self, folder):
83+
"""Performs a backup."""
84+
if not os.path.exists(folder):
85+
os.makedirs(folder)
86+
87+
if os.listdir(folder):
88+
print('error: folder is not empty')
89+
return
90+
91+
files = []
92+
for item in self.items:
93+
if item.itemtype == 'Folder':
94+
for dirname, _, fnames in os.walk(item.path):
95+
targetdir = dirname[len(os.path.commonpath([item.path, dirname])):]
96+
if targetdir.startswith('/'):
97+
targetdir = targetdir[1:]
98+
targetdir = os.path.join(item.name, targetdir)
99+
for fname in fnames:
100+
source = os.path.join(dirname, fname)
101+
target = os.path.join(targetdir, fname)
102+
files.append(BackupFile(source, target))
103+
elif item.itemtype == 'File':
104+
files.append(BackupFile(item.path, item.name))
105+
106+
with open(os.path.join(folder, 'checksums.txt'), 'w',
107+
newline='\n', encoding='utf-8') as checksum_file:
108+
for file in files:
109+
target = os.path.join(folder, file.target)
110+
os.makedirs(os.path.dirname(target), exist_ok=True)
111+
shutil.copy2(file.source, target)
112+
with open(target, 'rb') as f:
113+
digest = hashlib.file_digest(f, "sha256")
114+
checksum_file.write(f'SHA256 ({file.target}) = {digest.hexdigest()}\n')
115+
30116
# pylint: disable-next=too-many-instance-attributes
31117
class MainWindow:
32118
"""MainWindow class."""
33119

120+
root: tk.Tk
121+
project: BackupProject
122+
34123
# pylint: disable-next=too-many-locals,too-many-statements
35-
def __init__(self, project: str):
124+
def __init__(self, project: BackupProject):
36125
root = tk.Tk()
37126
self.root = root
38127
root.title(DEFAULT_TITLE)
@@ -136,15 +225,24 @@ def __init__(self, project: str):
136225
self.progressbar.pack(side=tk.TOP, fill=tk.X, padx=(5,5), pady=(5,5))
137226

138227
# load project
139-
items = self.load_project()
228+
items = self.project.items
140229
for item in items:
141-
name = item['name']
142-
itemtype = item['type']
143-
path = item['path']
144-
image = self.get_image(itemtype)
145-
self.listbox.insert('', tk.END, text=name, image=image, values=(itemtype , path))
230+
image = self.get_image(item.itemtype)
231+
self.listbox.insert('', tk.END, text=item.name,
232+
image=image, values=(item.itemtype , item.path))
146233
self.on_selection_changed(None)
147234

235+
def get_items(self) -> list[BackupItem]:
236+
"""Return the list of items"""
237+
items = []
238+
for itemid in self.listbox.get_children():
239+
item = self.listbox.item(itemid)
240+
name = item['text']
241+
itemtype = item['values'][0]
242+
path = item['values'][1]
243+
items.append(BackupItem(name, itemtype, path))
244+
return items
245+
148246
def get_image(self, itemtype: str):
149247
"""Returns an image by the provided items type."""
150248
if itemtype == 'Folder':
@@ -153,37 +251,10 @@ def get_image(self, itemtype: str):
153251
return self.file_image
154252
return self.file_image
155253

156-
def load_project(self):
157-
"""Loads the project file."""
158-
items = []
159-
try:
160-
with open(self.project, 'r', encoding='utf-8') as f:
161-
doc = yaml.load(f, Loader=yaml.SafeLoader)
162-
if doc['schema_version'] != 1:
163-
raise AssertionError('unknown schema')
164-
for item in doc['items']:
165-
items.append(item)
166-
# pylint: disable-next=broad-exception-caught
167-
except Exception as ex:
168-
print(f'warn: failed to load project \'{self.project}\': {ex}')
169-
return items
170-
171254
def save_project(self):
172255
"""Saves the proves file."""
173-
try:
174-
items = []
175-
for itemid in self.listbox.get_children():
176-
item = self.listbox.item(itemid)
177-
name = item['text']
178-
itemtype = item['values'][0]
179-
path = item['values'][1]
180-
items.append({'name': name, 'type': itemtype, 'path': path})
181-
data = {'schema_version': 1, 'items': items}
182-
with open(self.project, 'w', encoding='utf-8') as f:
183-
f.write(yaml.dump(data, sort_keys=False))
184-
# pylint: disable-next=broad-exception-caught
185-
except Exception as ex:
186-
print(f'failed to save \'{self.project}\': {ex}')
256+
self.project.items = self.get_items()
257+
self.project.save()
187258

188259
def add_folder(self):
189260
"""Adds a new folder."""
@@ -262,53 +333,15 @@ def start_backup(self):
262333
folder = askopendirname()
263334
if folder:
264335
print('backup started')
265-
items = []
266-
for itemid in self.listbox.get_children():
267-
item = self.listbox.item(itemid)
268-
name = item['text']
269-
itemtype = item['values'][0]
270-
path = item['values'][1]
271-
items.append({'name': name, 'type': itemtype, 'path': path})
336+
self.project.items = self.get_items()
272337
self.progressbar.start()
273-
tae.async_execute(self.do_backup(items, folder),
338+
tae.async_execute(self.do_backup(self.project, folder),
274339
callback=self.finish_backup, wait=False, visible=False)
275340

276341
# pylint: disable-next=too-many-locals
277-
async def do_backup(self, items, folder):
342+
async def do_backup(self, project, folder):
278343
"""Performs the actual backup"""
279-
# get file list
280-
if os.listdir(folder):
281-
print('error: folder is not empty')
282-
return
283-
284-
files = []
285-
for item in items:
286-
itemtype = item['type']
287-
if itemtype == 'Folder':
288-
path = item['path']
289-
for dirname, _, fnames in os.walk(item['path']):
290-
targetdir = dirname[len(os.path.commonpath([path, dirname])):]
291-
if targetdir.startswith('/'):
292-
targetdir = targetdir[1:]
293-
targetdir = os.path.join(item['name'], targetdir)
294-
for fname in fnames:
295-
source = os.path.join(dirname, fname)
296-
target = os.path.join(targetdir, fname)
297-
files.append({'source': source, 'target': target})
298-
elif itemtype == 'File':
299-
files.append({'source': item['path'], 'target': item['name']})
300-
301-
with open(os.path.join(folder, 'checksums.txt'), 'w',
302-
newline='\n', encoding='utf-8') as checksum_file:
303-
for file in files:
304-
source = file['source']
305-
rel_target = file['target']
306-
target = os.path.join(folder, rel_target)
307-
os.makedirs(os.path.dirname(target), exist_ok=True)
308-
shutil.copy2(source, target)
309-
with open(target, 'rb') as f:
310-
digest = hashlib.file_digest(f, "sha256")
311-
checksum_file.write(f'SHA256 ({rel_target}) = {digest.hexdigest()}\n')
344+
project.backup(folder)
312345

313346
def on_closing(self):
314347
"""Saves the project when the application is closed."""
@@ -331,9 +364,16 @@ def main():
331364
default_project = os.path.join(Path.home(), 'backup-py.yaml')
332365
parser = argparse.ArgumentParser()
333366
parser.add_argument('-p', '--project', type=str, help='project file', default=default_project)
367+
parser.add_argument('-t', '--target', type=str, help='target directory', default=None)
334368
args = parser.parse_args()
335-
app = MainWindow(args.project)
336-
app.run()
369+
370+
project = BackupProject(args.project)
371+
372+
if args.target:
373+
project.backup(args.target)
374+
else:
375+
app = MainWindow(project)
376+
app.run()
337377

338378

339379
if __name__ == '__main__':

0 commit comments

Comments
 (0)