From 83a65ab26e6f61d628468dd7e4681b3b3667a195 Mon Sep 17 00:00:00 2001 From: sld-admin Date: Sat, 14 Mar 2026 10:53:52 +0000 Subject: [PATCH] Added Logger + Fixed Code + README and README_IT --- .gitignore | 3 +- README.md | 257 +++++++++++++++++++++- README_IT.md | 258 ++++++++++++++++++++++ __pycache__/constants.cpython-313.pyc | Bin 1394 -> 1591 bytes __pycache__/functions.cpython-313.pyc | Bin 7552 -> 12178 bytes constants.py | 21 +- dir_backups.json | 3 +- functions.py | 304 +++++++++++++++++--------- logger.py | 50 +++++ script.py | 9 +- 10 files changed, 786 insertions(+), 119 deletions(-) create mode 100644 README_IT.md create mode 100644 logger.py diff --git a/.gitignore b/.gitignore index d133e9a..222ac3c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Ignora tutti i backup -backups/ \ No newline at end of file +backups/ +__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index d319979..4c63cc0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,258 @@ # sld-filebackups-py -This is a script that take backups of the folders declared in a json file, and it also running a backup_rotate. \ No newline at end of file +A lightweight, zero-dependency Python backup utility that archives files and folders defined in a JSON list, with automatic rotation of old backups. Designed to run as a daily cron job on Linux servers. + +--- + +## Table of Contents + +- [Features](#features) +- [Project Structure](#project-structure) +- [How It Works](#how-it-works) +- [Installation](#installation) +- [Configuration](#configuration) + - [config.json](#configjson) + - [dir_backups.json](#dir_backupsjson) + - [Environment (init.py)](#environment-initpy) +- [Usage](#usage) +- [Backup Storage Layout](#backup-storage-layout) +- [Backup Rotation](#backup-rotation) +- [Logging](#logging) +- [Running as a Cron Job](#running-as-a-cron-job) +- [Requirements](#requirements) +- [License](#license) + +--- + +## Features + +- **Selective backup** — define which paths to back up in a JSON file, each with its own enable/disable flag; no need to touch the code to add or remove entries +- **Folder backups** — directories are archived as `.tar.gz` (only the folder name is preserved as the archive root, no absolute path leaking) +- **File backups** — single files are compressed as `.gz` +- **Skip-if-exists logic** — if a backup for today already exists, it is skipped automatically, making the script safe to call multiple times per day +- **Auto-rotation** — after each backup run, old archives beyond a configurable retention count are automatically deleted per subfolder +- **Dry-run mode** — preview exactly what rotation would delete, without removing anything +- **Structured logging** — always outputs to console (useful for reading cron output); optionally writes to a persistent log file +- **Multi-environment support** — switch between `local`, `local2`, and `prod` path configurations in a single file +- **Graceful error handling** — malformed JSON entries, missing paths, empty folders, and permission errors are caught and logged without crashing the whole run + +--- + +## Project Structure +``` +backups_script/ +├── script.py # Entry point and CLI argument parser +├── functions.py # Core logic: backup, rotation, checks +├── constants.py # Shared state: paths, loaded config, timestamps +├── logger.py # Logging setup (console + optional file handler) +├── init.py # Environment selector (local / prod) +├── config.json # Runtime configuration +├── dir_backups.json # Declarative list of paths to back up +└── LICENSE # GNU GPL v3 +``` + +### Module Responsibilities + +| File | Role | +|---|---| +| `init.py` | Defines `ROOT_DIR_APP` and `ROOT_DIR_BACKUPS` based on the selected environment. Imported first by everything else. | +| `constants.py` | Builds all derived paths (backup folder, config paths), loads `config.json` and `dir_backups.json` into memory, captures today's date and current time. | +| `logger.py` | Reads `config.json` directly and configures the root Python logger with a `StreamHandler` (always on) and an optional `FileHandler`. | +| `functions.py` | Contains all business logic: `default_backup_dir()`, `check_existing_folders()`, `backups_now()`, `autorotate_backups()`, `show_enabled()`. | +| `script.py` | Bootstraps logging, then parses CLI arguments and calls the appropriate function(s). With no flags, runs a full backup + rotation. | + +--- + +## How It Works + +1. `script.py` calls `setup_logger()`, which reads `config.json` and sets up logging. +2. `default_backup_dir()` ensures the root backup folder and the host-named subfolder exist. +3. `check_existing_folders()` reads `dir_backups.json`, filters for enabled entries (`flag == 1`), verifies each path exists, and classifies it as `"folder"` or `"file"`. Empty or unreadable directories are excluded. +4. `backups_now()` iterates the verified paths: + - For **folders**: creates a `_YYYY-MM-DD.tar.gz` archive using Python's `tarfile` module. + - For **files**: creates a `_YYYY-MM-DD.gz` compressed copy using `gzip` + `shutil.copyfileobj`. + - If the target archive already exists today, the entry is skipped. +5. `autorotate_backups()` scans each immediate subfolder of the host backup directory, sorts `.gz` files by modification time (newest first), and deletes any beyond the `keep_backups` threshold. + +--- + +## Installation + +No packages to install. The script uses Python's standard library only. +```bash +git clone https://gitea.sld-server.org/sld-admin/sld-filebackups-py.git +cd sld-filebackups-py +``` + +Then set your environment and paths in `init.py` and `dir_backups.json`. + +--- + +## Configuration + +### `config.json` +```json +{ + "keep_backups": 7, + "logs": false, + "logs_path": "/home/backups/logs" +} +``` + +| Key | Type | Default | Description | +|---|---|---|---| +| `keep_backups` | integer | `7` | How many recent backup archives to retain per subfolder. Older ones are deleted by the rotation step. | +| `logs` | boolean | `false` | If `true`, a `backup.log` file is written to `logs_path` in addition to console output. | +| `logs_path` | string | `~/backups/logs` | Directory where `backup.log` will be created. Created automatically if it does not exist. | + +> **Note:** Even when `logs` is `false`, all output is still printed to stdout/stderr, which means cron will capture it via mail or redirection as usual. + +--- + +### `dir_backups.json` + +This is the declarative list of everything to back up. Each entry is a JSON array of exactly three values: +```json +[ + [ "/absolute/path/to/folder", 1, "BackupName" ], + [ "/absolute/path/to/file", 1, "ConfigBackup" ], + [ "/path/that/is/disabled", 0, "OldEntry" ] +] +``` + +| Position | Field | Description | +|---|---|---| +| 0 | `path` | Absolute path to the file or folder to back up. | +| 1 | `enabled` | `1` = include in backup runs. `0` = skip entirely (the entry is parsed but never processed). | +| 2 | `name` | A short identifier used as the subfolder name inside the backup destination, and as the prefix of the archive filename. Must be unique across entries. | + +**Tips:** +- To temporarily disable an entry without deleting it, set the flag to `0`. +- The `name` field becomes a directory under `//`, so avoid spaces and special characters. +- Folders are only backed up if they are non-empty and readable. + +--- + +### Environment (`init.py`) +```python +env = "local" # Switch between: "local", "local2", "prod" +``` + +| Environment | `ROOT_DIR_APP` | `ROOT_DIR_BACKUPS` | +|---|---|---| +| `local` | `/home/sld-admin/Scrivania/backups_script/` | `/backups/Daily_File_Backups/` | +| `local2` | `/home/simo-positive/Desktop/backups_script/` | `/backups/Daily_File_Backups/` | +| `prod` | `/opt/sld-backups/` | `/home/backups/backups_root/Daily_File_Backups/` | + +If an unknown value is set, the script exits immediately with an error. + +--- + +## Usage +```bash +# Full backup + auto-rotation (default, no flags needed) +python3 script.py + +# Show which paths are enabled and which are disabled +python3 script.py --show + +# Check whether declared paths exist on disk and print a status report +python3 script.py --check + +# Run backup with verbose debug output +python3 script.py --debug + +# Run only the rotation step (no new backups created) +python3 script.py --rotate + +# Preview what rotation would delete, without actually deleting anything +python3 script.py --rotate --dry +``` + +### CLI Reference + +| Flag | Long form | Description | +|---|---|---| +| `-s` | `--show` | Print enabled and disabled paths from `dir_backups.json`. | +| `-d` | `--debug` | Run backup with `debug="on"`, which enables verbose path-checking output. | +| `-c` | `--check` | Run `check_existing_folders()` and print a detailed status for each declared path. | +| `-r` | `--rotate` | Run `autorotate_backups()` only. Can be combined with `--dry`. | +| | `--dry` | Dry-run mode for `--rotate`: logs candidates for deletion but deletes nothing. | + +--- + +## Backup Storage Layout + +Backups are written under: +``` +/ +└── / + ├── Documents/ + │ ├── Documents_2026-03-10.tar.gz + │ ├── Documents_2026-03-11.tar.gz + │ └── Documents_2026-03-12.tar.gz + └── ConfigBackup/ + ├── ConfigBackup_2026-03-10.gz + └── ConfigBackup_2026-03-11.gz +``` + +- Each entry in `dir_backups.json` gets its own subfolder named after its `name` field. +- Archives are named `_YYYY-MM-DD.tar.gz` (folders) or `_YYYY-MM-DD.gz` (files). +- The host's hostname is used as a top-level grouping folder, which makes it easy to collect backups from multiple machines into the same root. + +--- + +## Backup Rotation + +The rotation step (`autorotate_backups`) runs automatically after every backup, or can be triggered manually with `--rotate`. + +**Logic:** +1. Scans each immediate subfolder of `//`. +2. Finds all `*.gz` files (this covers both `.gz` and `.tar.gz`). +3. Sorts them by modification time, newest first. +4. Keeps the first `keep_backups` (default: 7) and deletes the rest. + +**Dry-run** (`--rotate --dry`) logs exactly which files would be deleted, with no filesystem changes. Useful for verifying the retention setting before applying it. + +--- + +## Logging + +All functions use Python's standard `logging` module via a named logger (`__name__`). The root logger is configured by `logger.py` at startup. + +- **Console output** is always active (via `StreamHandler`), regardless of the `logs` setting. +- **File output** is added when `"logs": true` is set in `config.json`. The log file is `/backup.log` and is appended to on each run. +- Log format: `YYYY-MM-DD HH:MM:SS [LEVEL] message` + +--- + +## Running as a Cron Job + +To run a full backup every day at 2:00 AM: +```bash +crontab -e +``` +``` +0 2 * * * /usr/bin/python3 /opt/sld-backups/script.py >> /home/backups/logs/cron.log 2>&1 +``` + +Since the script always writes to stdout, cron output redirection captures the full run log even if file logging is disabled in `config.json`. + +--- + +## Requirements + +- Python **3.6+** +- **No third-party packages** — uses only the standard library: + - `tarfile`, `gzip`, `shutil` — archiving and compression + - `logging` — structured output + - `argparse` — CLI argument parsing + - `pathlib` — path handling + - `socket` — hostname detection + - `json` — configuration loading + +--- + +## License + +GNU General Public License v3.0 — see [LICENSE](LICENSE) for full terms. \ No newline at end of file diff --git a/README_IT.md b/README_IT.md new file mode 100644 index 0000000..d2c29ef --- /dev/null +++ b/README_IT.md @@ -0,0 +1,258 @@ +# sld-filebackups-py + +Utility di backup in Python, leggera e senza dipendenze esterne, che archivia file e cartelle dichiarati in un file JSON con rotazione automatica dei backup più vecchi. Pensata per girare come cron job giornaliero su server Linux. + +--- + +## Indice + +- [Funzionalità](#funzionalità) +- [Struttura del progetto](#struttura-del-progetto) +- [Come funziona](#come-funziona) +- [Installazione](#installazione) +- [Configurazione](#configurazione) + - [config.json](#configjson) + - [dir_backups.json](#dir_backupsjson) + - [Ambiente (init.py)](#ambiente-initpy) +- [Utilizzo](#utilizzo) +- [Struttura dei backup](#struttura-dei-backup) +- [Rotazione automatica](#rotazione-automatica) +- [Logging](#logging) +- [Esecuzione come cron job](#esecuzione-come-cron-job) +- [Requisiti](#requisiti) +- [Licenza](#licenza) + +--- + +## Funzionalità + +- **Backup selettivo** — definisci i percorsi da salvare in un file JSON, con un flag di abilitazione per ogni voce; non è necessario toccare il codice per aggiungere o rimuovere entry +- **Backup di cartelle** — le directory vengono archiviate come `.tar.gz` (solo il nome della cartella viene preservato come radice dell'archivio, nessun percorso assoluto esposto) +- **Backup di file singoli** — i file vengono compressi come `.gz` +- **Skip automatico se già esiste** — se un backup per la data odierna è già presente, viene saltato; il script può essere lanciato più volte al giorno senza duplicati +- **Rotazione automatica** — dopo ogni esecuzione, gli archivi più vecchi oltre una soglia configurabile vengono eliminati per ogni sottocartella +- **Modalità dry-run** — anteprima precisa di cosa verrebbe eliminato dalla rotazione, senza cancellare nulla +- **Logging strutturato** — output sempre presente su console (utile per leggere l'output di cron); opzionalmente scrive su file di log persistente +- **Supporto multi-ambiente** — switch tra configurazioni `local`, `local2` e `prod` in un unico file +- **Gestione degli errori robusta** — entry JSON malformate, percorsi mancanti, cartelle vuote ed errori di permessi vengono catturati e loggati senza interrompere l'intera esecuzione + +--- + +## Struttura del progetto +``` +backups_script/ +├── script.py # Punto di ingresso e parser degli argomenti CLI +├── functions.py # Logica principale: backup, rotazione, controlli +├── constants.py # Stato condiviso: percorsi, config caricata, timestamp +├── logger.py # Setup del logging (console + file handler opzionale) +├── init.py # Selettore di ambiente (local / prod) +├── config.json # Configurazione runtime +├── dir_backups.json # Lista dichiarativa dei percorsi da salvare +└── LICENSE # GNU GPL v3 +``` + +### Responsabilità dei moduli + +| File | Ruolo | +|---|---| +| `init.py` | Definisce `ROOT_DIR_APP` e `ROOT_DIR_BACKUPS` in base all'ambiente selezionato. Importato per primo da tutto il resto. | +| `constants.py` | Costruisce tutti i percorsi derivati (cartella backup, percorsi config), carica `config.json` e `dir_backups.json` in memoria, cattura la data odierna e l'ora corrente. | +| `logger.py` | Legge `config.json` direttamente e configura il logger root di Python con uno `StreamHandler` (sempre attivo) e un `FileHandler` opzionale. | +| `functions.py` | Contiene tutta la logica di business: `default_backup_dir()`, `check_existing_folders()`, `backups_now()`, `autorotate_backups()`, `show_enabled()`. | +| `script.py` | Inizializza il logging, poi analizza gli argomenti CLI e chiama la funzione appropriata. Senza flag, esegue backup completo + rotazione. | + +--- + +## Come funziona + +1. `script.py` chiama `setup_logger()`, che legge `config.json` e configura il logging. +2. `default_backup_dir()` verifica che la cartella di backup root e la sottocartella con il nome host esistano, creandole se necessario. +3. `check_existing_folders()` legge `dir_backups.json`, filtra le entry abilitate (`flag == 1`), verifica che ogni percorso esista su disco e lo classifica come `"folder"` o `"file"`. Le directory vuote o non leggibili vengono escluse. +4. `backups_now()` itera i percorsi verificati: + - Per le **cartelle**: crea un archivio `_YYYY-MM-DD.tar.gz` tramite il modulo `tarfile`. + - Per i **file singoli**: crea una copia compressa `_YYYY-MM-DD.gz` tramite `gzip` + `shutil.copyfileobj`. + - Se l'archivio di oggi esiste già, l'entry viene saltata. +5. `autorotate_backups()` scansiona ogni sottocartella diretta della directory di backup dell'host, ordina i file `.gz` per data di modifica (più recenti prima) ed elimina quelli oltre la soglia `keep_backups`. + +--- + +## Installazione + +Nessun pacchetto da installare. Il script usa solo la libreria standard di Python. +```bash +git clone https://gitea.sld-server.org/sld-admin/sld-filebackups-py.git +cd sld-filebackups-py +``` + +Poi imposta il tuo ambiente e i percorsi in `init.py` e `dir_backups.json`. + +--- + +## Configurazione + +### `config.json` +```json +{ + "keep_backups": 7, + "logs": false, + "logs_path": "/home/backups/logs" +} +``` + +| Chiave | Tipo | Default | Descrizione | +|---|---|---|---| +| `keep_backups` | intero | `7` | Quanti archivi recenti conservare per sottocartella. I più vecchi vengono eliminati dalla rotazione. | +| `logs` | booleano | `false` | Se `true`, viene scritto un file `backup.log` in `logs_path` oltre all'output su console. | +| `logs_path` | stringa | `~/backups/logs` | Cartella dove verrà creato `backup.log`. Viene creata automaticamente se non esiste. | + +> **Nota:** Anche quando `logs` è `false`, tutto l'output viene comunque stampato su stdout/stderr, quindi cron lo cattura normalmente tramite mail o redirezione. + +--- + +### `dir_backups.json` + +È la lista dichiarativa di tutto ciò che deve essere salvato. Ogni entry è un array JSON di esattamente tre valori: +```json +[ + [ "/percorso/assoluto/cartella", 1, "NomeBackup" ], + [ "/percorso/assoluto/file", 1, "BackupConfig" ], + [ "/percorso/disabilitato", 0, "VecchiaEntry" ] +] +``` + +| Posizione | Campo | Descrizione | +|---|---|---| +| 0 | `percorso` | Percorso assoluto del file o della cartella da salvare. | +| 1 | `abilitato` | `1` = includi nelle esecuzioni di backup. `0` = salta completamente (la entry viene letta ma mai elaborata). | +| 2 | `nome` | Identificativo breve usato come nome della sottocartella nella destinazione del backup e come prefisso del nome dell'archivio. Deve essere unico tra le entry. | + +**Suggerimenti:** +- Per disabilitare temporaneamente una entry senza eliminarla, imposta il flag a `0`. +- Il campo `nome` diventa una directory dentro `//`, quindi evita spazi e caratteri speciali. +- Le cartelle vengono salvate solo se non sono vuote e sono leggibili. + +--- + +### Ambiente (`init.py`) +```python +env = "local" # Valori disponibili: "local", "local2", "prod" +``` + +| Ambiente | `ROOT_DIR_APP` | `ROOT_DIR_BACKUPS` | +|---|---|---| +| `local` | `/home/sld-admin/Scrivania/backups_script/` | `/backups/Daily_File_Backups/` | +| `local2` | `/home/simo-positive/Desktop/backups_script/` | `/backups/Daily_File_Backups/` | +| `prod` | `/opt/sld-backups/` | `/home/backups/backups_root/Daily_File_Backups/` | + +Se viene impostato un valore sconosciuto, il script termina immediatamente con un errore. + +--- + +## Utilizzo +```bash +# Backup completo + rotazione automatica (comportamento di default, nessun flag richiesto) +python3 script.py + +# Mostra quali percorsi sono abilitati e quali disabilitati +python3 script.py --show + +# Verifica se i percorsi dichiarati esistono su disco e stampa un report +python3 script.py --check + +# Esegui il backup con output di debug verboso +python3 script.py --debug + +# Esegui solo la rotazione (nessun nuovo backup creato) +python3 script.py --rotate + +# Anteprima di cosa verrebbe eliminato dalla rotazione, senza cancellare nulla +python3 script.py --rotate --dry +``` + +### Riferimento flag CLI + +| Flag | Forma lunga | Descrizione | +|---|---|---| +| `-s` | `--show` | Stampa i percorsi abilitati e disabilitati da `dir_backups.json`. | +| `-d` | `--debug` | Esegue il backup con `debug="on"`, abilitando output verboso sul controllo dei percorsi. | +| `-c` | `--check` | Esegue `check_existing_folders()` e stampa lo stato dettagliato per ogni percorso dichiarato. | +| `-r` | `--rotate` | Esegue solo `autorotate_backups()`. Può essere combinato con `--dry`. | +| | `--dry` | Modalità dry-run per `--rotate`: logga i candidati all'eliminazione ma non cancella nulla. | + +--- + +## Struttura dei backup + +I backup vengono scritti sotto: +``` +/ +└── / + ├── Documenti/ + │ ├── Documenti_2026-03-10.tar.gz + │ ├── Documenti_2026-03-11.tar.gz + │ └── Documenti_2026-03-12.tar.gz + └── BackupConfig/ + ├── BackupConfig_2026-03-10.gz + └── BackupConfig_2026-03-11.gz +``` + +- Ogni entry in `dir_backups.json` ottiene la propria sottocartella con il nome del campo `nome`. +- Gli archivi seguono il pattern `_YYYY-MM-DD.tar.gz` (cartelle) o `_YYYY-MM-DD.gz` (file). +- Il nome host della macchina viene usato come cartella di primo livello, rendendo semplice raccogliere backup di più macchine sotto la stessa root. + +--- + +## Rotazione automatica + +La rotazione (`autorotate_backups`) viene eseguita automaticamente dopo ogni backup, oppure può essere avviata manualmente con `--rotate`. + +**Logica:** +1. Scansiona ogni sottocartella diretta di `//`. +2. Trova tutti i file `*.gz` (copre sia `.gz` che `.tar.gz`). +3. Li ordina per data di modifica, dal più recente al più vecchio. +4. Conserva i primi `keep_backups` (default: 7) ed elimina i restanti. + +**Dry-run** (`--rotate --dry`) logga esattamente quali file verrebbero eliminati, senza nessuna modifica al filesystem. Utile per verificare l'impostazione di retention prima di applicarla. + +--- + +## Logging + +Tutte le funzioni usano il modulo standard `logging` di Python tramite un logger con nome (`__name__`). Il logger root viene configurato da `logger.py` all'avvio. + +- **Output su console** sempre attivo (via `StreamHandler`), indipendentemente dall'impostazione `logs`. +- **Output su file** aggiunto quando `"logs": true` è impostato in `config.json`. Il file di log è `/backup.log` e viene aggiunto ad ogni esecuzione. +- Formato log: `YYYY-MM-DD HH:MM:SS [LIVELLO] messaggio` + +--- + +## Esecuzione come cron job + +Per eseguire un backup completo ogni giorno alle 2:00: +```bash +crontab -e +``` +``` +0 2 * * * /usr/bin/python3 /opt/sld-backups/script.py >> /home/backups/logs/cron.log 2>&1 +``` + +Poiché il script scrive sempre su stdout, la redirezione dell'output di cron cattura il log completo dell'esecuzione anche se il log su file è disabilitato in `config.json`. + +--- + +## Requisiti + +- Python **3.6+** +- **Nessun pacchetto di terze parti** — usa solo la libreria standard: + - `tarfile`, `gzip`, `shutil` — archiviazione e compressione + - `logging` — output strutturato + - `argparse` — parsing degli argomenti CLI + - `pathlib` — gestione dei percorsi + - `socket` — rilevamento del nome host + - `json` — caricamento della configurazione + +--- + +## Licenza + +GNU General Public License v3.0 — vedi [LICENSE](LICENSE) per i termini completi. \ No newline at end of file diff --git a/__pycache__/constants.cpython-313.pyc b/__pycache__/constants.cpython-313.pyc index bc3bbfe9005f8bb44e21750becfbedbd89662544..a0c6824b297b6a56a19314145be168549f4794f2 100644 GIT binary patch delta 915 zcmb7>!E4h{9LIkzFKzPDrq-sN?b@`_IcsD?593T$<@YjAEO>%oIi z@U)xE9PG{g4TAUwv;~jCHkd7<2QS`+I}{v?N*6R&is}2N=S*|%;ha}T z1vGBuQS7GaI|JjK?i(`vI%_>Zakm-Jap4MO$u;%#x*C^4gJnY+ua8{WRgw4o7O zdwyud7i{0W5C=U>XFQt6d6)q+qBDh=KJ{;1roo5TKHg%eOl7eSA4P;>z(a2?EO0`hmtV7*|Jg;nNs+_6)nZwM;i{oF|znXpBplC~i#i z_I;LQJvOL#fF-^Ako_hG~7;c3S F)j#|zp&S4J delta 668 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 literal 7552 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 diff --git a/constants.py b/constants.py index 301b04f..ef97beb 100644 --- a/constants.py +++ b/constants.py @@ -1,9 +1,10 @@ -# constants.py +# constants.py (modificato) from init import * import socket import json from datetime import date, datetime import os +from pathlib import Path LISTFILE = os.path.join(ROOT_DIR_APP, "dir_backups.json") CONF_FILE = os.path.join(ROOT_DIR_APP, "config.json") @@ -13,11 +14,17 @@ DATETODAY = date.today() TIMENOW = datetime.now().strftime("%H:%M:%S") TIMEDATA = '[ ' + str(DATETODAY) + ' - ' + str(TIMENOW) + ' ]' -with open(LISTFILE, "r") as listfile: - JSON_LIST = json.load(listfile) +# safer load +try: + with open(LISTFILE, "r") as listfile: + JSON_LIST = json.load(listfile) +except Exception: + JSON_LIST = [] -with open(CONF_FILE, "r") as conf_file: - JSON_CONF = json.load(conf_file) - -print(TIMEDATA) +try: + with open(CONF_FILE, "r") as conf_file: + JSON_CONF = json.load(conf_file) +except Exception: + JSON_CONF = {} +# removed print(TIMEDATA) at module import time \ No newline at end of file diff --git a/dir_backups.json b/dir_backups.json index d4503ae..8b72802 100644 --- a/dir_backups.json +++ b/dir_backups.json @@ -1,4 +1,5 @@ [ [ "/folder/to/backups", 1, "BackupName" ], - [ "/file/to/backups/disabled/by/the/0/flag/next/to/this", 0, "BackupName2" ] + [ "/file/to/backups/disabled/by/the/0/flag/next/to/this", 0, "BackupName2" ], + [ "/home/sld-admin/Documents", 1, "Doucuments" ] ] diff --git a/functions.py b/functions.py index 58f482e..26bee59 100644 --- a/functions.py +++ b/functions.py @@ -1,167 +1,257 @@ -from constants import * +# functions.py from pathlib import Path -import os, gzip, tarfile, shutil - -## Create the backup default folders -def default_backup_dir(): - os.makedirs(HOST_BACKUP_FOLDER, exist_ok=True) - -from pathlib import Path -from constants import * +import logging import os +import gzip +import tarfile +import shutil +from typing import List, Tuple -def autorotate_backups(dry_run: bool = False): - """ - Scansiona tutte le sottocartelle immediate di HOST_BACKUP_FOLDER. - Per ogni sottocartella prende i file *.gz (inclusi .tar.gz), li ordina - per mtime (più nuovi prima), mantiene i primi `keep_backups` e rimuove - gli altri (a meno che dry_run==True). - Restituisce (candidates_found, actually_deleted). - """ +# Import Costants: (ROOT_DIR, JSON_LIST, JSON_CONF, HOST_BACKUP_FOLDER, DATETODAY, ...) +from constants import * +_LOG = logging.getLogger(__name__) + +def default_backup_dir() -> None: + """ + Ensure the host backup folder exists. + """ + try: + Path(HOST_BACKUP_FOLDER).mkdir(parents=True, exist_ok=True) + _LOG.info("Backup base directory ensured: %s", HOST_BACKUP_FOLDER) + except Exception: + _LOG.exception("Failed to create HOST_BACKUP_FOLDER: %s", HOST_BACKUP_FOLDER) + + +## Backup files rotation +def autorotate_backups(dry_run: bool = False) -> Tuple[int, int]: + """ + Rotate backup files in each immediate subfolder of HOST_BACKUP_FOLDER. + + Behavior: + - For each immediate subfolder, find files matching *.gz (this includes .tar.gz), + sort them by modification time (newest first), keep the first `keep_backups` + and delete the older ones. + - If dry_run is True, only log what would be deleted. + + Returns: + (candidates_found, actually_deleted) + """ base = Path(HOST_BACKUP_FOLDER) if not base.exists(): - print("ERROR: HOST_BACKUP_FOLDER does not exist:", base) + _LOG.error("HOST_BACKUP_FOLDER does not exist: %s", base) return 0, 0 - keep = int(JSON_CONF.get("keep_backups", 7)) + try: + keep = int(JSON_CONF.get("keep_backups", 7)) + except Exception: + keep = 7 + _LOG.warning("Invalid keep_backups value in config, falling back to %d", keep) total_candidates = 0 total_deleted = 0 - # ottengo tutte le directory immediate dentro HOST_BACKUP_FOLDER + # immediate subdirectories targets = sorted([p for p in base.iterdir() if p.is_dir()]) if not targets: - print("No subfolders found in HOST_BACKUP_FOLDER:", base) + _LOG.info("No subfolders found in HOST_BACKUP_FOLDER: %s", base) return 0, 0 for folder in targets: - # prendi solo file (evita di includere directory per errore) - backups = sorted( - (f for f in folder.glob("*.gz") if f.is_file()), - key=lambda f: f.stat().st_mtime, - reverse=True - ) + try: + backups = sorted( + (f for f in folder.glob("*.gz") if f.is_file()), + key=lambda f: f.stat().st_mtime, + reverse=True + ) + except Exception: + _LOG.exception("Failed to list backups in folder: %s", folder) + continue old_backups = backups[keep:] - print("\nFolder:", folder) - print("Total backups:", len(backups)) - print("Keep:", keep) - print("Old to remove:", len(old_backups)) + _LOG.info("Folder: %s", folder) + _LOG.info(" Total backups found: %d", len(backups)) + _LOG.info(" Keep: %d", keep) + _LOG.info(" Old backups to remove: %d", len(old_backups)) for b in old_backups: - print(" Old backup:", b) + _LOG.info(" Candidate for removal: %s", b) - # elimina se non dry_run if not dry_run and old_backups: for b in old_backups: try: b.unlink() total_deleted += 1 - print(" -> deleted") - except Exception as e: - print(f" -> failed to delete {b}: {e}") + _LOG.info(" -> deleted: %s", b) + except Exception: + _LOG.exception(" -> failed to delete: %s", b) total_candidates += len(old_backups) - print("\nSummary:") - print(f" Candidates found: {total_candidates}") - print(f" Actually deleted: {total_deleted} (dry_run={dry_run})") + _LOG.info("Rotation summary: candidates_found=%d, actually_deleted=%d (dry_run=%s)", + total_candidates, total_deleted, dry_run) return total_candidates, total_deleted - - -## Show what backups path are enabled or disabled -def show_enabled(): - print() - print("### ENABLED PATHS ###") - for path, flag, name in JSON_LIST: - if flag > 0: - print(f"- {path}") - print ("") - print("### DISABLED PATHS ###") - for path, flag, name in JSON_LIST: - if flag == 0: - print(f"- {path}") - -## Checking which of the enabled path are available for a backup -def check_existing_folders(debug="off"): - checked_paths = [] - correct_folder = [] - correct_file = [] - notexists = [] - empty = [] - - - - for path, flag, namepath in JSON_LIST: - if flag != 1: +## Show what is enabled in the file json +def show_enabled() -> None: + """ + Log enabled and disabled paths defined in JSON_LIST. + """ + _LOG.info("### ENABLED PATHS ###") + for entry in JSON_LIST: + try: + path, flag, name = entry + except Exception: + _LOG.warning("Malformed entry in dir_backups.json: %s", entry) continue - pathnow = Path(path) + if flag and int(flag) > 0: + _LOG.info("- %s (name: %s)", path, name) + print("") + _LOG.info("### DISABLED PATHS ###") + for entry in JSON_LIST: + try: + path, flag, name = entry + except Exception: + continue + if int(flag) == 0: + _LOG.info("- %s (name: %s)", path, name) + + +## Check if the declared folder exists +def check_existing_folders(debug: str = "off") -> List[Tuple[Path, str, str]]: + """ + Check which enabled paths exist and classify them as 'folder' or 'file'. + + Returns a list of tuples: (Path(path), name, "folder"|"file") + + If a path is a directory, it is considered valid only if it contains at least one entry. + """ + checked_paths: List[Tuple[Path, str, str]] = [] + correct_folder: List[str] = [] + correct_file: List[str] = [] + notexists: List[str] = [] + empty: List[str] = [] + + for entry in JSON_LIST: + try: + path_str, flag, namepath = entry + except Exception: + _LOG.warning("Skipping malformed entry: %s", entry) + continue + + try: + if int(flag) != 1: + continue + except Exception: + _LOG.warning("Invalid flag for entry %s, skipping", entry) + continue + + pathnow = Path(path_str) + if pathnow.exists(): - if pathnow.is_dir() and any(pathnow.iterdir()): - checked_paths.append([pathnow, namepath, "folder"]) - correct_folder.append(f"- Folder exists: {pathnow}") - elif pathnow.is_file(): - checked_paths.append([pathnow, namepath, "file"]) - correct_file.append(f"- File exists: {pathnow}") - else: - empty.append(f"- Empty folder or special file: {pathnow}") + try: + if pathnow.is_dir(): + try: + # consider non-empty directory only + if any(pathnow.iterdir()): + checked_paths.append((pathnow, namepath, "folder")) + correct_folder.append(f"- Folder exists: {pathnow}") + else: + empty.append(f"- Empty folder: {pathnow}") + except PermissionError: + _LOG.warning("Permission denied reading directory: %s", pathnow) + empty.append(f"- Unreadable/empty folder: {pathnow}") + elif pathnow.is_file(): + checked_paths.append((pathnow, namepath, "file")) + correct_file.append(f"- File exists: {pathnow}") + else: + empty.append(f"- Special file / unknown type: {pathnow}") + except Exception: + _LOG.exception("Error while checking path: %s", pathnow) else: notexists.append(f"- Path does not exist: {pathnow}") - if debug=="on": - print("###### CHECKING EXISTING FOLDERS/FILES ######") - print() - print(f"# FOLDERS CHECK OK - [ {len(correct_folder)} ] #") + if debug == "on": + _LOG.debug("###### CHECKING EXISTING FOLDERS/FILES ######") + _LOG.debug("# FOLDERS CHECK OK - [ %d ]", len(correct_folder)) for folder in correct_folder: - print(folder) - print("") - - print(f"# FILES CHECK OK - [ {len(correct_file)} ] #") + _LOG.debug(folder) + _LOG.debug("# FILES CHECK OK - [ %d ]", len(correct_file)) for file in correct_file: - print(file) - print("") - - print(f"# FOLDERS EMPTY - [ {len(empty)} ] #") + _LOG.debug(file) + _LOG.debug("# FOLDERS EMPTY - [ %d ]", len(empty)) for emptyfold in empty: - print(emptyfold) - print("") - - print(f"# FILES / FOLDERS NOT EXISTS - [ {len(notexists)} ] #") + _LOG.debug(emptyfold) + _LOG.debug("# FILES / FOLDERS NOT EXISTS - [ %d ]", len(notexists)) for not_exists in notexists: - print(not_exists) - print("") + _LOG.debug(not_exists) return checked_paths -## Function available for the backup -def backups_now(debug="off"): - listnow = check_existing_folders() + +## Backups action +def backups_now(debug: str = "off") -> None: + """ + Perform backups for each valid path discovered by check_existing_folders. + + - Directories are archived as tar.gz + - Single files are compressed as .gz + + If debug == "on", additional logging is emitted. + """ + listnow = check_existing_folders(debug=debug) base_backup = Path(HOST_BACKUP_FOLDER) - base_backup.mkdir(parents=True, exist_ok=True) + try: + base_backup.mkdir(parents=True, exist_ok=True) + except Exception: + _LOG.exception("Failed to ensure base backup directory: %s", base_backup) + return + + date_str = str(DATETODAY) # DATETODAY is provided by constants.py (date object) for path, name, backtype in listnow: pathbackup = base_backup / name - pathbackup.mkdir(parents=True, exist_ok=True) + try: + pathbackup.mkdir(parents=True, exist_ok=True) + except Exception: + _LOG.exception("Failed to create backup subfolder: %s", pathbackup) + continue if backtype == "folder": - tar_path = pathbackup / f"{name}_{DATETODAY}.tar.gz" - if not tar_path.exists(): - if debug=="on": - print(f"Backing up folder: {path}") + tar_filename = f"{name}_{date_str}.tar.gz" + tar_path = pathbackup / tar_filename + if tar_path.exists(): + _LOG.info("Folder backup already exists, skipping: %s", tar_path) + continue + + _LOG.info("Backing up folder: %s -> %s", path, tar_path) + try: + # create a tar.gz archive; arcname preserves only the folder name with tarfile.open(tar_path, "w:gz") as tar: tar.add(path, arcname=path.name) + _LOG.info("Successfully created archive: %s", tar_path) + except Exception: + _LOG.exception("Failed to create tar.gz for folder: %s", path) elif backtype == "file": - gz_path = pathbackup / f"{name}_{DATETODAY}.gz" - if not gz_path.exists(): - if debug=="on": - print(f"Backing up file: {path}") + gz_filename = f"{name}_{date_str}.gz" + gz_path = pathbackup / gz_filename + if gz_path.exists(): + _LOG.info("File backup already exists, skipping: %s", gz_path) + continue + + _LOG.info("Backing up file: %s -> %s", path, gz_path) + try: + # open source file and compress into gzip file with open(path, "rb") as f_in, gzip.open(gz_path, "wb") as f_out: - shutil.copyfileobj(f_in, f_out) \ No newline at end of file + shutil.copyfileobj(f_in, f_out) + _LOG.info("Successfully created gzip: %s", gz_path) + except Exception: + _LOG.exception("Failed to create gzip for file: %s", path) + else: + _LOG.warning("Unknown backtype '%s' for path: %s", backtype, path) \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..0c47170 --- /dev/null +++ b/logger.py @@ -0,0 +1,50 @@ +# logger.py +import logging +import json +from pathlib import Path +import os + +def _load_config(): + cfg_path = Path(__file__).parent / "config.json" + if cfg_path.exists(): + try: + return json.loads(cfg_path.read_text()) + except Exception: + return {} + return {} + +def setup_logger(): + cfg = _load_config() + logs_enabled = bool(cfg.get("logs", False)) + logs_path = cfg.get("logs_path", None) + + root_logger = logging.getLogger() + # reset handlers (utile se viene richiamato più volte) + root_logger.handlers = [] + + formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") + + # always add a stream handler so messages appear on console (useful for cron output) + sh = logging.StreamHandler() + sh.setFormatter(formatter) + sh.setLevel(logging.INFO) + root_logger.addHandler(sh) + + if logs_enabled: + if logs_path is None: + # default fallback + logs_path = str(Path.home() / "backups" / "logs") + log_dir = Path(logs_path) + log_dir.mkdir(parents=True, exist_ok=True) + fh = logging.FileHandler(log_dir / "backup.log") + fh.setFormatter(formatter) + fh.setLevel(logging.INFO) + root_logger.addHandler(fh) + + root_logger.setLevel(logging.INFO) + + # informative note when file logging is disabled + if not logs_enabled: + root_logger.info("File logging disabled (config.json: logs=false)") + else: + root_logger.info(f"File logging enabled, log file: {logs_path}/backup.log") \ No newline at end of file diff --git a/script.py b/script.py index 09866ca..3b6542e 100755 --- a/script.py +++ b/script.py @@ -1,5 +1,12 @@ #!/bin/python3 import argparse + +# configure logging first (logger reads config.json located next to this file) +from logger import setup_logger +setup_logger() + +import logging +# ora importiamo le funzioni (che useranno logging invece di print) from functions import * default_backup_dir() @@ -18,9 +25,7 @@ elif args.debug: backups_now(debug="on") elif args.check: checked = check_existing_folders(debug="on") - #print(checked) elif args.rotate: - # passa il flag dry al chiamante; default è delete se non specifichi --dry autorotate_backups(dry_run=args.dry) else: backups_now()