Gibt es eine Möglichkeit, die Verzeichnisstruktur zu synchronisieren, wenn sich die Dateien bereits auf beiden Seiten befinden?

24

Ich habe zwei Laufwerke mit den gleichen Dateien, aber die Verzeichnisstruktur ist völlig unterschiedlich.

Gibt es eine Möglichkeit, alle Dateien auf der Zielseite so zu verschieben, dass sie der Struktur der Quellseite entsprechen? Mit einem Drehbuch vielleicht?

Beispielsweise hat Laufwerk A:

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

Während Laufwerk B hat:

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

Die fraglichen Dateien sind riesig (800 GB), daher möchte ich sie nicht erneut kopieren. Ich möchte nur die Struktur synchronisieren, indem ich die erforderlichen Verzeichnisse erstelle und die Dateien verschiebe.

Ich dachte an ein rekursives Skript, das jede Quelldatei auf dem Ziel findet, sie dann in ein passendes Verzeichnis verschiebt und sie bei Bedarf erstellt. Aber - das übersteigt meine Fähigkeiten!

Eine andere elegante Lösung wurde hier angegeben: /superuser/237387/any-way-to-sync-directory-structure-when-die-Dateien-sind- bereits-auf- beiden Seiten / 238086

Dan
quelle
Sind Sie sicher, dass der Name den Inhalt einer Datei eindeutig bestimmt? Andernfalls sollten Sie Dateien anhand ihrer Prüfsummen vergleichen.
Kasterma

Antworten:

11

Ich werde mit Gilles gehen und Sie auf Unison hinweisen, wie von Hasen J vorgeschlagen . Unison war DropBox 20 Jahre vor DropBox. Sehr solider Code, den viele Leute (ich selbst eingeschlossen) jeden Tag benutzen - es lohnt sich, ihn zu lernen. Trotzdem joinbraucht es all die Werbung, die es bekommen kann :)


Das ist nur eine halbe Antwort, aber ich muss mich wieder an die Arbeit machen :)

Grundsätzlich wollte ich das wenig bekannte joinHilfsprogramm demonstrieren, das genau das macht: Verbindet zwei Tabellen in einem bestimmten Feld.

Richten Sie zunächst einen Testfall mit Dateinamen und Leerzeichen ein:

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

(Bearbeiten Sie einige Verzeichnis- und / oder Dateinamen in new).

Nun wollen wir eine Map erstellen: Hash -> Dateiname für jedes Verzeichnis und dann dazu verwenden join, Dateien mit demselben Hash abzugleichen. Geben Sie Folgendes ein, um die Karte zu generieren makemap.sh:

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

makemap.sh spuckt eine Datei mit Zeilen der Form 'hash "filename"' aus, also fügen wir einfach die erste Spalte hinzu:

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

Das erzeugt moves.txtwas so aussieht:

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

Der nächste Schritt wäre, die Züge tatsächlich zu machen, aber meine Versuche blieben beim Zitieren hängen ... mv -iund mkdir -psollten sich als nützlich erweisen.

Janus
quelle
Entschuldigung, ich verstehe nichts davon!
Dan
1
joinist wirklich interessant. Vielen Dank, dass Sie mich darauf aufmerksam gemacht haben.
Steven D
@Dan. Es tut uns leid. Das Problem ist, dass ich nicht weiß, welche Annahmen ich über Ihre Dateinamen machen kann. Das Skripting ohne Annahmen macht keinen Spaß, insbesondere in diesem Fall, in dem ich die Dateinamen in einer Datei namens dwheeler.com/essays/fixing-unix-linux-filenames.html ausgegeben habe .
Janus
1
Dies verschwendet wahrscheinlich viel Zeit (und CPU-Last), da diese riesigen Dateien zum Erstellen der MD5-Hashes vollständig gelesen werden müssen. Wenn der Dateiname und die Dateigröße übereinstimmen, ist es wahrscheinlich übertrieben, die Dateien zu hacken. Das Hashing sollte in einem zweiten Schritt und nur für die Dateien durchgeführt werden, deren Name oder Größe mindestens einer (auf derselben Festplatte) entspricht.
Hauke ​​Laging
Müssen Sie die Dateien, die Sie als joinEingabe verwenden, nicht sortieren ?
cjm
8

Es gibt ein Hilfsprogramm namens unison:

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

Beschreibung vom Standort:

Unison ist ein Dateisynchronisationstool für Unix und Windows. Damit können zwei Replikate einer Sammlung von Dateien und Verzeichnissen auf unterschiedlichen Hosts (oder unterschiedlichen Festplatten auf demselben Host) gespeichert, separat geändert und dann aktualisiert werden, indem die Änderungen in jedem Replikat auf den anderen übertragen werden.

Beachten Sie, dass Unison verschobene Dateien bei der ersten Ausführung nur erkennt, wenn mindestens einer der Stammordner remote ist. Verwenden Sie ihn daher auch dann ssh://localhost/path/to/dirals einen der Stammordner, wenn Sie lokale Dateien synchronisieren .

hasen
quelle
@ Gilles: Bist du sicher? Ich benutze unisono für alles und sehe oft Dateien, die umbenannt und / oder weit entfernt wurden. Wollen Sie damit sagen, dass dies nur für bereits synchronisierte Dateien funktioniert, bei denen unisono die Möglichkeit hatte, Inode-Nummern aufzuzeichnen (oder welche anderen Tricks auch immer)?
Janus
@Janus: Danke für die Korrektur, mein Kommentar war in der Tat falsch. Unison erkennt Dateien, die bereits beim ersten Start verschoben wurden. (Das funktioniert nicht, wenn beide Wurzeln lokal sind, weshalb es in meinem Test nicht funktioniert hat.) Daher ist Unisono ein sehr guter Vorschlag.
Gilles 'SO- hör auf böse zu sein'
@ Gilles. Gut zu wissen - es scheint einige Stellen zu geben, an denen der Algorithmus zwischen lokaler und entfernter Synchronisation unterscheidet. Ich hätte eigentlich nicht gedacht, dass es für die erste Synchronisierung funktionieren würde. +1 für unisono!
Janus
4

Verwenden Sie Unison wie von hasen j vorgeschlagen . Ich lasse diese Antwort als potenziell nützliches Skriptbeispiel oder zur Verwendung auf einem Server mit nur installierten Basisdienstprogrammen offen.


Ich gehe davon aus, dass die Dateinamen in der gesamten Hierarchie eindeutig sind. Ich gehe auch davon aus, dass kein Dateiname eine neue Zeile enthält und dass die Verzeichnisbäume nur Verzeichnisse und reguläre Dateien enthalten.

  1. Sammeln Sie zuerst die Dateinamen auf der Quellseite.

    (cd /A && find . \! -type d) >A.find
  2. Verschieben Sie dann die Dateien auf der Zielseite. Erstellen Sie zunächst einen abgeflachten Dateibaum auf der Zielseite. Verwenden Sie lnstatt , mvwenn man hart Links um in der alten Hierarchie behalten will.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Wenn im Ziel möglicherweise einige Dateien fehlen, erstellen Sie eine ähnlich reduzierte Datei /A.stagingund kopieren Sie die Daten mit rsync von der Quelle zum Ziel.

    rsync -au /A.staging/ /B.staging/
  4. Benennen Sie nun die Dateien um.

    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/$_: $!"
    '

    Äquivalent:

    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. Wenn Sie sich für die Metadaten der Verzeichnisse interessieren, rufen Sie schließlich rsync mit den bereits vorhandenen Dateien auf.

    rsync -au /A/ /B.new/

Beachten Sie, dass ich die Snippets in diesem Beitrag nicht getestet habe. Benutzung auf eigene Gefahr. Bitte melden Sie einen Fehler in einem Kommentar.

Gilles 'SO - hör auf böse zu sein'
quelle
2

Insbesondere wenn eine fortlaufende Synchronisierung nützlich wäre, könnten Sie versuchen, den git-annex herauszufinden .

Es ist relativ neu; Ich habe nicht versucht, es selbst zu benutzen.

Ich kann es vorschlagen, da es vermeidet, eine zweite Kopie der Dateien zu behalten. Dies bedeutet, dass die Dateien als schreibgeschützt ("gesperrt") markiert werden müssen, wie dies bei bestimmten Versionskontrollsystemen ohne Git der Fall ist.

Dateien werden durch die Dateierweiterung sha256sum + (standardmäßig) identifiziert. Es sollte also in der Lage sein, zwei Repos mit identischem Dateiinhalt, aber unterschiedlichen Dateinamen zu synchronisieren, ohne dass Schreibvorgänge ausgeführt werden müssen (und, falls gewünscht, über ein Netzwerk mit geringer Bandbreite). Es muss natürlich alle Dateien lesen, um sie zu prüfen.

sourcejedi
quelle
1

Wie wäre es mit so etwas:

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

Dies setzt voraus, dass die Namen der Dateien, die Sie synchronisieren möchten, auf dem gesamten Laufwerk eindeutig sind. Andernfalls ist eine vollständige Automatisierung nicht möglich.

Das obige Skript funktioniert in einfachen Fällen, kann jedoch fehlschlagen, wenn nameSymbole enthalten sind, die für reguläre Ausdrücke eine besondere Bedeutung haben. Das grepAuflisten von Dateien kann auch viel Zeit in Anspruch nehmen, wenn viele Dateien vorhanden sind. Sie können erwägen, diesen Code zu übersetzen, um eine Hash-Tabelle zu verwenden, die Dateinamen Pfaden zuordnet, z. B. in Ruby.

Alex
quelle
Das sieht vielversprechend aus - aber werden die Dateien verschoben oder nur Symlinks erstellt?
Dan
Ich glaube, ich verstehe das meiste davon. aber was macht die grepLeitung? Findet es nur den vollständigen Pfad der übereinstimmenden Datei in dstlist?
Dan
@Dan: Anscheinend entstehen durch die Verwendung lndavon Symlinks. Sie können mvdie Dateien verschieben, achten Sie jedoch darauf, vorhandene Dateien nicht zu überschreiben. Möglicherweise möchten Sie auch leere Verzeichnisse bereinigen, nachdem Sie die Dateien entfernt haben. Ja, dieser grepBefehl sucht nach einer Zeile, die auf dem Dateinamen endet, und gibt den vollständigen Pfad dazu auf dem Ziellaufwerk an.
Alex
1

Angenommen, die Basisdateinamen sind in den Bäumen eindeutig, ist dies ziemlich einfach:

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

Wenn Sie die alten leeren Verzeichnisse bereinigen möchten, verwenden Sie:

find B -depth -type d -delete
Uditha Desilva
quelle
1

Ich war auch mit diesem Problem konfrontiert. Die auf md5sum basierende Lösung hat bei mir nicht funktioniert, da ich meine Dateien mit einem webdavMount synchronisiere . Das Berechnen von md5sum-Summen auf dem webdavZiel würde auch große Dateioperationen bedeuten.

Ich habe ein kleines Skript reorg_Remote_Dir_detect_moves.sh (auf Github) erstellt, das versucht, die am meisten verschobenen Dateien zu erkennen , und dann ein neues temporäres Shell-Skript mit mehreren Befehlen zum Anpassen des Remote-Verzeichnisses erstellt. Da ich mich nur um die Dateinamen kümmere, ist das Skript keine perfekte Lösung.

Aus Sicherheitsgründen werden mehrere Dateien ignoriert: A) Dateien mit demselben (gleichen Anfangs) Namen auf jeder Seite und B) Dateien, die sich nur auf der Remote-Seite befinden. Sie werden ignoriert und übersprungen.

Übersprungene Dateien werden dann von Ihrem bevorzugten Synchronisierungstool (z. B. rsync, unison...) verarbeitet, das Sie nach dem Ausführen des temporären Shell-Skripts verwenden müssen.

Vielleicht ist mein Skript für jemanden nützlich? Wenn ja (um es klarer zu machen), gibt es drei Schritte:

  1. Führen Sie das Shell-Skript reorg_Remote_Dir_detect_moves.sh (auf Github)
  2. Dadurch wird das temporäre Shell-Skript erstellt /dev/shm/REORGRemoteMoveScript.sh=> führe dies aus, um die Bewegungen auszuführen (wird beim Mounten schnell ausgeführt webdav)
  3. Führen Sie Ihr bevorzugtes Synchronisierungstool aus (z. B. rsync, unison...)
Aex Oquare
quelle
1

Hier ist mein Versuch einer Antwort. Als Warnung stamme meine gesamte Skripterfahrung aus Bash. Wenn Sie also eine andere Shell verwenden, können die Befehlsnamen oder die Syntax abweichen.

Diese Lösung erfordert das Erstellen von zwei separaten Skripten.

Dieses erste Skript ist für das Verschieben der Dateien auf dem Ziellaufwerk verantwortlich.

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

Das zweite Skript erstellt die vom ersten Skript verwendete MD5-Zuordnungsdatei und ruft dann das erste Skript für jede Datei auf dem Ziellaufwerk auf.

# 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 '{}' \; 

Im Grunde genommen simulieren die beiden Skripte ein assoziatives Array mit $md5_map_file. Zunächst werden alle MD5s für die Dateien auf dem Quelllaufwerk berechnet und gespeichert. Zu den MD5s gehören die relativen Pfade vom Stammverzeichnis des Laufwerks. Dann wird für jede Datei auf dem Ziellaufwerk der MD5 berechnet. Mit diesem md5 wird der Pfad dieser Datei auf dem Quelllaufwerk gesucht. Die Datei auf dem Ziellaufwerk wird dann so verschoben, dass sie mit dem Pfad der Datei auf dem Quelllaufwerk übereinstimmt.

Mit diesem Skript sind einige Einschränkungen verbunden:

  • Es wird davon ausgegangen, dass sich jede Datei in $ dst auch in $ src befindet
  • Es werden keine Verzeichnisse aus $ dst entfernt, sondern nur die Dateien verschoben. Ich bin derzeit nicht in der Lage, einen sicheren Weg zu finden, dies automatisch zu tun
Cledoux
quelle
Es muss eine Weile dauern, bis die MD5 berechnet sind: Der gesamte Inhalt muss tatsächlich gelesen werden. Wenn Dan sicher ist, dass die Dateien identisch sind, ist das einfache Verschieben in der Verzeichnisstruktur sehr schnell (kein Lesen). Also, md5sumscheint hier nicht das Richtige zu sein. (Übrigens, es rsyncgibt einen Modus, in dem keine Prüfsummen berechnet werden.)
imz - Ivan Zakharyaschev
Es ist ein Kompromiss zwischen Genauigkeit und Geschwindigkeit. Ich wollte eine Methode bereitstellen, die einen höheren Grad an Genauigkeit verwendet als nur Dateinamen.
Cledoux