Sauvegarde générale "GNU/Linux" (script bash)

Contenu du snippet

#!/bin/bash

# initié le 5/14
# 
# conditions initiales pour le bon fonctionnement du script:
#  -sdX -> ddi source
#  -sdY -> dde destinataire
#  -partition DOCUMENTS (inclu dans sdX) est deja montee
#  -partition SAUVEGARDES (inclu dans sdY) n'est pas encore montee
#  -les programmes wmctrl et countdown sont deja installes
#  -on considere que les clefs usb n'ont qu'une partition chacune
# 
# fonctionnalitées:
#   -eteint la machine à la fin de la sauvegarde
#   -sauvegarde part docs + sys + 1ere part de clefs clefs_usb
#   -grace à renice, ne prend pas trop de ressource
#   -plusieurs clef usb peuvent etre connectees a la fois

# TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM
# prog_name=$(basename $0)

# exemple :
# date="1451054767_14h46_le_ven._25_déc._2015"
# fait="1"
# ismount="0"
# isroot="0"
# isconf="0"
# home_user_dir="/home_user_dir/$user"
# errors_file="$home_user_dir/erreurs_backup_$date.txt"
# dest_dir="/run/media/$user/SAUVEGARDES"
# dir_tmp="$home_user_dir/tmp"
# uuid_docs="8105f762-e3e8-419b-a1a1-08ef7cc5df85"
# uuid_win="96A46206A461E967"
# uuid_dest="8455f762-e3e8-419b-a1a1-08cd7cc5df85"
# mount_point_dest="/dev/sdb1"
# mount_point_docs="/dev/sda1"
# mount_point_win="/dev/sda3"
# docs_dir="/run/media/robert/DOCUMENTS"
# backup_dir="$docs_dir/Documents/backup"

#programmes utilisés :
# wmctrl rsync tree espeak

# diff_time()
# {
#  echo "rien"
# }

pause()
{
 read -r -p "$*"
}

#function for handling errors:
error_exit()
{
 local parent_lineno message code
 message="$1"
 
 if [ -n "$2" ]; then
  code="$2"
 else
  code="1 (default)"
 fi
 
 if [ -n "$3" ]; then
  parent_lineno="$3"
 else
  parent_lineno="?"
 fi
 
 if [ -n "$message" ]; then
  echo "Error on or near line ${parent_lineno} : ${message}; exiting with status ${code}." > "$errors_file"
 else
  echo "Error on or near line ${parent_lineno}; exiting with status ${code}." > "$errors_file"
 fi
 
 sleep "3"
 
 exit "${code}"
}

clean_up()
{
 local code
 
 if [ -n "$1" ]; then
  code="$1"
 else
  code="0"
 fi
 
 if [ -n "$home_user_dir" ] && [ -d "$dir_tmp" ]; then
  rm -R "$dir_tmp"
 fi
 
 sleep "3"
 
 exit "$code"
}

#attention!!! fonction recursive!
mount_part()
{
#  if [ ! "$(mount | grep $3)" ]
 if mount | grep -q "$3"; then
  echo "Remontage de : $3"
  umount_part "$1" "$3"
  mount_part "$1" "$2" "$3"
 else
  echo "Création du dossier qui servira a monter la partition $1 puis montage de cette dernière."
  if [ ! -d "$2" ]; then
   mkdir "$2" || error_exit "Cannot create directory! Aborting." "$?"
  fi
  mount "$3" "$2" || error_exit "Cannot mount filesystem! Aborting." "$?"
 fi
#sudo mount -t $4 $3 $2 || error_exit "Cannot mount filesystem! Aborting.." 32
}

umount_part()
{
 echo "Démontage de la partition $1 puis suppression du dossier qui servit à monter cette dernière."
#  umount -t $(mount | grep "$2" | awk '{print $5}') $2 || error_exit "Cannot umount filesystem! Aborting.." "$?"
 umount "$2" || error_exit "Cannot umount filesystem! Aborting." "$?"
 if [ -d "$2" ]; then
  rmdir "$2"
 fi
}

sudo_rm()
{
 su "$user" -c "/usr/bin/wmctrl -a Konsole"
 echo -e "�33[1;31mLe dossier : "$1" sera supprimé dans 15 secondes !�33[0m"
 sleep 15
 rm -R "$1"
}

#MBR:Master Boot Record
save_MBR()
{
 check_mount "$mount_point_dest" "Sauvegarde des mbr avortée : le dd de destination n'est pas connecté !"
 check_key
 if [ "$ismount" -eq "1" ] && [ "$iskey" -eq "1" ]; then
  echo "Sauvegarde des MBR des dd contenants les partions DOCUMENTS & SAUVEGARDES."
  
  local ddi=${mount_point_docs:0:8}
  local dde=${mount_point_dest:0:8}
  
  rm -Rf "$backup_dir"/mbr/
  rm -Rf "$backup_dir"/table_des_partitions/
  
  check_dir "$backup_dir"/mbr/
  check_dir "$backup_dir"/table_des_partitions/

  for ddx in $ddi $dde
  do
   local name=""
   name="$(ls -l /dev/disk/by-id/ | grep -v part | grep "${ddx:5}" | awk '{print $9}' | head -1)_$date"
   sfdisk -d "$ddx" > "$backup_dir"/table_des_partitions/"${name}".dmp > /dev/null 2>&1
   dd if="$ddx" of="$backup_dir"/mbr/"${name}" bs=512 count=1 > /dev/null 2>&1
  done
  
  tar cP "$backup_dir/mbr/" "$backup_dir/table_des_partitions/" | gzip > "$backup_dir/mbr_$date.tar.gzip"
  su "$user" -c "gpg -r $user -e -a $backup_dir/mbr_$date.tar.gzip"
  rm "$backup_dir/mbr_$date.tar.gzip"
  
  sed -i "1iSubject: sauvegarde des mbr et cie" "$backup_dir/mbr_$date.tar.gzip.asc"
  sed -i "1iTo: "truc" <roberto@alagna>" "$backup_dir/mbr_$date.tar.gzip.asc"
  sed -i "1iFrom: "monchu" <coucou@laposte.net>" "$backup_dir/mbr_$date.tar.gzip.asc"
  
  curl --url "smtps://smtp.laposte.net:465" --ssl-reqd --mail-from "coucou@laposte.net" --mail-rcpt "coucou@laposte.net" --upload-file "$backup_dir/mbr_$date.tar.gzip.asc" --user "coucou@laposte.net:GmlesBytes"
 fi
}

save_DOCUMENTS()
{
 check_existence "$mount_point_docs" "les documents"
 if [ "$ishere" -eq "1" ]; then
  local delta dir tmp today file_tmp
  
  let "delta=60*60*24*28"
  today="$date"
  file_tmp="$dir_tmp/list_of_dir.$$.$RANDOM"
  
  check_dir "$dest_dir"/docs_supprimes/"$today"/
  check_dir "$dest_dir"/logs/
  check_dir "$dir_tmp"
  
  echo "Sauvegarde du contenu de la partition DOCUMENTS."
  rsync -hau --delete --backup --backup-dir="$dest_dir"/docs_supprimes/"$today"/ --stats --exclude=**/lost+found*/ --exclude=**/*Trash*/ --exclude=**/*trash*/ "$docs_dir/" "$dest_dir/documents_perso/" > "$dest_dir/logs/documents_perso_$today" || error_exit "Cannot use rsync ! Aborting." "$?"
  #    sudo rsync -hau --progress --delete --backup --backup-dir=$dest_dir/docs_supprimes/$today/ --stats --exclude=**/lost+found*/ --exclude=**/*Trash*/ --exclude=**/*trash*/ $docs_dir/ $dest_dir/documents_perso/ || error_exit "Cannot use rsync! Aborting.." "$?"
  ls "$dest_dir/docs_supprimes/" > "$file_tmp" || error_exit "Cannot use ls ! Aborting.." "$?"
  if [ -s "$file_tmp" ]; then
   cd "$dest_dir/docs_supprimes/" || error_exit "Cannot use cd ! Aborting.." "$?"
   while read -r dir; do
    if [ -n  "${dir%%_*}" ]; then
     let "tmp=$(date +%s)-${dir%%_*}" || error_exit "Cannot use let ! Aborting.." "$?"
    else
     let "tmp=$delta+1" || error_exit "Cannot use let ! Aborting." "$?"
    fi
    
    if [ "$tmp" -gt "$delta" ]; then
     sudo_rm "$dir"
    fi
   done < "$file_tmp"
  fi
  rm "$file_tmp"
 #  tree -Cqav "$docs_dir/" > "$dest_dir/documents_perso/tree.txt"
 fi
}

save_sys_win()
{
 check_existence "$mount_point_win" "Windows"
 if [ "$ishere" -eq "1" ]; then
  check_dir "$dest_dir/systemes/"
  check_dir "$dest_dir/logs/"

  mount_part "WINDOWS" "/mnt/WINDOWS/" "$mount_point_win"
  
  echo "Sauvegarde du système Windows actuel."
  rsync -hau --delete --stats /mnt/WINDOWS/ "$dest_dir/systemes/win" > "$dest_dir/logs/systeme_win_${date}" || error_exit "Cannot use rsync ! Aborting." "$?"
  
 #  tree -Cqav "/mnt/WINDOWS/" > "$dest_dir/systemes/win/tree.txt"
  
  umount_part "WINDOWS" "/mnt/WINDOWS/"
 fi
}

save_sys_lin()
{
 check_existence "$mount_point_lin" "un système GNU/Linux"
 if [ "$ishere" -eq "1" ]; then
  local date_tmp tmp delta former_distrib_name
  check_dir "$dest_dir/systemes/lin/"
  check_dir "$dest_dir/logs/"
  
  echo "Sauvegarde du système GNU/Linux."

  let "delta=60*60*24*29"
  #ls range par def par ordre croissant ?
  date_tmp="$(ls "$dest_dir/logs/" | grep lin_ | head -1)"
  if [ -n  "$date_tmp" ]; then
   date_tmp="${date_tmp##*lin_}"
   date_tmp="${date_tmp%%_*}"
   let "tmp=$(date +%s)-$date_tmp" || error_exit "Cannot use let ! Aborting.." "$?"
  else
   let "tmp=$delta+1" || error_exit "Cannot use let ! Aborting." "$?"
  fi
  
  if [ "$tmp" -gt "$delta" ]; then
   sudo_rm "$dest_dir"/systemes/lin/
   mkdir -p "$dest_dir"/systemes/lin/{mnt,tmp,proc,media,dev,sys}
   mv "$dest_dir"/logs/*lin_* "$dest_dir"/logs_old/
  fi
  
 #  pacman -Qqe | grep -vx "$(pacman -Qqm)" > "$backup_dir/paquets_installés.txt"
 #  tar -cjf "$backup_dir/pacman-database.tar.bz2" "/var/lib/pacman/local"
  #"$backup_dir/pacman-database.tar.bz2"
  
  rsync -auHX --delete --stats --numeric-ids --exclude=/media --exclude="/run/media/$user" --exclude=/tmp --exclude=/dev --exclude=/proc --exclude=/sys --exclude=/mnt --exclude=**/lost+found*/ --exclude=**/*Trash*/ --exclude=**/*trash*/ / "$dest_dir/systemes/lin" >> "$dest_dir/logs/systeme_lin_${date}"
  
 #  tree -Cqav "/" > "$dest_dir/systemes/lin/tree.txt"
  blkid > "$dest_dir/logs/partitionnement_ddi_(sudo_blkid).txt" || error_exit "Cannot use blkid ! Aborting." "$?"
  fdisk -l > "$dest_dir/logs/sudo_fdisk_-l.txt" || error_exit "Cannot use fdisk ! Aborting." "$?"
 fi
}

save_usb_key()
{
 if [ "$isroot" -eq "1" ]; then
  local file_tmp="$dir_tmp/mount_point_key.$$.$RANDOM"
  local dir_key_source="$home_user_dir/key"
  local dir_key_dest=""
  local id_key=""
  local t=""
  
  check_dir "$dir_tmp"
  
  for device in "usb" "mmc"; do
   ls /dev/disk/by-id/*"$device"*  > /dev/null 2>&1
   if [ "$?" -eq "0" ]
   then
    ls /dev/disk/by-id/*"$device"* | grep part1 | grep -v TOSHIBA_External_USB_3.0 > "$file_tmp"
    if [ -s "$file_tmp" ]
    then
     while read -r mount_point_key; do
      id_key=${mount_point_key##*/}
      dir_key_dest="$backup_dir/clefs_usb/${id_key}"
      t=$(ls -l "/dev/disk/by-id/$id_key")
      t="/dev/${t##*/}"
      check_dir "$dir_key_dest"
      mount_part "de la clef $device" "$dir_key_source/" "$t"
      echo "Sauvegarde du contenu de la partition $t de la clef $device."
      rsync -hau --delete-after --exclude=**/lost+found*/ --exclude=**/*Trash*/ --exclude=**/*trash*/ --exclude=**/_ISO --exclude=**/*usique* "$dir_key_source/" "$dir_key_dest" > "$dir_key_dest/log_$date"
      tree -Cqav "$dir_key_source/" > "$dir_key_dest/tree.txt"
      umount_part "de la clef $device" "$dir_key_source/"
     done < "$file_tmp"
     rm "$file_tmp"
    fi
   else
    echo "Aucun périphérique $device n'est connecté !"
   fi
  done
 fi
}

save_speedy()
{
 check_key
 if [ "$iskey" -eq "1" ]; then
  local file_list="$backup_dir/list.txt"
  local file_tmp="$dir_tmp/list.$$.$RANDOM"
  local output="$backup_dir/save_speedy-${date}.tar.gzip"
  
  check_dir "$dir_tmp"
  
  while read -r dir; do
   if [ -e "$dir" ]; then
    echo "$dir" >> "$file_tmp"
   elif [ -e "$docs_dir$dir" ]; then
    echo "$docs_dir$dir" >> "$file_tmp"
   else
    echo -e "e[1;31m$dir n'existe pas !e[0m"
   fi
  done < "$file_list"
  
  tree -Cqav "$docs_dir/" > "$backup_dir/tree.txt"
  tar -czP --atime-preserve -T "$file_tmp" -f "$output"
  su "$user" -c "gpg -r $user -e $output"
  rm "$output" "$file_tmp"
  su "$user" -c "/usr/bin/espeak -v fr "La sauvegarde rapide s'est terminée.""
  sleep "5"
 fi
}

shutdown_()
{
 if [ "$isroot" -eq "1" ]; then
  local mes="Extinction de l'ordinateur dans 30 secondes !"
  local tmp=""
  tmp="$(wmctrl -l | grep -im 1 "terminal")"
  tmp="${tmp:0:10}"
  su "$user" -c "/usr/bin/wmctrl -ia $tmp"
#   su "$user" -c "/usr/bin/notify-send -i /usr/share/icons/oxygen/32x32/actions/system-shutdown.png backup.sh "$mes""
  echo -e "e[1;31m$mese[0m"
  su "$user" -c "/usr/bin/espeak -v fr "$mes""
  read -r -n 1 -t 30 test
  if [ -z  "$test" ]; then
   shutdown -ah now
  fi
 fi
}

show_help()
{
 echo "Aide :"
 echo " -k sauvegarde du contenu des clefs usb avec suppressions des fichiers de destinations qui ne sont pas à la source"
 echo " -s sauvegarde des documents précieux puis compression et chiffrement de ces derniers"
 echo " -m sauvegarde des MBR uniquement"
 echo " -d sauvegarde des documents uniquement"
 echo " -l sauvegarde du système GNU/Linux uniquement"
 echo " -w sauvegarde du système Windows uniquement"
 echo " -x exécution de shutdown -ah now"
 echo "sinon, tout en même temps et dans l'ordre"
 echo " -h cette aide"
#  echo " -r /dev/sdXY restauration du système"
}

restore_systeme()
{
#cf : "[https://wiki.archlinux.fr/Enhancing_Arch_Linux_Stability#.C3.89vitez_certaines_commandes_avec_pacman]"
 local answer=""
 
 check_dir "$dest_dir/logs/"
 
 echo "Restauration du système sauvegarde sur $1."
 echo "Voulez-vous supprimer le contenu de $1 ? [y/n]"
 read -r answer
 if [ "$answer" = "Y" ] || [ "$answer" = "y" ]; then
  rm -R "${1:?}/"
  rsync -ha --stats --numeric-ids "$dest_dir/systemes/" "$1" > "$dest_dir/logs/restore_system_${date}.txt"
 fi
 
 xargs -a "$backup_dir/paquets_installés.txt" pacman -S --needed
 tar -xjvf "$backup_dir/pacman-database.tar.bz2" -C /
}

restore()
{
 part_name=$(blkid "$OPTARG")
 part_name=${part_name##*LABEL="}
 part_name=${part_name%%"*}
 
 if  $OPTARG != /dev/sd* ; then
  echo "L'argument doit être du type "sdXY", avec X et Y respectivement une lettre de l'alphabet et un chiffre !"
  clean_up "0"
 fi
 echo "-- restauration du système sur la partition $part_name --"
 mount_part "SAUVEGARDES" "$dest_dir/" "$mount_point_dest"
 mount_part "$part_name" "/media/RESTORE/" "$OPTARG"
 restore_systeme "/media/RESTORE"
 umount_part "$part_name" "/media/RESTORE/"
 umount_part "SAUVEGARDES" "$dest_dir/"
 unset part_name
}

check_dir()
{
 if [ ! -d "$1" ]; then
  mkdir -p "$1" || error_exit "Cannot create directory ! Aborting." "$?"
 fi
}

check_space()
{
 df | grep "$1" | awk '{ print $4; }'
}

check_root()
{
 if  "$EUID" -ne "0" ; then
  echo "Must be run as root for $1."
 else
  let "isroot=1"
 fi
}

check_existence()
{
 if [ "$1" = "NULL" ]; then
  ishere="0"
  echo "La partition contenant $2 n'existe pas !"
 else
  ishere="1"
 fi
}

check_key()
{
 su "$user" -c "gpg --list-keys $user > /dev/null 2>&1"
 if [ "$?" -eq "0" ]; then
  iskey="1"
 else
  iskey="0"
  echo "Il n'existe pas de clefs gpg pour chiffrer les données !"
 fi
}

#if device recipient is plug
check_mount()
{
 if [ -n "$1" ]; then
  let "ismount=1"
 else
  echo "$2"
  let "fait=0"
 fi
}

manage_saves()
{
 check_mount "$mount_point_dest" "Sauvegarde des systèmes et/ou documents avortée : le dd de destination n'est pas connecté !"
 if [ "$ismount" -eq "1" ] && [ "$isroot" -eq "1" ]; then
  mount_part "SAUVEGARDES" "$dest_dir/" "$mount_point_dest"
  case "$1" in
   DOCS)
    save_DOCUMENTS
    ;;
   SYS_LIN)
    save_sys_lin
    ;;
   SYS_WIN)
    save_sys_win
    ;;
   ALL)
    save_DOCUMENTS
    save_sys_win
    save_sys_lin
    ;;
  esac;
  cd "$HOME" || error_exit "Cannot use cd ! Aborting." "$?"
  umount_part "SAUVEGARDES" "$dest_dir/"
 fi
}

install_programs()
{
 for prog in $*
 do
  type "$prog" > /dev/null 2>&1 || echo "Veuillez installer $prog."
 done
}

load_conf()
{
 install_programs tree wmctrl espeak rsync

 if [ ! -e "/etc/backup.conf" ]
 then
  echo "Voici les partitions disponibles :"
  blkid -o list
  echo -e -n "Entrer le nom de l'utilisateur :nt->"
  read -r user
  echo -e -n "Entrer l'uuid de la partition contenant les documents (NULL si inexistant) :nt->"
  read -r uuid_docs
  echo -e -n "Entrer l'uuid de la partition contenant le système Windows (NULL si inexistant) :nt->"
  read -r uuid_win
  echo -e -n "Entrer l'uuid de la partition où tout sera sauvegardé :nt->"
  read -r uuid_dest
  echo -e -n "Entrer l'uuid d'une partition contenant un système GNU/Linux (NULL si inexistant) :nt->"
  read -r uuid_lin
  
  if [ "$uuid_docs" = "NULL" ]; then
   mount_point_docs="NULL"
  else
   mount_point_docs="$(blkid -U "$uuid_docs")"
  fi
  
  if [ "$uuid_win" = "NULL" ]; then
   mount_point_win="NULL"
  else
   mount_point_win="$(blkid -U "$uuid_win")"
  fi
  
  if [ "$uuid_lin" = "NULL" ]; then
   mount_point_lin="NULL"
  else
   mount_point_lin="$(blkid -U "$uuid_lin")"
  fi
  
  echo "$user $mount_point_docs $mount_point_win $uuid_dest $mount_point_lin" > "/etc/backup.conf" || error_exit "Cannot write in file ! Aborting." "$?"
 else
  read -r user mount_point_docs mount_point_win uuid_dest uuid_lin < "/etc/backup.conf" || error_exit "Cannot read in file ! Aborting." "$?"
 fi
 
 mount_point_dest=$(blkid -U "$uuid_dest")
 
 docs_dir=$(mount | grep "$mount_point_docs" | awk '{print $3 }')
 sys_lin_dir=$(mount | grep "$mount_point_lin" | awk '{print $3 }')
 dest_dir="/media/$user/SAUVEGARDES"
 backup_dir="$docs_dir/Documents/backup"
 home_user_dir="/home/$user"

 errors_file="$home_user_dir/erreurs_backup_$date.txt"
 # logs="/run/media/$user/SAUVEGARDES"

 check_dir "$backup_dir"

 let "isconf=1"
}

trap clean_up SIGHUP SIGINT SIGTERM

isroot="0"

check_root "launch this script"
if [ "$isroot" -eq "0" ]; then
 show_help
 exit "0"
fi

user=""
date="$(date +%s_%kh%M_le_%a_%d_%b_%Y)"
mount_point_dest=""
mount_point_docs=""
uuid_docs=""
uuid_win=""
uuid_lin=""
uuid_dest=""
fait="1"
ismount="0"
ishere="0"
iskey="0"
#à utiliser un jour...
isconf="0"

load_conf

dir_tmp="$home_user_dir/tmp"

# On donne une priorité faible au processus pour ne pas dégrader les performances des autres applications
renice +19 $$ > /dev/null 2>&1

#  while getopts "lwdmhxr:kKs" opt
while getopts "lwdmhxks" opt
do
 case $opt in
  l)
   echo "-- sauvegarde du système GNU/Linux uniquement --"
   manage_saves "SYS_LIN"
   ;;
  w)
   echo "-- sauvegarde du système Windows uniquement --"
   manage_saves "SYS_WIN"
   ;;
  d)
   echo "-- sauvegarde des documents uniquement --"
   manage_saves "DOCS"
   ;;
  m)
   echo "-- sauvegarde des MBR uniquement --"
   save_MBR
   ;;
  h)
   show_help
   clean_up 0
   ;;
  x)
   shutdown_
   ;;
  k)
   echo "-- sauvegarde des clefs usb et mmc uniquement --"
   save_usb_key
   ;;
  s)
   echo "-- sauvegarde rapide --"
   save_speedy
   ;;
  ?)
   echo "invalid option: -$OPTARG"
   show_help
   clean_up "0"
   ;;
#   r)
#    restore
#    ;;
 esac
done

if [ "$#" -eq "0" ]; then
 echo "-- sauvegarde générale --"
 save_usb_key
 save_MBR
 save_speedy
 manage_saves "ALL"
 if [ "$fait" -eq "1" ]; then
  shutdown_
 fi
fi

sleep "3" > /dev/null 2>&1

clean_up "1"

# from "[http://unix.stackexchange.com/questions/92199/how-can-i-reliably-get-the-operating-systems-name]"
# know_name_distrib()
# {
#  # Determine OS platform
#  local UNAME=$(uname | tr "[:upper:]" "[:lower:]")
#  # If Linux, try to determine specific distribution
#  if [ "$UNAME" == "linux" ]; then
#   # If available, use LSB to identify distribution
#   if [ -f /etc/lsb-release -o -d /etc/lsb-release.d ]; then
#    export DISTRO=$(lsb_release -i | cut -d: -f2 | sed s/'^t'//)
#   # Otherwise, use release info file
#   else
#    export DISTRO=$(ls -d /etc/[A-Za-z]*[_-][rv]e[lr]* | grep -v "lsb" | cut -d'/' -f3 | cut -d'-' -f1 | cut -d'_' -f1)
#   fi
#  fi
#  # For everything else (or if above failed), just use generic identifier
#  [ "$DISTRO" == "" ] && export DISTRO=$UNAME
#  unset UNAME
#  
#  echo "$DISTRO"
# }

Compatibilité : 0.1

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.