Add lib
Adds backup and storage libraries to ease backing up of files on command.
This commit is contained in:
parent
eedecb34d5
commit
067ab48de0
2 changed files with 266 additions and 0 deletions
129
lib/backup.py
Normal file
129
lib/backup.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
"""
|
||||||
|
Backup Module:
|
||||||
|
|
||||||
|
Classes:
|
||||||
|
- BackupManager
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from . import Storage
|
||||||
|
|
||||||
|
__all__ = ["BackupManager"]
|
||||||
|
|
||||||
|
class BackupManager:
|
||||||
|
"""
|
||||||
|
Facilitates backup creation. Stores all backups in ~/.storage/backups
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- create_backup()
|
||||||
|
- get_backups()
|
||||||
|
- get_delete_candidates()
|
||||||
|
- delete_excess_backups()
|
||||||
|
"""
|
||||||
|
def __init__(self, target_path: Path, backup_storage: Storage = Storage("backups"),
|
||||||
|
date_format: str = "%d_%m_%y__%H%M%S", separator: str = "-") -> None:
|
||||||
|
"""
|
||||||
|
Test
|
||||||
|
"""
|
||||||
|
self.target_path = target_path
|
||||||
|
self.backup_storage = backup_storage
|
||||||
|
self.date_format = date_format
|
||||||
|
self.separator = separator
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_path(self) -> Path:
|
||||||
|
"""
|
||||||
|
Target to create backups of. This could be a file or folder.
|
||||||
|
"""
|
||||||
|
return self._target_path
|
||||||
|
|
||||||
|
@target_path.setter
|
||||||
|
def target_path(self, new_path: Path):
|
||||||
|
if isinstance(new_path, Path):
|
||||||
|
if new_path.exists() and (new_path.is_dir() or new_path.is_file()):
|
||||||
|
self._target_path = new_path
|
||||||
|
else:
|
||||||
|
raise ValueError(new_path)
|
||||||
|
else:
|
||||||
|
raise TypeError(new_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_storage(self) -> Storage:
|
||||||
|
"""
|
||||||
|
Storage object to store created backups in.
|
||||||
|
"""
|
||||||
|
return self._backup_storage
|
||||||
|
|
||||||
|
@backup_storage.setter
|
||||||
|
def backup_storage(self, new_storage: Storage):
|
||||||
|
if isinstance(new_storage, Storage):
|
||||||
|
self._backup_storage = new_storage
|
||||||
|
else:
|
||||||
|
raise TypeError(new_storage)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_format(self) -> str:
|
||||||
|
"""The datetime format string used to date the backups."""
|
||||||
|
return self._date_format
|
||||||
|
|
||||||
|
@date_format.setter
|
||||||
|
def date_format(self, new_format):
|
||||||
|
if isinstance(new_format, str):
|
||||||
|
self._date_format = new_format
|
||||||
|
else:
|
||||||
|
raise TypeError(new_format)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def separator(self) -> str:
|
||||||
|
"""The separator that separates the archive name from the date"""
|
||||||
|
return self._separator
|
||||||
|
|
||||||
|
@separator.setter
|
||||||
|
def separator(self, new_separator):
|
||||||
|
if isinstance(new_separator, str):
|
||||||
|
if new_separator not in self.target_path.name and new_separator not in self.date_format:
|
||||||
|
self._separator = new_separator
|
||||||
|
else:
|
||||||
|
raise ValueError(new_separator)
|
||||||
|
else:
|
||||||
|
raise TypeError(new_separator)
|
||||||
|
|
||||||
|
def create_backup(self):
|
||||||
|
"""
|
||||||
|
Create a backup of the target path, stored in the backup storage folder.
|
||||||
|
"""
|
||||||
|
date_string = datetime.now().strftime(self.date_format)
|
||||||
|
backup_name = f"{self.target_path.name}{self.separator}{date_string}.zip"
|
||||||
|
backup_path = self.backup_storage.get_file(backup_name)
|
||||||
|
with ZipFile(backup_path, mode="w") as zip_file:
|
||||||
|
for item in self.target_path.glob("**/*"):
|
||||||
|
zip_file.write(item, item.relative_to(self.target_path))
|
||||||
|
|
||||||
|
def get_backups(self):
|
||||||
|
"""
|
||||||
|
Get all backups found in the given folder.
|
||||||
|
"""
|
||||||
|
return self.backup_storage.list_files(f"{self.target_path.stem}{self.separator}*.zip")
|
||||||
|
|
||||||
|
def get_delete_candidates(self, max_backup_count) -> list[Path]:
|
||||||
|
"""
|
||||||
|
Get all candidates for deletion with the given max_backup_count.
|
||||||
|
If none are available for deletion, returns None.
|
||||||
|
"""
|
||||||
|
def get_date(file: Path) -> datetime:
|
||||||
|
"""
|
||||||
|
Turns the datetime string in the file name into a datetime object.
|
||||||
|
"""
|
||||||
|
date_string = file.name.split(self.separator)[1].replace(file.suffix, "")
|
||||||
|
return datetime.strptime(date_string, self.date_format)
|
||||||
|
|
||||||
|
backups = self.get_backups()
|
||||||
|
backups.sort(key=get_date)
|
||||||
|
return backups[:(len(backups)-max_backup_count)] # returns the oldest excess backups
|
||||||
|
|
||||||
|
def delete_excess_backups(self, max_backup_count: int):
|
||||||
|
"""Delete all excess backups"""
|
||||||
|
for file in self.get_delete_candidates(max_backup_count):
|
||||||
|
file.unlink()
|
137
lib/storage.py
Normal file
137
lib/storage.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
"""
|
||||||
|
Storage Module:
|
||||||
|
|
||||||
|
Classes:
|
||||||
|
- Storage
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
__all__ = ['Storage']
|
||||||
|
|
||||||
|
class Storage:
|
||||||
|
"""
|
||||||
|
Unifies all filesystem access methods under a single class.
|
||||||
|
Minimizes room for mistakes and improves reusability.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- get_file()
|
||||||
|
- get_folder()
|
||||||
|
- write_file()
|
||||||
|
- read_file()
|
||||||
|
- delete_file()
|
||||||
|
- rename_file()
|
||||||
|
- add_file()
|
||||||
|
- list_files()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, folder: str, root_folder: str = ".storage") -> None:
|
||||||
|
"""
|
||||||
|
Create a new storage instance, it automatically creates and manages a folder in the user's home directory, all you have to do is supply a subfolder to store files in.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder (str): the name of the folder to use.
|
||||||
|
"""
|
||||||
|
self.root = Path.home().joinpath(root_folder)
|
||||||
|
self.folder = self.root.joinpath(folder)
|
||||||
|
self.folder.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
def get_file(self, path: str) -> Path:
|
||||||
|
"""Get the fully qualified path of a file in the folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): the name of the file to grab.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: the path of the file.
|
||||||
|
"""
|
||||||
|
return self.folder.joinpath(path)
|
||||||
|
|
||||||
|
def get_folder(self, name: str) -> Storage:
|
||||||
|
"""
|
||||||
|
Get a folder inside the Storage directory.
|
||||||
|
Returns a Storage object representing that folder.
|
||||||
|
"""
|
||||||
|
return Storage(name, root_folder=self.folder.name)
|
||||||
|
|
||||||
|
def write_file(self, name: str, data: dict):
|
||||||
|
"""Write data to the given file in JSON format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the file.
|
||||||
|
data (dict): the data to write.
|
||||||
|
"""
|
||||||
|
file = self.get_file(name)
|
||||||
|
file.touch(exist_ok=True)
|
||||||
|
with file.open("w+", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
|
||||||
|
def read_file(self, name: str) -> dict:
|
||||||
|
"""Read data from the given file in JSON format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: the data from the file.
|
||||||
|
"""
|
||||||
|
file = self.get_file(name)
|
||||||
|
if not file.exists():
|
||||||
|
return {}
|
||||||
|
with file.open("r+", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def delete_file(self, name: str):
|
||||||
|
"""Delete the given file from the folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): the name of the file.
|
||||||
|
"""
|
||||||
|
file = self.get_file(name)
|
||||||
|
file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
def rename_file(self, old_name: str, new_name: str):
|
||||||
|
"""Rename a file in the folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
old_name (str): the current name of the file.
|
||||||
|
new_name (str): the new name of the file.
|
||||||
|
"""
|
||||||
|
file = self.get_file(old_name)
|
||||||
|
new_file = self.folder.joinpath(new_name)
|
||||||
|
file.rename(new_file)
|
||||||
|
|
||||||
|
def add_file(self, name: str, path: Path | None = None, binary: bytes | None = None):
|
||||||
|
"""Add a copy of a file to the folder.
|
||||||
|
If a binary stream is given, it is saved directly to the named location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name to save it under.
|
||||||
|
path (Path): The path of the file to copy from.
|
||||||
|
binary (BinaryIO): The binary stream to copy from.
|
||||||
|
"""
|
||||||
|
if path is not None and binary is not None:
|
||||||
|
raise ValueError(binary, "Cannot supply both a path and a binary stream.")
|
||||||
|
elif path is not None:
|
||||||
|
shutil.copy(path, self.folder.joinpath(name))
|
||||||
|
elif binary is not None:
|
||||||
|
with self.folder.joinpath(name).open("wb+") as f:
|
||||||
|
f.write(binary)
|
||||||
|
|
||||||
|
def list_files(self, pattern: str | None = None) -> list[Path]:
|
||||||
|
"""
|
||||||
|
Return a list of all files in the directory.
|
||||||
|
"""
|
||||||
|
files: list[Path] = []
|
||||||
|
if pattern is None:
|
||||||
|
for file in self.folder.iterdir():
|
||||||
|
files.append(file)
|
||||||
|
else:
|
||||||
|
for file in self.folder.glob(pattern):
|
||||||
|
files.append(file)
|
||||||
|
return files
|
Loading…
Reference in a new issue