{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "source": "Il progetto SyncEase: sincronizzare una cartella sorgente e una di destinazione usando il multithreading e il multiprocessing.\nIl progetto è stato diviso in step in modo da renderlo leggibile e comprensibile.\n\n* 1 step: Scansione di sorgente e destinazione. La funzione \"scansiona_cartella\" restituisce un dizionario {percorso_relativo: (dimensione, mtime)} per ogni file all'interno della directory.\n* 2 step: Confronto dei file. La funzione \"confronta_cartelle\" restituisce tre liste all'interno di un dizionario {\"nuovi\": nuovi, \"modificati\": modificati, \"obsoleti\": obsoleti}.\n* 3 step: Creazione lista di operazioni. La funzione \"crea_lista_operazioni\" prende in ingresso il dizionario e unifica in una lista i file da copiare (nuovi e modificati) e quelli da cancellare (obsoleti).\n* 4 step: Creazione della lista di sottogruppi. La funzione \"dividi_in_sottogruppi\" prende in ingresso una lista e il numero di sottogruppi in cui dividerla, e restituisce sempre altrettante sottoliste equilibrate, distribuendo gli elementi a passo costante (slicing lista[i::n]).\n* 5 step: Esecuzione di una singola operazione. Le funzioni \"copy_file\" e \"delete_file\" eseguono rispettivamente una copia e una cancellazione di un singolo file, restituendo un esito (successo/errore). La funzione \"esegui_operazione\" riceve una singola tupla (tipo, percorso_relativo), ricostruisce i percorsi completi sorgente e destinazione a partire dalle cartelle radice, e chiama la funzione corretta in base al tipo.\n* 6 step: Esecuzione parallela di un sottogruppo (multithreading). La funzione \"processa_sottogruppo\" riceve un sottogruppo di operazioni e, tramite un pool di thread (ThreadPoolExecutor), esegue in parallelo \"esegui_operazione\" su ciascuna di esse, restituendo la lista degli esiti — questa è la funzione che ogni processo del pool di multiprocessing eseguirà sul proprio sottogruppo.\n* 7 step: Presentazione dei risultati. La funzione \"riepiloga_risultati\" resta una funzione pura che smista i risultati e restituisce sempre un riepilogo, senza stampare nulla; la stampa a video è demandata alla funzione separata \"stampa_riepilogo\". La funzione \"stampa_dry_run\" mostra invece le operazioni pianificate quando \"sincronizza\" viene invocata con dry_run=True.\n* 8 step: Pulizia della destinazione. La funzione \"rimuovi_cartelle_vuote\" elimina le sottocartelle rimaste vuote in destinazione dopo la rimozione dei file obsoleti.\n* 9 step: Orchestrazione con multiprocessing. La funzione \"sincronizza\" collega tutti gli step precedenti: crea la destinazione se non esiste, scansiona sorgente e destinazione, le confronta, costruisce la lista di operazioni, la divide in sottogruppi e distribuisce ogni sottogruppo a un processo di un pool di multiprocessing (ProcessPoolExecutor), dove ciascuno esegue \"processa_sottogruppo\". Al termine, raccoglie ed appiattisce i risultati di tutti i processi, ripulisce le cartelle vuote e restituisce sempre il riepilogo finale.\n* 10 step: Uso da riga di comando. La funzione \"parse_argomenti\" permette di lanciare il programma su cartelle reali (ad esempio il backup di un server locale) passando sorgente, destinazione, numero di processi/thread e l'opzione --dry-run, senza dover modificare il codice.",
   "metadata": {
    "id": "_UuV7nKOlbGV"
   }
  },
  {
   "cell_type": "markdown",
   "source": [
    "# IMPORT E COSTANTI"
   ],
   "metadata": {
    "id": "X1SBY_oIlvSp"
   }
  },
  {
   "cell_type": "code",
   "source": "import argparse\nfrom concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor\nimport os\nfrom pathlib import Path\nimport shutil\nimport sys\nimport tempfile\nimport time\n\nTHREAD = 4\nPROCESSI = os.cpu_count()",
   "metadata": {
    "id": "Pg3F3t5Xlr5M"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"scansiona_cartella\" prende un percorso e controlla per prima cosa se non è una stringa vuota.\n",
    "Trasforma il percorso in un oggetto Path, successivamente controlla se esiste ed è una directory in caso di eccezione\n",
    "viene lanciata un eccezione opportunamente. Superati i controlli viene scansiona la cartella e sottocartelle ricorsivamente tramite \"os.walk\"\n",
    "in modo da estrarre dai metadati dimensione e tempo di modifica. La funzione restituisce un dizionario formato: {percorso_relativo: (dimensione, mtime)}.\n",
    "In caso di errore durante la  scansione viene sollevata un'eccezione."
   ],
   "metadata": {
    "id": "LQZ5EUP4l3DF"
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def scansiona_cartella(percorso):\n",
    "    \"\"\"\n",
    "    Scansiona ricorsivamente una cartella e restituisce un dizionario\n",
    "    {percorso_relativo: (dimensione, mtime)} per ogni file trovato\n",
    "    \"\"\"\n",
    "    if not percorso:\n",
    "        raise ValueError(\"Percorso non specificato\")\n",
    "\n",
    "    folder = Path(percorso)\n",
    "    files = {}\n",
    "\n",
    "    if not folder.exists():\n",
    "        raise FileNotFoundError(f\"La cartella '{folder}' non esiste\")\n",
    "    if not folder.is_dir():\n",
    "        raise NotADirectoryError(f\"'{folder}' non è una cartella, ma un file\")\n",
    "    try:\n",
    "        for root, dirs, filenames in os.walk(folder):\n",
    "            for filename in filenames:\n",
    "                full_path = Path(root)/filename\n",
    "                relative_path = full_path.relative_to(folder)\n",
    "                filename_stat = full_path.stat()\n",
    "                files[str(relative_path)] = (filename_stat.st_size, filename_stat.st_mtime)\n",
    "    except Exception as exc:\n",
    "        print(f\"Errore durante la scansione della cartella '{folder}': {exc}\")\n",
    "        raise\n",
    "\n",
    "    return files"
   ],
   "metadata": {
    "id": "HltVCq_Bl6Es"
   },
   "execution_count": 15,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"confronta_cartelle\" prende in ingresso il dizionario estratto dalla funzione \"scansiona_cartella\" sia per la sorgente\n",
    "che per la destione.\n",
    "- Se il file è presente nella sorgente e non nella destinazione allora è nuovo\n",
    "- Se il file è presente nella destinazione e non nella sorgente allora è obsoleto\n",
    "- Se la dimensione del file della sorgente è diverso da quello della destinazione oppure il tempo della sorgente è maggiore della destinazione allora il file è stato modificato"
   ],
   "metadata": {
    "id": "SnxZmUDjmCvk"
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def confronta_cartelle(source_files, dest_files):\n",
    "    \"\"\"\n",
    "    Confronta due dizionari {percorso_relativo: (dimensione, mtime)}\n",
    "    prodotti da scansiona_cartella, e restituisce un dizionario con\n",
    "    tre liste di percorsi relativi: nuovi, modificati, obsoleti.\n",
    "    \"\"\"\n",
    "    source_keys = set(source_files.keys())\n",
    "    dest_keys = set(dest_files.keys())\n",
    "\n",
    "    nuovi = list(source_keys - dest_keys)      # in sorgente, non in destinazione\n",
    "    obsoleti = list(dest_keys - source_keys)   # in destinazione, non in sorgente\n",
    "\n",
    "    modificati = []\n",
    "    comuni = source_keys & dest_keys           # presenti in entrambe\n",
    "    for percorso in comuni:\n",
    "        dimensione_src, mtime_src = source_files[percorso]\n",
    "        dimensione_dst, mtime_dst = dest_files[percorso]\n",
    "        if dimensione_src != dimensione_dst or mtime_src > mtime_dst:\n",
    "            modificati.append(percorso)\n",
    "\n",
    "    return {\"nuovi\": nuovi, \"modificati\": modificati, \"obsoleti\": obsoleti}"
   ],
   "metadata": {
    "id": "xJr3A347mFPR"
   },
   "execution_count": 16,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"crea_lista_operazioni\" ha il compito di trasformare il dizionario con le liste \"nuovi\", \"modificati\" e \"obsoleti\"\n",
    "in un unica lista piatta di operazioni da dividere in sottogruppi \"copia\" che comprende sia i file nuovi e modificati e \"obsoleti\" che sono\n",
    "i file che devono essere cancellati"
   ],
   "metadata": {
    "id": "2ll0S9OWmIp4"
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def crea_lista_operazioni(risultato_confronto):\n",
    "    \"\"\"\n",
    "    Trasforma il risultato di confronta_cartelle in un'unica lista piatta\n",
    "    di operazioni, ciascuna rappresentata come tupla (tipo, percorso).\n",
    "    \"\"\"\n",
    "    copia = risultato_confronto[\"nuovi\"] + risultato_confronto[\"modificati\"]\n",
    "\n",
    "    operazioni_copia = [(\"copia\", percorso) for percorso in copia]\n",
    "    operazioni_elimina = [(\"elimina\", percorso) for percorso in risultato_confronto[\"obsoleti\"]]\n",
    "\n",
    "    return operazioni_copia + operazioni_elimina"
   ],
   "metadata": {
    "id": "yJVStqt8mLdc"
   },
   "execution_count": 17,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"dividi_in_sottogruppi\" divide la lista di dimensione n"
   ],
   "metadata": {
    "id": "Nt7tJ5vdmMqg"
   }
  },
  {
   "cell_type": "code",
   "source": "def dividi_in_sottogruppi(lista, n):\n    \"\"\"\n    Divide una lista in n sottoliste, distribuendo gli elementi a passo\n    costante (slicing lista[i::n]) in modo da ottenere sempre n gruppi\n    il più possibile equilibrati.\n    \"\"\"\n    if not lista or n <= 0:\n        return []\n\n    return [lista[i::n] for i in range(n)]",
   "metadata": {
    "id": "j3DBI2YfmPbL"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"copy_file\" copia i file da una sorgente a una destinazione"
   ],
   "metadata": {
    "id": "EFwRXeVimSW1"
   }
  },
  {
   "cell_type": "code",
   "source": "def copy_file(source_path, destination_path):\n    \"\"\"\n    Copia un singolo file dalla sorgente alla destinazione mantenendo i metadati.\n    \"\"\"\n    try:\n        os.makedirs(os.path.dirname(destination_path), exist_ok=True)  # crea le sottocartelle mancanti\n        shutil.copy2(source_path, destination_path)  # copia contenuto + metadata (mtime compreso)\n        return {\n            \"tipo\": \"copia\",\n            \"successo\": True,\n            \"sorgente\": str(source_path),\n            \"destinazione\": str(destination_path),\n            \"errore\": None\n        }\n    except Exception as exc:\n        return {\n            \"tipo\": \"copia\",\n            \"successo\": False,\n            \"sorgente\": str(source_path),\n            \"destinazione\": str(destination_path),\n            \"errore\": str(exc)\n        }",
   "metadata": {
    "id": "UBKM3wlImVkf"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"delete_file\" cancella un file"
   ],
   "metadata": {
    "id": "ApGYn_Y0mX96"
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def delete_file(path):\n",
    "    \"\"\"\n",
    "    Elimina un singolo file dalla destinazione.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        os.remove(path)\n",
    "        return {\n",
    "            \"tipo\": \"elimina\",\n",
    "            \"successo\": True,\n",
    "            \"sorgente\": None,\n",
    "            \"destinazione\": str(path),\n",
    "            \"errore\": None\n",
    "        }\n",
    "    except Exception as exc:\n",
    "        return {\n",
    "            \"tipo\": \"elimina\",\n",
    "            \"successo\": False,\n",
    "            \"sorgente\": None,\n",
    "            \"destinazione\": str(path),\n",
    "            \"errore\": str(exc)\n",
    "        }"
   ],
   "metadata": {
    "id": "wYQ1gV4Zmam2"
   },
   "execution_count": 20,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"esegui_operazione\" esegue una singola operazione di copia o cancellazione determinata dal tipo_operazione presente\n",
    "nella tupla (operazione,percorso_relativo)."
   ],
   "metadata": {
    "id": "kCg1pL6Ambw-"
   }
  },
  {
   "cell_type": "code",
   "source": [
    "def esegui_operazione(operazione, source_folder, destination_folder):\n",
    "    \"\"\"\n",
    "    Esegue una singola operazione (copia o eliminazione), usando il percorso relativo presente\n",
    "    all'interno della tupla (operazione,percorso_relativo) per creare i percorsi completi sorgenti e destinazione\n",
    "    \"\"\"\n",
    "    tipo_operazione , percorso_relativo = operazione\n",
    "\n",
    "    percorso_src = Path(source_folder) / percorso_relativo\n",
    "    percorso_dest = Path(destination_folder) / percorso_relativo\n",
    "\n",
    "    if tipo_operazione == \"copia\":\n",
    "        return copy_file(percorso_src, percorso_dest)\n",
    "    else:\n",
    "        return delete_file(percorso_dest)"
   ],
   "metadata": {
    "id": "GXt23ItWmfLR"
   },
   "execution_count": 21,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"processa_sottogruppo\" esegue in parallelo, con un pool di thread,\n",
    "tutte le operazioni di un singolo sottogruppo assegnato a un processo."
   ],
   "metadata": {
    "id": "6hxkW4aPmgQq"
   }
  },
  {
   "cell_type": "code",
   "source": "def processa_sottogruppo(sottogruppo, source_folder, destination_folder, num_thread=THREAD):\n    \"\"\"\n    Esegue in parallelo, con un pool di thread, tutte le operazioni\n    (copia o eliminazione) di un singolo sottogruppo.\n    \"\"\"\n    if not source_folder or not destination_folder or not sottogruppo:\n        return []\n    with ThreadPoolExecutor(max_workers=min(len(sottogruppo), num_thread)) as executor:\n        futures = [executor.submit(esegui_operazione, operazione, source_folder, destination_folder) for operazione in sottogruppo]\n        return [f.result() for f in futures]",
   "metadata": {
    "id": "MrmCiw5NmiH4"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "La funzione \"riepiloga_risultati\" smista una lista piatta di risultati delle operazioni\n",
    "in un riepilogo con copie e cancellazioni, riuscite e fallite."
   ],
   "metadata": {
    "id": "kERazcGzmkGD"
   }
  },
  {
   "cell_type": "code",
   "source": "def riepiloga_risultati(risultati):\n    \"\"\"\n    Prende la lista piatta di risultati (ciascuno un dizionario con tipo, successo, ecc.)\n    e restituisce un dizionario di riepilogo con quattro categorie: copie riuscite,\n    cancellazioni riuscite, copie fallite, cancellazioni fallite.\n    \"\"\"\n    copiati, eliminati, errori_copia, errori_eliminazione = [], [], [], []\n\n    for risultato in risultati:\n        if risultato[\"tipo\"] == \"copia\" and risultato[\"successo\"]:\n            copiati.append(risultato)\n        elif risultato[\"tipo\"] == \"copia\" and not risultato[\"successo\"]:\n            errori_copia.append(risultato)\n        elif risultato[\"tipo\"] == \"elimina\" and risultato[\"successo\"]:\n            eliminati.append(risultato)\n        else:\n            errori_eliminazione.append(risultato)\n\n    return {\n        \"copiati\": copiati,\n        \"eliminati\": eliminati,\n        \"errori_copia\": errori_copia,\n        \"errori_eliminazione\": errori_eliminazione,\n    }",
   "metadata": {
    "id": "QO4CHv2ymntS"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": "La funzione \"stampa_riepilogo\" si occupa solo della presentazione: stampa a video il contenuto di un riepilogo\nprodotto da \"riepiloga_risultati\", che resta invece una funzione pura, senza effetti collaterali di stampa,\ncosì da poter essere richiamata anche da codice esterno (ad esempio da un test automatico).",
   "metadata": {}
  },
  {
   "cell_type": "code",
   "source": "def stampa_riepilogo(riepilogo):\n    \"\"\"\n    Stampa a video il contenuto di un riepilogo prodotto da riepiloga_risultati.\n    \"\"\"\n    print(\"\\n--- RIEPILOGO SINCRONIZZAZIONE ---\")\n    print(f\"File copiati con successo: {len(riepilogo['copiati'])}\")\n    print(f\"File eliminati con successo: {len(riepilogo['eliminati'])}\")\n    print(f\"Errori durante la copia: {len(riepilogo['errori_copia'])}\")\n    print(f\"Errori durante l'eliminazione: {len(riepilogo['errori_eliminazione'])}\")\n\n    errori = riepilogo[\"errori_copia\"] + riepilogo[\"errori_eliminazione\"]\n    if errori:\n        print(\"\\n--- DETTAGLIO ERRORI ---\")\n        for err in errori:\n            print(f\"[{err['tipo'].upper()}] File: {err['destinazione']} | Errore: {err['errore']}\")",
   "metadata": {},
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": "La funzione \"rimuovi_cartelle_vuote\" ripulisce la destinazione dalle sottocartelle rimaste vuote dopo l'eliminazione\ndei file obsoleti, percorrendo l'albero delle directory dal basso verso l'alto (topdown=False) in modo da poter\nrimuovere anche le cartelle annidate che restano vuote solo dopo aver svuotato quelle al loro interno.",
   "metadata": {}
  },
  {
   "cell_type": "code",
   "source": "def rimuovi_cartelle_vuote(cartella):\n    \"\"\"\n    Rimuove ricorsivamente le sottocartelle rimaste vuote all'interno di\n    una cartella, senza toccare la cartella radice.\n    \"\"\"\n    for root, dirs, _ in os.walk(cartella, topdown=False):\n        for nome_dir in dirs:\n            percorso_dir = Path(root) / nome_dir\n            try:\n                percorso_dir.rmdir()\n            except OSError:\n                pass  # la cartella non è vuota: la si lascia intatta",
   "metadata": {},
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": "La funzione \"stampa_dry_run\" mostra a video le operazioni che \"sincronizza\" eseguirebbe quando invocata con\ndry_run=True, senza modificare alcun file: utile per verificare l'esito di una sincronizzazione prima di\neseguirla realmente, ad esempio prima del primo avvio su un server in produzione.",
   "metadata": {}
  },
  {
   "cell_type": "code",
   "source": "def stampa_dry_run(operazioni):\n    \"\"\"\n    Mostra le operazioni pianificate da una sincronizzazione senza eseguirle.\n    \"\"\"\n    print(\"\\n--- DRY RUN: nessuna modifica verrà effettuata ---\")\n    if not operazioni:\n        print(\"Nessuna operazione da eseguire: sorgente e destinazione sono già sincronizzate.\")\n        return\n    for tipo, percorso in operazioni:\n        azione = \"COPIA\" if tipo == \"copia\" else \"ELIMINA\"\n        print(f\"[{azione}] {percorso}\")",
   "metadata": {},
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": "La funzione \"sincronizza\" orchestra l'intera sincronizzazione tra sorgente e destinazione: crea la cartella di\ndestinazione se non esiste ancora, scansiona sorgente e destinazione, le confronta, costruisce la lista di\noperazioni e la divide in sottogruppi. Se invocata con dry_run=True si limita a mostrare le operazioni\npianificate senza modificare alcun file; altrimenti le distribuisce a un pool di processi (ciascuno dei quali\nusa al suo interno un pool di thread) e al termine ripulisce le sottocartelle rimaste vuote in destinazione.\nRestituisce sempre il riepilogo dell'operazione, anche quando non c'è nulla da sincronizzare, così da poter\nessere richiamata anche da codice esterno, ad esempio da un test automatico. I parametri num_processi e\nnum_thread permettono di personalizzare il grado di parallelismo rispetto ai valori di default THREAD e PROCESSI.",
   "metadata": {
    "id": "e2iVEtxcm3Ql"
   }
  },
  {
   "cell_type": "code",
   "source": "def sincronizza(source_folder, destination_folder, num_processi=None, num_thread=None, dry_run=False):\n    \"\"\"\n    Orchestra l'intera sincronizzazione. Crea la destinazione se non esiste, scansiona sorgente e destinazione,\n    confronta le cartelle e costruisce la lista di operazioni. Se dry_run è True si limita a mostrare le\n    operazioni pianificate; altrimenti le distribuisce a un pool di processi (ciascuno dei quali usa al suo\n    interno un pool di thread) e al termine ripulisce le cartelle vuote rimaste in destinazione.\n    Restituisce sempre il riepilogo dell'operazione.\n    \"\"\"\n    if num_processi is None:\n        num_processi = PROCESSI\n    if num_thread is None:\n        num_thread = THREAD\n\n    Path(destination_folder).mkdir(parents=True, exist_ok=True)\n\n    scan_src = scansiona_cartella(source_folder)\n    scan_dest = scansiona_cartella(destination_folder)\n    conf_cart = confronta_cartelle(scan_src, scan_dest)\n    list_oper = crea_lista_operazioni(conf_cart)\n\n    if dry_run:\n        stampa_dry_run(list_oper)\n        return list_oper\n\n    sottogruppi = dividi_in_sottogruppi(list_oper, num_processi)\n\n    if not sottogruppi:\n        print(\"Nessuna operazione da eseguire: sorgente e destinazione sono già sincronizzate.\")\n        riepilogo = riepiloga_risultati([])\n        stampa_riepilogo(riepilogo)\n        return riepilogo\n\n    with ProcessPoolExecutor(max_workers=num_processi) as executor:\n        futures = [\n            executor.submit(processa_sottogruppo, sottogruppo, source_folder, destination_folder, num_thread)\n            for sottogruppo in sottogruppi\n        ]\n        risultati_per_processo = [f.result() for f in futures]  # lista di liste, una per processo\n\n    # appiattisce la lista di liste in un'unica lista di risultati\n    risultati = [risultato for lista_risultati in risultati_per_processo for risultato in lista_risultati]\n\n    rimuovi_cartelle_vuote(destination_folder)\n\n    riepilogo = riepiloga_risultati(risultati)\n    stampa_riepilogo(riepilogo)\n    return riepilogo",
   "metadata": {
    "id": "8aZnDAQ7m43Z"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": [
    "Mostra il numero di file presenti nella sorgente e nelle destinazione"
   ],
   "metadata": {
    "id": "RJVwYU0AGJwO"
   }
  },
  {
   "cell_type": "code",
   "source": "def mostra_stato_cartelle(src, dst):\n    \"\"\"Stampa il conteggio e l'elenco dei file presenti in sorgente e destinazione.\"\"\"\n    files_src = sorted(str(p.relative_to(src)) for p in Path(src).rglob(\"*\") if p.is_file())\n    files_dst = sorted(str(p.relative_to(dst)) for p in Path(dst).rglob(\"*\") if p.is_file())\n\n    print(f\"Sorgente     ({len(files_src)} file): {files_src}\")\n    print(f\"Destinazione ({len(files_dst)} file): {files_dst}\")\n    print(\"-\" * 50)",
   "metadata": {
    "id": "QVTv8XhLGKKH"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": "La funzione \"parse_argomenti\" definisce l'interfaccia a riga di comando del programma, così da poter lanciare\nuna sincronizzazione reale (ad esempio il backup di un server locale) passando sorgente e destinazione, ed\neventualmente il numero di processi, di thread o l'opzione --dry-run, senza dover modificare il codice.",
   "metadata": {}
  },
  {
   "cell_type": "code",
   "source": "def parse_argomenti(argv=None):\n    \"\"\"\n    Definisce e interpreta gli argomenti da riga di comando per usare il\n    programma su cartelle reali senza modificare il codice.\n    \"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Sincronizza una cartella sorgente con una cartella di destinazione.\"\n    )\n    parser.add_argument(\"source\", help=\"Cartella sorgente\")\n    parser.add_argument(\"destination\", help=\"Cartella di destinazione\")\n    parser.add_argument(\n        \"--processi\", type=int, default=PROCESSI,\n        help=f\"Numero di processi da usare (default: {PROCESSI})\"\n    )\n    parser.add_argument(\n        \"--thread\", type=int, default=THREAD,\n        help=f\"Numero di thread per processo (default: {THREAD})\"\n    )\n    parser.add_argument(\n        \"--dry-run\", action=\"store_true\",\n        help=\"Mostra le operazioni che verrebbero eseguite senza modificare alcun file\"\n    )\n    return parser.parse_args(argv)",
   "metadata": {},
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "source": "if __name__ == \"__main__\":\n    if \"ipykernel\" not in sys.modules and len(sys.argv) > 1:\n        # Esecuzione reale da riga di comando, ad esempio:\n        # python sync.py /percorso/sorgente /percorso/backup --processi 2 --dry-run\n        argomenti = parse_argomenti()\n        sincronizza(\n            argomenti.source,\n            argomenti.destination,\n            num_processi=argomenti.processi,\n            num_thread=argomenti.thread,\n            dry_run=argomenti.dry_run,\n        )\n    else:\n        for numero_file in [10, 500]:\n            print(f\" TEST CON {numero_file} FILE\")\n\n            with tempfile.TemporaryDirectory() as src_dir, tempfile.TemporaryDirectory() as dst_dir:\n                src = Path(src_dir)\n                dst = Path(dst_dir) / \"backup\"  # non esiste ancora: verifica la creazione automatica\n\n                for i in range(numero_file):\n                    if i % 5 == 0:\n                        cartella = src / \"sottocartella\"\n                        cartella.mkdir(exist_ok=True)\n                    else:\n                        cartella = src\n                    (cartella / f\"file_{i}.txt\").write_text(f\"contenuto {i}\", encoding=\"utf-8\")\n\n                print(f\"\\nDestinazione '{dst.name}' esiste ancora? {dst.exists()}\")\n\n                print(\"\\nDry-run sulla prima sincronizzazione\")\n                sincronizza(str(src), str(dst), dry_run=True)\n                print(f\"Destinazione creata da mkdir del dry-run? {dst.exists()}  (nessun file copiato)\")\n\n                print(\"\\nTutti nuovi\")\n                inizio = time.perf_counter()\n                sincronizza(str(src), str(dst))\n                durata = time.perf_counter() - inizio\n                print(f\"Tempo impiegato: {durata:.3f} secondi\")\n                print(\"\\nDopo la 1a sincronizzazione\")\n                mostra_stato_cartelle(src, dst)\n\n                print(\"\\nNessun cambiamento\")\n                inizio = time.perf_counter()\n                sincronizza(str(src), str(dst))\n                durata = time.perf_counter() - inizio\n                print(f\"Tempo impiegato: {durata:.3f} secondi\")\n\n                time.sleep(1.1)  # garantisce che il nuovo mtime sia effettivamente più recente\n                n_modificati = max(1, numero_file // 10)\n                for i in range(n_modificati):\n                    cartella = src / \"sottocartella\" if i % 5 == 0 else src\n                    (cartella / f\"file_{i}.txt\").write_text(f\"contenuto modificato {i}\", encoding=\"utf-8\")\n\n                print(f\"\\n{n_modificati} file modificati ---\")\n                inizio = time.perf_counter()\n                sincronizza(str(src), str(dst))\n                durata = time.perf_counter() - inizio\n                print(f\"Tempo impiegato: {durata:.3f} secondi\")\n\n                n_eliminati = max(1, numero_file // 10)\n                for i in range(numero_file - n_eliminati, numero_file):\n                    cartella = src / \"sottocartella\" if i % 5 == 0 else src\n                    percorso = cartella / f\"file_{i}.txt\"\n                    if percorso.exists():\n                        percorso.unlink()\n\n                # svuota del tutto la sottocartella in sorgente, cosi in destinazione\n                # diventa obsoleta e permette di verificare la pulizia delle cartelle vuote\n                for percorso in (src / \"sottocartella\").glob(\"*\"):\n                    percorso.unlink()\n\n                print(f\"\\n{n_eliminati} file rimossi dalla sorgente, sottocartella svuotata\")\n                mostra_stato_cartelle(src, dst)\n                inizio = time.perf_counter()\n                sincronizza(str(src), str(dst))\n                durata = time.perf_counter() - inizio\n                print(f\"Tempo impiegato: {durata:.3f} secondi\")\n                print(\"\\nDopo la 4a sincronizzazione (rimozione obsoleti)\")\n                mostra_stato_cartelle(src, dst)\n                print(f\"Sottocartella vuota rimossa dalla destinazione? {not (dst / 'sottocartella').exists()}\")",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "r1hldULQm8I1",
    "outputId": "6373346f-9ba8-4402-a305-fc8ec5090ad0"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "source": "#Conclusione\n\nIl progetto realizzato implementa un sistema di sincronizzazione tra una cartella sorgente e una cartella destinazione, sfruttando sia il multithreading che il multiprocessing per gestire sia il carico di lettura e scrittura (I/O) nel file system sia al calcolo di lavoro su più core della CPU.\n\nIl flusso si articola in più fasi:\n- Nella prima fase si ripercorre ricorsivamente sia la cartella sorgente che la destinazione restituendo per ciascun percorso relativo la dimensione e la data di modifica del file.\n- Nella seconda fase vengono confrontate le due cartelle sfruttando le operazioni insiemistiche sulle chiavi per classificare i file in tre categorie (nuovi, modificati o obsoleti)\n- Nella terza fase vengono trasformate queste tre liste in un'unica sequenza piatta, ciascuna rappresentata da una tupla formata da azione e percorso relativo. Si è scelto di unificare le operazioni di copia e modifica in quanto la libreria **shutil.copy2** non distingue tra file nuovi e file modificati: esegue semplicemente un’operazione di copia dal percorso sorgente a quello di destinazione, sovrascrivendo il file di destinazione se esiste già.\n- A partire da questa lista entrano in gioco il multiprocessing e il multithreading. La funzione dividi_in_sottogruppi divide in sottogruppi equilibrati, pari al numero di processi. Ogni sottogruppo viene affidato a un processo tramite ProcessPoolExecutor. Ogni processo, indipendente dall'altro, può lavorare realmente in parallelo sfruttando i core della CPU della macchina.\n- All'interno di ogni processo, il lavoro non viene gestito in sequenza ma da un pool di thread (ThreadPoolExecutor) che esegue in parallelo le singole operazioni di copia ed eliminazione del sottogruppo chiamando la funzione esegui_operazione. Questa scelta è scaturita dal fatto che le operazioni di lettura e scrittura dei file sono operazioni I/O, permettendo così a più thread di procedere realmente in parallelo mentre attendono la risposta del disco.\n- Al termine, la funzione sincronizza ripulisce le sottocartelle rimaste vuote in destinazione e restituisce sempre un riepilogo dell'operazione, così da poter essere richiamata anche da codice esterno (ad esempio da un test automatico) e non solo per il suo effetto di stampa a video.\n\nIn definitiva, l'architettura lavora su due livelli distinti di parallelismo: il multiprocessing distribuisce il carico complessivo sui core della CPU, mentre il multithreading, all'interno di ogni singolo processo gestisce le operazioni di I/O.\n\nVantaggi e svantaggi sul numero di processi pari ai core della cpu.\n\nVantaggi:\n- Portabilità: adattamento alla macchina su cui gira il programma\n- Scalabilità: su una macchina più potente il sistema sfrutta più core\n- Evita i due estremi: sotto-utilizzo delle risorse su hardware potente , oppure overhead di gestione eccessivo su hardware modesto.\n\nSvantaggi:\n- carico di lavoro: il numero di processi dipende dal tipo di storage che dal numero di core della CPU\n-Su dischi meccanici tradizionali, troppi processi che scrivono in parallelo possono peggiorare le prestazioni per la contesa sulle testine di lettura/scrittura; su SSD il beneficio resta più evidente.\n\nInfine, rispetto alla prima versione, il programma è stato reso più solido dal punto di vista pratico: la cartella di destinazione viene creata automaticamente al primo avvio (il caso tipico del backup di un server locale), le sottocartelle rimaste vuote vengono ripulite, e un'interfaccia a riga di comando (con opzioni --processi, --thread e --dry-run) permette di usarlo su cartelle reali senza toccare il codice.",
   "metadata": {
    "id": "8cq6movEG8U4"
   }
  }
 ]
}