diff --git a/.env.example b/.env.example index ac20780..ba06ef8 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,15 @@ -# Path to the local folder to back up +# (Required) Absolute path to the local folder to back up SOURCE_FOLDER="" -# Name of the remote bucket (destination) +# (Required) Name of the remote bucket or container TARGET_BUCKET="" -# Cloud provider (gcs, s3, azure) +# (Required) Cloud provider (gcs, s3, azure) SERVICE_NAME="" -# Cloud provider access string or path to key file -SERVICE_KEY="" \ No newline at end of file +# (Required) Cloud provider access string or path to key file +SERVICE_KEY="" + +# ----------------------------------------------------------- + +# (Optional) Path to log file and level +LOG_FILE="" +LOG_LEVEL="WARNING" \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index efa8b99..1975223 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,9 +1,24 @@ +import os from dotenv import load_dotenv + from .db import Database, dbname from .fs import FileSystem, file_exists from .backup import Backup +# Required environment variables +required_vars = ( + "SOURCE_FOLDER", + "TARGET_BUCKET", + "SERVICE_NAME", + "SERVICE_KEY", + "LOG_LEVEL" +) + if not file_exists(".env"): raise FileNotFoundError("Environment variable file does not exist. Copy '.env.example' to '.env'") -load_dotenv() \ No newline at end of file +load_dotenv() + +# Check that required environment variables are set +if not all(map(lambda var: os.getenv(var), required_vars)): + raise SystemExit("One or more required environment variables in '.env' have not been set") \ No newline at end of file diff --git a/src/backup.py b/src/backup.py index 9b9848c..a071603 100644 --- a/src/backup.py +++ b/src/backup.py @@ -1,3 +1,6 @@ +import os +import logging +from logging.handlers import RotatingFileHandler from typing import Union from .cloud import Storage as StorageClient @@ -7,6 +10,7 @@ from . import dbname class Backup(FileSystem): def __init__(self): super().__init__() + self.enable_logging() self.has_change = False @@ -15,6 +19,37 @@ class Backup(FileSystem): self.compress = self.db.get_flag("COMPRESS") + # Configure logging + def enable_logging(self): + self.log = logging.getLogger(__name__) + self.log.debug("Start console logging") + log_format = logging.Formatter("[%(asctime)s][%(levelname)s]: %(name)s: %(message)s") + + # Log to console + log_console = logging.StreamHandler() + log_console.setLevel(logging.INFO) + log_console.setFormatter(log_format) + + self.log.addHandler(log_console) + + # Log to file + log_file_path = os.getenv("LOG_FILE") + if log_file_path: + self.log.debug("Start file logging") + log_file = RotatingFileHandler( + log_file_path, + mode = "a", + maxBytes = 50 * 1024 * 1024, + backupCount = 5, + encoding = None, + delay = False + ) + + log_file.setLevel(os.getenv("LOG_LEVEL")) + log_file.setFormatter(log_format) + + self.log.addHandler(log_file) + # Backup a file or folder def backup_item(self, item: Union[list, str], silent: bool = True) -> bool: if isinstance(item, str): @@ -32,31 +67,35 @@ class Backup(FileSystem): self.has_change = True - print(f"⧖ | Uploading: '{item[0]}'", end="\r") + self.log.info(f"'{item[0]}': Uploading") + print(f"⏳ | Uploading: '{item[0]}'", end="\r") blob = item # Upload as zip archive if self.compress: + self.log.debug(f"'{item[0]}': Compressing") blob = FileSystem.zip(blob) # Upload to cloud if self.cloud.upload(blob): - print(f"✓ | Upload sucessful: '{item[0]}'") + self.log.debug(f"'{item[0]}': Uploaded") + print(f"✅ | Upload successful: '{item[0]}'") # Update local database if not self.db.set_item(item): - print("🛈 | Failed to update database") + self.log.warn(f"'{item[0]}': Failed to update database") + print("⚠️ | Failed to update database") else: - print(f"✕ | Upload failed: '{item[0]}'") - if self.cloud.error: - print("🛈 | " + str(self.cloud.error)) - + self.log.error(f"'{item[0]}': {self.cloud.error}") + print(f"❌ | Upload failed: '{item[0]}'") # Remove temp zip if self.compress: FileSystem.delete(blob) + # Deprecated: Run when a single item is backed up directly if not silent and not self.has_change: - print("✓ | Up to date. No changes found") + self.log.info("No changes found") + print("✅ | Up to date. No changes found") return @@ -67,4 +106,5 @@ class Backup(FileSystem): self.backup_item(item) if not self.has_change: - print("✓ | Up to date. No changes found") \ No newline at end of file + self.log.info("No changes found") + print("✅ | Up to date. No changes found") \ No newline at end of file diff --git a/src/cloud/azure.py b/src/cloud/azure.py index cc939fe..6614297 100644 --- a/src/cloud/azure.py +++ b/src/cloud/azure.py @@ -27,5 +27,5 @@ class StorageClient: return True except Exception as e: if e.response.status_code == 403: - self.error = "Account lacks 'storage.objects.create' permissions on this bucket " + self.error = "Azure: Access key invalid or lacking required permissions" return False \ No newline at end of file diff --git a/src/cloud/gcs.py b/src/cloud/gcs.py index 1a98023..1f3e03c 100644 --- a/src/cloud/gcs.py +++ b/src/cloud/gcs.py @@ -35,5 +35,5 @@ class StorageClient: return True except Exception as e: if e.response.status_code == 403: - self.error = "Account lacks 'storage.objects.create' permissions on this bucket " + self.error = "GCS: Forbidden: Account lacks 'storage.objects.create' role on target bucket" return False \ No newline at end of file