62 votes

Comment lister récursivement les répertoires en C sous Linux ?

J'ai besoin de lister récursivement tous les répertoires et fichiers en programmation C. J'ai regardé dans FTW mais ce n'est pas inclus dans les 2 systèmes d'exploitation que j'utilise (Fedora et Minix). Je commence à avoir un gros mal de tête à cause de toutes les choses différentes que j'ai lues au cours des dernières heures.

Si quelqu'un connaît un extrait de code que je pourrais examiner, ce serait formidable, ou si quelqu'un peut me donner une bonne direction à ce sujet, je lui en serais très reconnaissant.

0 votes

Pourquoi ne pas simplement faire cela dans un langage de script ? Ce serait plus rapide et plus facile à écrire.

4 votes

@dbeer Et s'il a besoin de cette information dans un programme C ?

1 votes

Êtes-vous sûr de vouloir effectuer l'action de manière récursive ? Je signale que les liens cycliques et les limites de fichiers ouverts peuvent poser un problème pour les implémentations récursives. J'envisagerais d'utiliser une liste liée (ou deux), afin que le code puisse vérifier les dossiers précédemment traités. Cela permettra également au code d'utiliser un seul fichier ouvert tout en traversant des hiérarchies profondes.

104voto

Nominal Animal Points 7207

Pourquoi tout le monde s'obstine-t-il à réinventer la roue, encore et encore ?

La norme POSIX.1-2008 a normalisé le nftw() également définie dans la Single Unix Specification v4 (SuSv4), et disponible dans Linux (glibc, man 3 nftw ), OS X, et la plupart des variantes actuelles de BSD. Il n'est pas nouveau du tout.

Naïf opendir() / readdir() / closedir() -ne gèrent presque jamais les cas où des répertoires ou des fichiers sont déplacés, renommés ou supprimés pendant la traversée de l'arbre, alors que nftw() devrait les gérer avec élégance.

À titre d'exemple, considérez le programme C suivant qui liste l'arborescence des répertoires en commençant par le répertoire de travail actuel, ou par chacun des répertoires nommés sur la ligne de commande, ou juste les fichiers nommés sur la ligne de commande :

/* We want POSIX.1-2008 + XSI, i.e. SuSv4, features */
#define _XOPEN_SOURCE 700

/* Added on 2017-06-25:
   If the C library can support 64-bit file sizes
   and offsets, using the standard names,
   these defines tell the C library to do so. */
#define _LARGEFILE64_SOURCE
#define _FILE_OFFSET_BITS 64 

#include <stdlib.h>
#include <unistd.h>
#include <ftw.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

/* POSIX.1 says each process has at least 20 file descriptors.
 * Three of those belong to the standard streams.
 * Here, we use a conservative estimate of 15 available;
 * assuming we use at most two for other uses in this program,
 * we should never run into any problems.
 * Most trees are shallower than that, so it is efficient.
 * Deeper trees are traversed fine, just a bit slower.
 * (Linux allows typically hundreds to thousands of open files,
 *  so you'll probably never see any issues even if you used
 *  a much higher value, say a couple of hundred, but
 *  15 is a safe, reasonable value.)
*/
#ifndef USE_FDS
#define USE_FDS 15
#endif

int print_entry(const char *filepath, const struct stat *info,
                const int typeflag, struct FTW *pathinfo)
{
    /* const char *const filename = filepath + pathinfo->base; */
    const double bytes = (double)info->st_size; /* Not exact if large! */
    struct tm mtime;

    localtime_r(&(info->st_mtime), &mtime);

    printf("%04d-%02d-%02d %02d:%02d:%02d",
           mtime.tm_year+1900, mtime.tm_mon+1, mtime.tm_mday,
           mtime.tm_hour, mtime.tm_min, mtime.tm_sec);

    if (bytes >= 1099511627776.0)
        printf(" %9.3f TiB", bytes / 1099511627776.0);
    else
    if (bytes >= 1073741824.0)
        printf(" %9.3f GiB", bytes / 1073741824.0);
    else
    if (bytes >= 1048576.0)
        printf(" %9.3f MiB", bytes / 1048576.0);
    else
    if (bytes >= 1024.0)
        printf(" %9.3f KiB", bytes / 1024.0);
    else
        printf(" %9.0f B  ", bytes);

    if (typeflag == FTW_SL) {
        char   *target;
        size_t  maxlen = 1023;
        ssize_t len;

        while (1) {

            target = malloc(maxlen + 1);
            if (target == NULL)
                return ENOMEM;

            len = readlink(filepath, target, maxlen);
            if (len == (ssize_t)-1) {
                const int saved_errno = errno;
                free(target);
                return saved_errno;
            }
            if (len >= (ssize_t)maxlen) {
                free(target);
                maxlen += 1024;
                continue;
            }

            target[len] = '\0';
            break;
        }

        printf(" %s -> %s\n", filepath, target);
        free(target);

    } else
    if (typeflag == FTW_SLN)
        printf(" %s (dangling symlink)\n", filepath);
    else
    if (typeflag == FTW_F)
        printf(" %s\n", filepath);
    else
    if (typeflag == FTW_D || typeflag == FTW_DP)
        printf(" %s/\n", filepath);
    else
    if (typeflag == FTW_DNR)
        printf(" %s/ (unreadable)\n", filepath);
    else
        printf(" %s (unknown)\n", filepath);

    return 0;
}

int print_directory_tree(const char *const dirpath)
{
    int result;

    /* Invalid directory path? */
    if (dirpath == NULL || *dirpath == '\0')
        return errno = EINVAL;

    result = nftw(dirpath, print_entry, USE_FDS, FTW_PHYS);
    if (result >= 0)
        errno = result;

    return errno;
}

int main(int argc, char *argv[])
{
    int arg;

    if (argc < 2) {

        if (print_directory_tree(".")) {
            fprintf(stderr, "%s.\n", strerror(errno));
            return EXIT_FAILURE;
        }

    } else {

        for (arg = 1; arg < argc; arg++) {
            if (print_directory_tree(argv[arg])) {
                fprintf(stderr, "%s.\n", strerror(errno));
                return EXIT_FAILURE;
            }
        }

    }

    return EXIT_SUCCESS;
}

La plupart du code ci-dessus se trouve dans print_entry() . Sa tâche est d'imprimer chaque entrée du répertoire. Dans print_directory_tree() nous disons nftw() pour l'appeler pour chaque entrée de répertoire qu'il voit.

Le seul détail délicat ci-dessus est la décision sur le nombre de descripteurs de fichiers que l'on doit laisser nftw() utiliser. Si votre programme utilise au maximum deux descripteurs de fichiers supplémentaires (en plus des flux standard) pendant le parcours de l'arborescence des fichiers, 15 est connu pour être sûr (sur tous les systèmes disposant de nftw() et étant pour la plupart conformes à POSIX).

Sous Linux, vous pouvez utiliser sysconf(_SC_OPEN_MAX) pour trouver le nombre maximum de fichiers ouverts, et soustrayez le nombre de fichiers que vous utilisez simultanément avec la fonction nftw() mais je n'en prendrais pas la peine (sauf si je savais que l'utilitaire serait utilisé principalement avec des structures de répertoire pathologiquement profondes). Quinze descripteurs font pas limiter la profondeur de l'arbre ; nftw() devient simplement plus lent (et peut ne pas détecter les changements dans un répertoire s'il se déplace dans un répertoire plus profond que 13 répertoires à partir de celui-ci, bien que les compromis et la capacité générale à détecter les changements varient entre les systèmes et les implémentations de la bibliothèque C). L'utilisation d'une constante de compilation comme celle-ci permet de garder le code portable -- il devrait fonctionner non seulement sous Linux, mais aussi sous Mac OS X et toutes les variantes BSD actuelles, et la plupart des autres variantes Unix pas trop anciennes.

Dans un commentaire, Ruslan a mentionné qu'ils ont dû changer pour nftw64() parce qu'ils avaient des entrées de système de fichiers qui nécessitaient des tailles/offsets de 64 bits, et la version "normale" de nftw() a échoué avec errno == EOVERFLOW . La solution correcte est de ne pas passer à des fonctions 64 bits spécifiques à GLIBC, mais de définir _LARGEFILE64_SOURCE y _FILE_OFFSET_BITS 64 . Ils indiquent à la bibliothèque C de passer aux tailles de fichiers et aux décalages 64 bits si possible, tout en utilisant les fonctions standard ( nftw() , fstat() et cetera) et les noms de type ( off_t etc.).

76voto

Lloyd Macrohon Points 1332

Voici une version récursive :

#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>

void listdir(const char *name, int indent)
{
    DIR *dir;
    struct dirent *entry;

    if (!(dir = opendir(name)))
        return;

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_type == DT_DIR) {
            char path[1024];
            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
                continue;
            snprintf(path, sizeof(path), "%s/%s", name, entry->d_name);
            printf("%*s[%s]\n", indent, "", entry->d_name);
            listdir(path, indent + 2);
        } else {
            printf("%*s- %s\n", indent, "", entry->d_name);
        }
    }
    closedir(dir);
}

int main(void) {
    listdir(".", 0);
    return 0;
}

1 votes

Doit être défini dans <dirent.h>. Sur quelle plateforme compilez-vous ceci ?

0 votes

C'est en fait le compilateur intégré à l'IDE que j'utilisais qui ne l'aimait pas, il fonctionnait bien avec GCC dans le terminal.

0 votes

Oh BTW, changez ce code pour que ce soit while ((entry = readdir(dir)) et supprimez if ( !(entry = readdir(dir)), ou au moins si readdir échoue, assurez-vous d'appeler closedir avant de retourner.

8voto

Jan Points 8207
int is_directory_we_want_to_list(const char *parent, char *name) {
  struct stat st_buf;
  if (!strcmp(".", name) || !strcmp("..", name))
    return 0;
  char *path = alloca(strlen(name) + strlen(parent) + 2);
  sprintf(path, "%s/%s", parent, name);
  stat(path, &st_buf);
  return S_ISDIR(st_buf.st_mode);
}

int list(const char *name) {
  DIR *dir = opendir(name);
  struct dirent *ent;
  while (ent = readdir(dir)) {
    char *entry_name = ent->d_name;
    printf("%s\n", entry_name);
    if (is_directory_we_want_to_list(name, entry_name)) {
      // You can consider using alloca instead.
      char *next = malloc(strlen(name) + strlen(entry_name) + 2);
      sprintf(next, "%s/%s", name, entry_name);
      list(next);
      free(next);
    }
  }
  closedir(dir);
}

Les fichiers d'en-tête méritent d'être survolés dans ce contexte : stat.h , dirent.h . Gardez à l'esprit que le code ci-dessus ne vérifie pas les erreurs qui pourraient survenir.

Une approche complètement différente est proposée par ftw défini dans ftw.h.

5voto

Myst Points 4110

Comme je l'ai mentionné dans mon commentaire, je pense qu'une approche récursive présente deux défauts inhérents à cette tâche.

La première faille est la limite des fichiers ouverts. Cette limite impose une limite à la traversée profonde. S'il y a suffisamment de sous-dossiers, l'approche récursive se brisera. ( Voir l'édition concernant le débordement de pile )

Le deuxième défaut est un peu plus subtil. L'approche récursive rend très difficile le test des liens durs. Si une arborescence de dossiers est cyclique (à cause de liens durs), l'approche récursive se cassera (avec un peu de chance sans débordement de pile). ( Voir l'édition concernant les liens durs )

Cependant, il est assez simple d'éviter ces problèmes en remplaçant la récursion par un descripteur de fichier unique et des listes liées.

Je suppose que ce n'est pas un projet scolaire et que la récursion est facultative.

Voici un exemple d'application.

Utilisez a.out ./ pour afficher l'arborescence des dossiers.

Je m'excuse pour les macros et autres... J'utilise habituellement des fonctions en ligne, mais j'ai pensé qu'il serait plus facile de suivre le code si tout était dans une seule fonction.

#include <dirent.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

int main(int argc, char const *argv[]) {
  /* print use instruction unless a folder name was given */
  if (argc < 2)
    fprintf(stderr,
            "\nuse:\n"
            "    %s <directory>\n"
            "for example:\n"
            "    %s ./\n\n",
            argv[0], argv[0]),
        exit(0);

  /*************** a small linked list macro implementation ***************/

  typedef struct list_s {
    struct list_s *next;
    struct list_s *prev;
  } list_s;

#define LIST_INIT(name)                                                        \
  { .next = &name, .prev = &name }

#define LIST_PUSH(dest, node)                                                  \
  do {                                                                         \
    (node)->next = (dest)->next;                                               \
    (node)->prev = (dest);                                                     \
    (node)->next->prev = (node);                                               \
    (dest)->next = (node);                                                     \
  } while (0);

#define LIST_POP(list, var)                                                    \
  if ((list)->next == (list)) {                                                \
    var = NULL;                                                                \
  } else {                                                                     \
    var = (list)->next;                                                        \
    (list)->next = var->next;                                                  \
    var->next->prev = var->prev;                                               \
  }

  /*************** a record (file / folder) item type ***************/

  typedef struct record_s {
    /* this is a flat processing queue. */
    list_s queue;
    /* this will list all queued and processed folders (cyclic protection) */
    list_s folders;
    /* this will list all the completed items (siblings and such) */
    list_s list;
    /* unique ID */
    ino_t ino;
    /* name length */
    size_t len;
    /* name string */
    char name[];
  } record_s;

/* take a list_s pointer and convert it to the record_s pointer */
#define NODE2RECORD(node, list_name)                                           \
  ((record_s *)(((uintptr_t)(node)) -                                          \
                ((uintptr_t) & ((record_s *)0)->list_name)))

/* initializes a new record */
#define RECORD_INIT(name)                                                      \
  (record_s){.queue = LIST_INIT((name).queue),                                 \
             .folders = LIST_INIT((name).folders),                             \
             .list = LIST_INIT((name).list)}

  /*************** the actual code ***************/

  record_s records = RECORD_INIT(records);
  record_s *pos, *item;
  list_s *tmp;
  DIR *dir;
  struct dirent *entry;

  /* initialize the root folder record and add it to the queue */
  pos = malloc(sizeof(*pos) + strlen(argv[1]) + 2);
  *pos = RECORD_INIT(*pos);
  pos->len = strlen(argv[1]);
  memcpy(pos->name, argv[1], pos->len);
  if (pos->name[pos->len - 1] != '/')
    pos->name[pos->len++] = '/';
  pos->name[pos->len] = 0;
  /* push to queue, but also push to list (first item processed) */
  LIST_PUSH(&records.queue, &pos->queue);
  LIST_PUSH(&records.list, &pos->list);

  /* as long as the queue has items to be processed, do so */
  while (records.queue.next != &records.queue) {
    /* pop queued item */
    LIST_POP(&records.queue, tmp);
    /* collect record to process */
    pos = NODE2RECORD(tmp, queue);
    /* add record to the processed folder list */
    LIST_PUSH(&records.folders, &pos->folders);

    /* process the folder and add all folder data to current list */
    dir = opendir(pos->name);
    if (!dir)
      continue;

    while ((entry = readdir(dir)) != NULL) {

      /* create new item, copying it's path data and unique ID */
      item = malloc(sizeof(*item) + pos->len + entry->d_namlen + 2);
      *item = RECORD_INIT(*item);
      item->len = pos->len + entry->d_namlen;
      memcpy(item->name, pos->name, pos->len);
      memcpy(item->name + pos->len, entry->d_name, entry->d_namlen);
      item->name[item->len] = 0;
      item->ino = entry->d_ino;
      /* add item to the list, right after the `pos` item */
      LIST_PUSH(&pos->list, &item->list);

      /* unless it's a folder, we're done. */
      if (entry->d_type != DT_DIR)
        continue;

      /* test for '.' and '..' */
      if (entry->d_name[0] == '.' &&
          (entry->d_name[1] == 0 ||
           (entry->d_name[1] == '.' && entry->d_name[2] == 0)))
        continue;

      /* add folder marker */
      item->name[item->len++] = '/';
      item->name[item->len] = 0;

      /* test for cyclic processing */
      list_s *t = records.folders.next;
      while (t != &records.folders) {
        if (NODE2RECORD(t, folders)->ino == item->ino) {
          /* we already processed this folder! */
          break; /* this breaks from the small loop... */
        }
        t = t->next;
      }
      if (t != &records.folders)
        continue; /* if we broke from the small loop, entry is done */

      /* item is a new folder, add to queue */
      LIST_PUSH(&records.queue, &item->queue);
    }
    closedir(dir);
  }

  /*************** Printing the results and cleaning up ***************/
  while (records.list.next != &records.list) {
    /* pop list item */
    LIST_POP(&records.list, tmp);
    /* collect and process record */
    pos = NODE2RECORD(tmp, list);
    fwrite(pos->name, pos->len, 1, stderr);
    fwrite("\n", 1, 1, stderr);
    /* free node */
    free(pos);
  }
  return 0;
}

EDIT

@Stargateur a mentionné dans les commentaires que le code récursif va probablement déborder de la pile avant d'atteindre la limite du fichier ouvert.

Bien que je ne voie pas comment un dépassement de pile est meilleur, cette évaluation est probablement correcte tant que le processus n'est pas proche de la limite du fichier lorsqu'il est invoqué.

Un autre point mentionné par @Stargateur dans les commentaires est que la profondeur du code récursif est limitée par le nombre maximum de sous-répertoires (64000 sur le système de fichiers ext4) et que les liens durs sont extrêmement improbables (puisque les liens durs vers les dossiers ne sont pas autorisés sous Linux/Unix).

C'est une bonne nouvelle si le code est exécuté sous Linux (ce qui est le cas, d'après la question), donc ce problème n'est pas un réel souci (à moins d'exécuter le code sous macOS ou, peut-être, Windows)... bien que 64K sous-dossiers en récursivité puissent faire exploser la pile.

Cela dit, l'option non récursive présente tout de même des avantages, comme la possibilité d'ajouter facilement une limite au nombre d'éléments traités, ainsi que la possibilité de mettre le résultat en cache.

P.S.

Selon les commentaires, voici une version non récursive du code qui ne vérifie pas les hiérarchies cycliques. C'est plus rapide et devrait être suffisamment sûr pour être utilisé sur une machine Linux où les liens directs vers les dossiers ne sont pas autorisés.

#include <dirent.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

int main(int argc, char const *argv[]) {
  /* print use instruction unless a folder name was given */
  if (argc < 2)
    fprintf(stderr,
            "\nuse:\n"
            "    %s <directory>\n"
            "for example:\n"
            "    %s ./\n\n",
            argv[0], argv[0]),
        exit(0);

  /*************** a small linked list macro implementation ***************/

  typedef struct list_s {
    struct list_s *next;
    struct list_s *prev;
  } list_s;

#define LIST_INIT(name)                                                        \
  { .next = &name, .prev = &name }

#define LIST_PUSH(dest, node)                                                  \
  do {                                                                         \
    (node)->next = (dest)->next;                                               \
    (node)->prev = (dest);                                                     \
    (node)->next->prev = (node);                                               \
    (dest)->next = (node);                                                     \
  } while (0);

#define LIST_POP(list, var)                                                    \
  if ((list)->next == (list)) {                                                \
    var = NULL;                                                                \
  } else {                                                                     \
    var = (list)->next;                                                        \
    (list)->next = var->next;                                                  \
    var->next->prev = var->prev;                                               \
  }

  /*************** a record (file / folder) item type ***************/

  typedef struct record_s {
    /* this is a flat processing queue. */
    list_s queue;
    /* this will list all the completed items (siblings and such) */
    list_s list;
    /* unique ID */
    ino_t ino;
    /* name length */
    size_t len;
    /* name string */
    char name[];
  } record_s;

/* take a list_s pointer and convert it to the record_s pointer */
#define NODE2RECORD(node, list_name)                                           \
  ((record_s *)(((uintptr_t)(node)) -                                          \
                ((uintptr_t) & ((record_s *)0)->list_name)))

/* initializes a new record */
#define RECORD_INIT(name)                                                      \
  (record_s){.queue = LIST_INIT((name).queue), .list = LIST_INIT((name).list)}

  /*************** the actual code ***************/

  record_s records = RECORD_INIT(records);
  record_s *pos, *item;
  list_s *tmp;
  DIR *dir;
  struct dirent *entry;

  /* initialize the root folder record and add it to the queue */
  pos = malloc(sizeof(*pos) + strlen(argv[1]) + 2);
  *pos = RECORD_INIT(*pos);
  pos->len = strlen(argv[1]);
  memcpy(pos->name, argv[1], pos->len);
  if (pos->name[pos->len - 1] != '/')
    pos->name[pos->len++] = '/';
  pos->name[pos->len] = 0;
  /* push to queue, but also push to list (first item processed) */
  LIST_PUSH(&records.queue, &pos->queue);
  LIST_PUSH(&records.list, &pos->list);

  /* as long as the queue has items to be processed, do so */
  while (records.queue.next != &records.queue) {
    /* pop queued item */
    LIST_POP(&records.queue, tmp);
    /* collect record to process */
    pos = NODE2RECORD(tmp, queue);

    /* process the folder and add all folder data to current list */
    dir = opendir(pos->name);
    if (!dir)
      continue;

    while ((entry = readdir(dir)) != NULL) {

      /* create new item, copying it's path data and unique ID */
      item = malloc(sizeof(*item) + pos->len + entry->d_namlen + 2);
      *item = RECORD_INIT(*item);
      item->len = pos->len + entry->d_namlen;
      memcpy(item->name, pos->name, pos->len);
      memcpy(item->name + pos->len, entry->d_name, entry->d_namlen);
      item->name[item->len] = 0;
      item->ino = entry->d_ino;
      /* add item to the list, right after the `pos` item */
      LIST_PUSH(&pos->list, &item->list);

      /* unless it's a folder, we're done. */
      if (entry->d_type != DT_DIR)
        continue;

      /* test for '.' and '..' */
      if (entry->d_name[0] == '.' &&
          (entry->d_name[1] == 0 ||
           (entry->d_name[1] == '.' && entry->d_name[2] == 0)))
        continue;

      /* add folder marker */
      item->name[item->len++] = '/';
      item->name[item->len] = 0;

      /* item is a new folder, add to queue */
      LIST_PUSH(&records.queue, &item->queue);
    }
    closedir(dir);
  }

  /*************** Printing the results and cleaning up ***************/
  while (records.list.next != &records.list) {
    /* pop list item */
    LIST_POP(&records.list, tmp);
    /* collect and process record */
    pos = NODE2RECORD(tmp, list);
    fwrite(pos->name, pos->len, 1, stderr);
    fwrite("\n", 1, 1, stderr);
    /* free node */
    free(pos);
  }
  return 0;
}

4voto

chqrlie Points 17105

Voici une version simplifiée qui est récursive mais qui utilise beaucoup moins d'espace sur la pile :

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>

void listdir(char *path, size_t size) {
    DIR *dir;
    struct dirent *entry;
    size_t len = strlen(path);

    if (!(dir = opendir(path))) {
        fprintf(stderr, "path not found: %s: %s\n",
                path, strerror(errno));
        return;
    }

    puts(path);
    while ((entry = readdir(dir)) != NULL) {
        char *name = entry->d_name;
        if (entry->d_type == DT_DIR) {
            if (!strcmp(name, ".") || !strcmp(name, ".."))
                continue;
            if (len + strlen(name) + 2 > size) {
                fprintf(stderr, "path too long: %s/%s\n", path, name);
            } else {
                path[len] = '/';
                strcpy(path + len + 1, name);
                listdir(path, size);
                path[len] = '\0';
            }
        } else {
            printf("%s/%s\n", path, name);
        }
    }
    closedir(dir);
}

int main(void) {
    char path[1024] = ".";
    listdir(path, sizeof path);
    return 0;
}

Sur mon système, sa sortie est exactement identique à celle de find .

0 votes

Chqrlie répond à Charlie, c'est confus :p.

0 votes

@Stargateur : oui, j'ai remarqué cela aussi, mais sans aucun rapport.

0 votes

Le chemin est toujours trop long sur mac

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X