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