/*
Version: 17 juillet 2025

Pour sauvegarder un dossier et ses sous-dossiers vers une partition/un disque externe (c'est l'équivalent de Git push).
Il scanne tout le www (20Go 37000 fichiers) en 1 seconde!. En mode copie il tourne à 2% CPU, console non visible.
Tellement pratique pour n'oublier aucun fichier.

Après avoir travaillé sur un projet ex. /home/joe/www/apps/test
Pour voir les fichiers nouveaux/modifiés/supprimés, mais sans effectuer de modifications sur la sauvegarde:
$ cd ~
$ ./sauv www/apps/test /media/truc/C416-16A6 sanscopie
*!=taille*      www/apps/test/ausarmor.sh
*!=date*        www/apps/test/index.html
*existe pas*    www/apps/test/nbmessages
*fic supprimé*  www/apps/test/2/index00.php
Scanné dans www/test 8795 dossiers, 37168 fichiers → 3 fichiers à copier. 0 dossier, 1 fichier à supprimer

$ ./sauv www/apps/test /media/truc/C416-16A6
    si le dossier de destination n'existe pas il le crée récursivement.
    si le fichier de destination n'existe pas il le crée.
    si le fichier de destination a une taille différente il l'écrase.
    si le fichier de destination a une date de modification antérieure à la source il l'écrase.

    2e scan de la sauvegarde vers le dossier local:
    si le fichier existe sur la sauvegarde, mais pas en local alors il a été supprimé en local → le supprime sur la sauvegarde

Une option permet de copier vers la sauvegarde mais sans supprimer
$ ./sauv www/apps/test /media/truc/C416-16A6 nepassupprimer
Scanné dans www/test 8795 dossiers, 37168 fichiers → 3 fichiers ont été copiés. 0 dossier, 0 fichier ont été supprimés

Pour restaurer une sauvegarde:
$ cd /media/truc/C416-16A6
$ ./sauv www /home/joe sanscopie
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>

int dossiersscannés = 0;
int fichiersscannés = 0;
int nbfichiersacopier = 0;
int nbdossiersasupprimer = 0;
int nbfichiersasupprimer = 0;

/*==========================================================*/
// Copie un fichier vers la sauvegarde; si le dossier de destination n'existe pas, le crée
// ex: copie("/media/truc","www/test", "/home/joe/www/test/1.c", "/media/truc/www/test/1.c")
void copie(char* repsauv, char* dosrel, char* flocal, char* fsauv){
  // Pour gagner du temps regarde si le dossier de destination existe, si non il les crée un par un
  char* destfolder = NULL;
  asprintf(&destfolder, "%s/%s", repsauv, dosrel);  
  struct stat st = {0};
  // Crée chaque sous-dossier s'il n'existe pas
  if(stat(destfolder, &st) == -1){
    char* tmp = repsauv;        // /media/truc/C416-16A6
    char* str = strdup(dosrel); // test/machin/truc
    char* token = strtok(str, "/");
    while (token != NULL) {
      asprintf(&tmp, "%s/%s", tmp, token);  // /media/truc/C416-16A6/test puis /media/truc/C416-16A6/test/machin
      struct stat st = {0}; //crée le dossier s'il n'existe pas
      if (stat(tmp, &st) == -1) mkdir(tmp, 0700);
      token = strtok(NULL, "/");
    }
    free(str);
  }
  free(destfolder);

  // Maintenant que le dossier existe, y copie le fichier
  char c[8192];
  FILE* stream_R = fopen(flocal, "r");
  FILE* stream_W = fopen(fsauv, "w");
  while (!feof(stream_R)){
    size_t bytes = fread(c, 1, sizeof(c), stream_R);
    if (bytes) {fwrite(c, 1, bytes, stream_W);}
  }
  fclose(stream_R);
  fclose(stream_W);
}
/*==========================================================*/
void scancopie(char* dos, char* repsauv, char testSansCopie)
{
  // Commence par scanner les dossiers uniquements
  DIR *dip = opendir(dos); if (dip == NULL) {printf("Impossible d'ouvrir le répertoire %s\n", dos); return;}
  struct dirent *dit;
  dossiersscannés++;
  while ((dit = readdir(dip)) != NULL){
    if (dit->d_type == 4){
      if(!strcmp(dit->d_name, ".") || !strcmp(dit->d_name, "..")) continue;
      dossiersscannés++;
      char* sousdos = NULL;
      asprintf(&sousdos, "%s%s%s", dos,"/",dit->d_name);
      //printf("dos %s/%s\n", dos, dit->d_name);
      scancopie(sousdos, repsauv, testSansCopie); // dossier trouvé? le scanne
      free(sousdos);
    }
  }
  closedir(dip);

  // puis scanne les fichiers
  dip = opendir(dos); if (dip == NULL) {printf("Impossible d'ouvrir le répertoire %s\n", dos); return;}

  while ((dit = readdir(dip)) != NULL){
    if (dit->d_type == 8){
      fichiersscannés++;

      char* flocal = NULL;
      asprintf(&flocal, "%s/%s", dos, dit->d_name);        

      char* fsauv = NULL;
      asprintf(&fsauv, "%s/%s/%s", repsauv, dos, dit->d_name);  

      // Regarde si ce fichier existe dans la sauvegarde
      if (access(fsauv, F_OK) != 0){ // il n'existe pas → le copie
        printf("*existe pas*\t%s\n", flocal);
        nbfichiersacopier++;
        if(testSansCopie==0) copie(repsauv, dos, flocal,fsauv);
      }
      // il existe. Ont-ils la même taille? Sinon celui en local est plus récent?
      else{
        struct stat st;
        stat(flocal, &st);
        off_t  flocalsize = st.st_size;
        time_t flocaldate = st.st_mtime;

        stat(fsauv, &st);
        off_t  fsauvsize = st.st_size;
        time_t fsauvdate = st.st_mtime;

        if(flocalsize != fsauvsize){ // ils n'ont pas la même taille → copie
          //printf("*!=taille* %ld→%ld\t%s\n", flocalsize, fsauvsize, flocal);
          printf("*!=taille*\t%s\n", flocal);
          nbfichiersacopier++;
          if(testSansCopie==0) copie(repsauv, dos, flocal,fsauv);
        }
        else if((flocaldate -1) > fsauvdate){ // ils ont la même taille, mais le fichier source a été modifié après la sauvegarde
          //printf("*!=date* %ld→%ld %s\n", flocaldate, fsauvdate, flocal);
          printf("*!=date*\t%s\n", flocal);
          nbfichiersacopier++;
          if(testSansCopie==0) copie(repsauv, dos, flocal,fsauv);
        }
        //else printf("%s\n", flocal); // ils ont la même taille/même date de modif ne rien faire
      }
      free(fsauv);
      free(flocal);
    }
  }
  closedir(dip);
}

/*==========================================================*/
void scansuppr(char* dos, char* repsauv) // www/test, /media/machin/truc
{
  char* dsauv = NULL;
  asprintf(&dsauv, "%s/%s", repsauv, dos); // /media/machin/truc/www/test

  // Commence par scanner les dossiers uniquements
  DIR *dip = opendir(dsauv); if (dip == NULL) {printf("Impossible d'ouvrir le répertoire %s\n", dsauv); return;}
  struct dirent *dit;
  while ((dit = readdir(dip)) != NULL){
    if (dit->d_type == 4){
      if(!strcmp(dit->d_name, ".") || !strcmp(dit->d_name, "..")) continue;
      char* sousdos = NULL;
      asprintf(&sousdos, "%s/%s", dos, dit->d_name);
      //printf("sousdos %s\n", sousdos);

      DIR *diq;
      if ((diq = opendir(sousdos)) == NULL)
        printf("*DOS supprimé*\t%s\n", sousdos);
      else closedir(diq);

      scansuppr(sousdos, repsauv);
      free(sousdos);
    }
  }
  closedir(dip);

  // puis scanne les fichiers
  dip = opendir(dsauv); if (dip == NULL) {printf("Impossible d'ouvrir le répertoire %s\n", dsauv); return;}
  while ((dit = readdir(dip)) != NULL){
    if (dit->d_type == 8){
      char* flocal = NULL;
      asprintf(&flocal, "%s/%s", dos, dit->d_name);

      // un fichier a été supprimé en local → le supprimer sur la sauvegarde
      if (access(flocal, F_OK) != 0){
        // il n'existe pas → le supprime sur la sauvegarde
        char* fsauv = NULL;
        asprintf(&fsauv, "%s/%s", dsauv, dit->d_name);
        printf("*FIC supprimé*\t%s\n", flocal);
        //printf("*fic supprimé*\t\t%s\n", dit->d_name);
        nbfichiersasupprimer++;
        unlink(fsauv);
        free(fsauv);
      }
      free(flocal);
    }
  }
  closedir(dip);

  // Après avoir effacé ses fichiers, regarde si ce dossier existe en local pour éventuellement supprimer ce dossier sur la sauvegarde
  if ((dip = opendir(dos)) == NULL){
    //printf("*dos supprimé*\t%s\n", dos);
    rmdir(dsauv);
    nbdossiersasupprimer++;
  }
  else closedir(dip);

  free(dsauv);
}
/*==========================================================*/
int main(int argc, char *argv[])
{
  if (argc < 3){
     printf("\tSauvegarde d'un dossier vers un autre dossier, une autre partition, ou un disque externe\n\
    Usage: %s [dossier relatif à scanner ] [chemin absolu dossier sauvegarde] options: [sanscopie OU nepassupprimer]\n\
    Mettre chemins sans / final\n\
    Exemple %s www/test /media/truc/C416-16A6 sanscopie\n \
    Exemple %s www/test /media/truc/C416-16A6\n \
    Exemple %s www/test /media/truc/C416-16A6 nepassupprimer\n\
        sanscopie      : indique le nombre de dossiers/fichiers à supprimer mais ne les copie/supprime pas.\n\
        nepassupprimer : copie les fichiers vers la sauvegarde, mais ne fait pas le scan en sens inverse ni ne supprime.\n\
        sans option    : scanne  local → sauvegarde et copie, puis scanne sauvegarde → local et supprime.\n", argv[0], argv[0], argv[0], argv[0]);
     return 0;
  }

  // Regarde si le répertoire de sauvegarde est monté
  DIR *dip = opendir(argv[2]);
  if (dip == NULL) {printf("\tLe répertoire de sauvegarde est inaccessible: partition non montée?\n"); return 1;}
  else closedir(dip);

  char testSansCopie = (argc==4 && !strcmp(argv[3],"sanscopie")) ? 1 : 0;

  // Regarde les fichiers à copier vers la sauvegarde
  scancopie(argv[1], argv[2], testSansCopie);
  //printf("\tJ'ai scanné %d dossiers, %d fichiers\n", dossiersscannés, fichiersscannés);
  //printf("\t%d fichier%s %s\n", nbfichiersacopier, (nbfichiersacopier>1)?"s":"", (testSansCopie) ? "à copier" : "ont été copiés");

  // Si l'option "nepassupprimer" est présente, alors ne pas scanner ni supprimer
  if(argc==4 && !strcmp(argv[3],"nepassupprimer"));
  else
    //sinon scanne en sens inverse de la sauvegarde vers local,
    //pour regarder des fichiers ont été supprimés en local, donc à supprimer sur la sauvegarde
    scansuppr(argv[1], argv[2]);
    //printf("\t%d dossier%s, %d fichier%s %s\n",
    //        nbdossiersasupprimer, (nbdossiersasupprimer>1)?"s":"",
    //        nbfichiersasupprimer, (nbfichiersasupprimer>1)?"s":"",
    //        (testSansCopie) ? "à supprimer" : "ont été supprimés");

  printf("Scanné dans %s %d dossiers, %d fichiers → %d fichier%s %s. %d dossier%s, %d fichier%s %s\n",
      argv[1],
      dossiersscannés, fichiersscannés,
      nbfichiersacopier, (nbfichiersacopier>1)?"s":"", (testSansCopie) ? "à copier" : "ont été copiés",
      nbdossiersasupprimer, (nbdossiersasupprimer>1)?"s":"",
      nbfichiersasupprimer, (nbfichiersasupprimer>1)?"s":"",
      (testSansCopie) ? "à supprimer" : "ont été supprimés");

  return 0;
}