Est-il possible de synchroniser la structure du répertoire lorsque les fichiers sont déjà des deux côtés?

24

J'ai deux disques avec les mêmes fichiers, mais la structure du répertoire est totalement différente.

Existe-t-il un moyen de «déplacer» tous les fichiers du côté destination afin qu'ils correspondent à la structure du côté source? Avec un script peut-être?

Par exemple, le lecteur A a:

/foo/bar/123.txt
/foo/bar/234.txt
/foo/bar/dir/567.txt

Alors que le lecteur B a:

/some/other/path/123.txt
/bar/doo2/wow/234.txt
/bar/doo/567.txt

Les fichiers en question sont énormes (800 Go), donc je ne veux pas les recopier; Je veux juste synchroniser la structure en créant les répertoires nécessaires et en déplaçant les fichiers.

Je pensais à un script récursif qui trouverait chaque fichier source sur la destination, puis le déplacerait dans un répertoire correspondant, le créant si nécessaire. Mais - cela dépasse mes capacités!

Une autre solution élégante a été donnée ici: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086

Dan
la source
Êtes-vous sûr que le nom détermine uniquement le contenu d'un fichier, sinon vous devriez envisager de comparer les fichiers par leur somme de contrôle.
kasterma

Réponses:

11

Je vais avec Gilles et vous montrerai Unison comme suggéré par hasen j . Unison était DropBox 20 ans avant DropBox. Un code solide que beaucoup de gens (moi y compris) utilisent tous les jours - très intéressant à apprendre. Pourtant, il a joinbesoin de toute la publicité qu'il peut obtenir :)


Ce n'est qu'une demi-réponse, mais je dois retourner au travail :)

Fondamentalement, je voulais démontrer l' joinutilitaire peu connu qui fait exactement cela: joint deux tables sur un certain champ.

Tout d'abord, configurez un scénario de test comprenant des noms de fichiers avec des espaces:

for d in a b 'c c'; do mkdir -p "old/$d"; echo $RANDOM > "old/${d}/${d}.txt"; done
cp -r old new

(modifiez des noms de répertoires et / ou de fichiers dans new).

Maintenant, nous voulons construire une carte: hachage -> nom de fichier pour chaque répertoire, puis utiliser joinpour faire correspondre les fichiers avec le même hachage. Pour générer la carte, mettez ce qui suit dans makemap.sh:

find "$1" -type f -exec md5 -r "{}" \; \
  | sed "s/\([a-z0-9]*\) ${1}\/\(.*\)/\1 \"\2\"/" \

makemap.sh crache un fichier avec des lignes de la forme, 'hachage "nom de fichier"', donc nous nous joignons juste sur la première colonne:

join <(./makemap.sh 'old') <(./makemap.sh 'new') >moves.txt

Cela génère moves.txtce qui ressemble à ceci:

49787681dd7fcc685372784915855431 "a/a.txt" "bar/a.txt"
bfdaa3e91029d31610739d552ede0c26 "c c/c c.txt" "c c/c c.txt"

La prochaine étape serait de faire les mouvements, mais mes tentatives se sont bloquées sur les citations ... mv -iet mkdir -pdevraient être utiles.

Janus
la source
Désolé, je ne comprends rien de tout ça!
Dan
1
joinest vraiment intéressant. Merci de l'avoir porté à mon attention.
Steven D
@Dan. Désolé. Le problème est que je ne sais pas quelles hypothèses je peux faire sur vos noms de fichiers. Scripter sans hypothèses n'est pas amusant, surtout dans ce cas où j'ai choisi de sortir les noms de fichiers dans un fichier dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus
1
Cela gaspille probablement beaucoup de temps (et de charge CPU) car ces fichiers énormes doivent être lus complètement pour créer les hachages MD5. Si le nom et la taille du fichier correspondent, il est probablement exagéré de hacher les fichiers. Le hachage doit être effectué dans une deuxième étape et uniquement pour les fichiers qui correspondent à au moins un (sur le même disque) en nom ou en taille.
Hauke ​​Laging
Vous n'avez pas besoin de trier les fichiers que vous utilisez en joinentrée?
cjm
8

Il existe un utilitaire appelé unisson:

http://www.cis.upenn.edu/~bcpierce/unison/

Description du site:

Unison est un outil de synchronisation de fichiers pour Unix et Windows. Il permet à deux répliques d'une collection de fichiers et de répertoires d'être stockées sur différents hôtes (ou différents disques sur le même hôte), modifiées séparément, puis mises à jour en propageant les modifications de chaque réplique à l'autre.

Notez que Unison ne détecte les fichiers déplacés lors de la première exécution que si au moins une des racines est distante, donc même si vous synchronisez des fichiers locaux, utilisez-la ssh://localhost/path/to/dircomme l'une des racines.

hasen
la source
@ Gilles: Êtes-vous sûr? J'utilise l'unisson pour tout et je le vois souvent repérer des fichiers qui ont été renommés et / ou déplacés très loin. Voulez-vous dire que cela ne fonctionne que pour les fichiers déjà synchronisés où unisson a eu la possibilité d'enregistrer des numéros d'inode (ou toute autre astuce qu'il utilise)?
Janus
@Janus: Merci pour la correction, mon commentaire était en effet faux. Unison détecte les fichiers qui ont été déplacés, même lors de l'exécution initiale. (Il ne fait pas cela lorsque les deux racines sont locales, c'est pourquoi il ne l'a pas fait dans mon test.) Donc, l'unisson est une très bonne suggestion.
Gilles 'SO- arrête d'être méchant'
@Gilles. Bon à savoir - il semble y avoir pas mal d'endroits où l'algorithme fait la distinction entre les synchronisations locales et distantes. En fait, je ne pensais pas que cela fonctionnerait pour la première synchronisation. +1 à l'unisson!
Janus
4

Utilisez Unison comme suggéré par hasen j . Je laisse cette réponse comme un exemple de script potentiellement utile ou pour une utilisation sur un serveur avec uniquement des utilitaires de base installés.


Je suppose que les noms de fichiers sont uniques dans toute la hiérarchie. Je suppose également qu'aucun nom de fichier ne contient de nouvelle ligne et que les arborescences de répertoires ne contiennent que des répertoires et des fichiers normaux.

  1. Collectez d'abord les noms de fichiers côté source.

    (cd /A && find . \! -type d) >A.find
  2. Ensuite, déplacez les fichiers en place du côté destination. Tout d'abord, créez une arborescence de fichiers aplatie du côté destination. Utilisez lnau lieu de mvsi vous souhaitez conserver les liens durs dans l'ancienne hiérarchie.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Si certains fichiers peuvent être manquants dans la destination, créez un aplatissement similaire /A.staginget utilisez rsync pour copier les données de la source vers la destination.

    rsync -au /A.staging/ /B.staging/
  4. Renommez maintenant les fichiers en place.

    cd /B.new &&
    <A.find perl -l -ne '
      my $dir = '.'; s!^\./+!!;
      while (s!^([^/]+)/+!!) {  # Create directories as needed
        $dir .= "/$1";
        -d $dir or mkdir $dir or die "mkdir $dir: $!"
      }
      rename "/B.staging/$_", "$dir/$_" or die "rename -> $dir/$_: $!"
    '

    De manière équivalente:

    cd /B.new &&
    <A.find python -c '
    import os, sys
    for path in sys.stdin.read().splitlines():
        dir, base = path.rsplit("/", 2)
        os.rename(os.path.join("/B.new", base), path)
    '
  5. Enfin, si vous vous souciez des métadonnées des répertoires, appelez rsync avec les fichiers déjà en place.

    rsync -au /A/ /B.new/

Notez que je n'ai pas testé les extraits de cet article. À utiliser à vos risques et périls. Veuillez signaler toute erreur dans un commentaire.

Gilles 'SO- arrête d'être méchant'
la source
2

En particulier, si la synchronisation en cours est utile, vous pouvez essayer de comprendre git-annex .

C'est relativement nouveau; Je n'ai pas essayé de l'utiliser moi-même.

Je peux le suggérer car il évite de conserver une deuxième copie des fichiers ... cela signifie qu'il doit marquer les fichiers en lecture seule ("verrouillés"), comme certains systèmes de contrôle de version non Git.

Les fichiers sont identifiés par l'extension de fichier sha256sum + (par défaut). Il devrait donc être capable de synchroniser deux dépôts avec un contenu de fichier identique mais des noms de fichiers différents, sans avoir à effectuer d'écritures (et sur un réseau à faible bande passante, si vous le souhaitez). Il faudra bien sûr lire tous les fichiers pour les additionner.

sourcejedi
la source
1

Que diriez-vous quelque chose comme ça:

src=/mnt/driveA
dst=/mnt/driveB

cd $src
find . -name <PATTERN> -type f >/tmp/srclist
cd $dst
find . -name <PATTERN> -type f >/tmp/dstlist

cat /tmp/srclist | while read srcpath; do
    name=`basename "$srcpath"`
    srcdir=`dirname "$srcpath"`
    dstpath=`grep "/${name}\$" /tmp/dstlist`

    mkdir -p "$srcdir"
    cd "$srcdir" && ln -s "$dstpath" "$name"
done

Cela suppose que les noms des fichiers que vous souhaitez synchroniser sont uniques sur l'ensemble du lecteur: sinon, il ne peut pas être entièrement automatisé (cependant, vous pouvez fournir une invite pour que l'utilisateur choisisse le fichier à choisir s'il y en a plus d'un).

Le script ci-dessus fonctionnera dans des cas simples, mais peut échouer s'il namese trouve qu'il contient des symboles qui ont une signification particulière pour les expressions régulières. La grepliste des fichiers sur peut également prendre beaucoup de temps s'il y a beaucoup de fichiers. Vous pouvez envisager de traduire ce code pour utiliser une table de hachage qui mappera les noms de fichiers en chemins, par exemple dans Ruby.

alex
la source
Cela semble prometteur - mais déplace-t-il les fichiers ou crée-t-il simplement des liens symboliques?
Dan
Je pense que je comprends la plupart de cela; mais que fait la grepligne? Trouve-t-il simplement le chemin complet du fichier correspondant dans dstlist?
Dan
@Dan: apparemment, par son utilisation, lnil crée des liens symboliques. Vous pouvez utiliser mvpour déplacer les fichiers, mais attention à remplacer les fichiers existants. En outre, vous souhaiterez peut-être nettoyer les répertoires vides, le cas échéant, après avoir éloigné les fichiers. Oui, cette grepcommande recherche une ligne qui se termine sur le nom de fichier, révélant ainsi le chemin d'accès complet à celui-ci sur le lecteur de destination.
alex
1

En supposant que les noms de fichiers de base sont uniques dans les arbres, c'est assez simple:

join <(cd A; find . -type f | while read f; do echo $(basename $f) $(dirname $f); done | sort) \
     <(cd B; find . -type f | while read f; do echo $(basename $f) $(dirname $f); done | sort) |\
while read name to from
do
        mkdir -p B/$to
        mv -v B/$from/$name B/$to/
done

Si vous vouliez nettoyer les anciens répertoires vides, utilisez:

find B -depth -type d -delete
Uditha Desilva
la source
1

J'ai également rencontré ce problème. La solution basée sur md5sum n'a pas fonctionné pour moi, car je synchronise mes fichiers sur un webdavmontage. Le calcul des sommes md5sum sur la webdavdestination signifierait également des opérations de fichiers volumineux.

J'ai fait un petit script reorg_Remote_Dir_detect_moves.sh (sur github) qui essaie de détecter les fichiers les plus déplacés et crée ensuite un nouveau script shell temporaire avec plusieurs commandes pour ajuster le répertoire distant. Comme je ne m'occupe que des noms de fichiers, le script n'est pas une solution parfaite.

Pour des raisons de sécurité, plusieurs fichiers seront ignorés: A) Les fichiers avec le même (même début) noms de chaque côté, et B) Les fichiers qui ne sont que du côté distant. Ils seront ignorés et ignorés.

Les fichiers ignorés seront ensuite traités par votre outil de synchronisation préféré (par exemple, rsync, unison , ...), que vous devrez utiliser après avoir exécuté le script shell temporaire.

Alors peut-être que mon script est utile pour quelqu'un? Si c'est le cas (pour être plus clair), il y a trois étapes:

  1. Exécutez le script shell reorg_Remote_Dir_detect_moves.sh (sur github)
  2. Cela créera le shell-script temporaire /dev/shm/REORGRemoteMoveScript.sh=> exécutez ceci pour faire les mouvements (sera rapide sur monté webdav)
  3. Exécutez votre outil de synchronisation préféré (par exemple rsync, unison, ...)
Aex Oquare
la source
1

Voici ma tentative de réponse. En guise d'avertissement, toute mon expérience de script vient de bash, donc si vous utilisez un shell différent, les noms de commande ou la syntaxe peuvent être différents.

Cette solution nécessite la création de deux scripts séparés.

Ce premier script est chargé de déplacer réellement les fichiers sur le lecteur de destination.

md5_map_file="<absolute-path-to-a-temporary-file>"

# Given a single line from the md5 map file, list
# only the path from that line.
get_file()
{
  echo $2
}

# Given an md5, list the filename from the md5 map file
get_file_from_md5()
{
  # Grab the line from the md5 map file that has the
  # md5 sum passed in and call get_file() with that line.
  get_file `cat $md5_map_file | grep $1`
}

file=$1

# Compute the md5
sum=`md5sum $file`

# Get the new path for the file
new_file=`get_file_from_md5 $sum`

# Make sure the destination directory exists
mkdir -p `dirname $new_file`
# Move the file, prompting if the move would cause an overwrite
mv -i $file $new_file

Le deuxième script crée le fichier de mappage md5 utilisé par le premier script, puis appelle le premier script sur chaque fichier du lecteur de destination.

# Do not put trailing /
src="<absolute-path-to-source-drive>"
dst="<absolute-path-to-destination-drive>"
script_path="<absolute-path-to-the-first-script>"
md5_map_file="<same-absolute-path-from-first-script>"


# This command searches through the source drive
# looking for files.  For every file it finds,
# it computes the md5sum and writes the md5 sum and
# the path to the found filename to the filename stored
# in $md5_map_file.
# The end result is a file listing the md5 of every file
# on the source drive
cd $src
find . -type f -exec md5sum "{}" \; > $md5_map_file

# This command searches the destination drive for files and calls the first
# script for every file it finds.
cd $dst
find . -type f -exec $script_path '{}' \; 

Fondamentalement, ce qui se passe est que les deux scripts simulent un tableau associatif avec $md5_map_file . Tout d'abord, tous les md5 des fichiers sur le lecteur source sont calculés et stockés. Les md5 sont associés aux chemins relatifs depuis la racine du lecteur. Ensuite, pour chaque fichier sur le lecteur de destination, le md5 est calculé. En utilisant ce md5, le chemin de ce fichier sur le lecteur source est recherché. Le fichier sur le lecteur de destination est ensuite déplacé pour correspondre au chemin du fichier sur le lecteur source.

Il y a quelques mises en garde avec ce script:

  • Il suppose que chaque fichier dans $ dst est également dans $ src
  • Il ne supprime aucun répertoire de $ dst, déplace uniquement les fichiers. Je ne suis actuellement pas en mesure de penser à un moyen sûr de le faire automatiquement
cledoux
la source
Le calcul des md5 doit prendre un certain temps: tout le contenu doit en fait être lu. Alors que si Dan est sûr que les fichiers sont identiques, il suffit de les déplacer dans la structure du répertoire très rapidement (pas de lecture). Donc, md5sumne semble pas être la chose à utiliser ici. (BTW, rsynca un mode dans lequel il ne calcule pas les sommes de contrôle.)
imz - Ivan Zakharyaschev
C'est un compromis entre précision et vitesse. Je voulais fournir une méthode qui utilise un degré de précision plus élevé que les noms de fichiers.
cledoux