Jakiś sposób synchronizacji struktury katalogów, gdy pliki są już po obu stronach?


24

Mam dwa dyski z tymi samymi plikami, ale struktura katalogów jest zupełnie inna.

Czy jest jakiś sposób na „przeniesienie” wszystkich plików po stronie docelowej, aby pasowały do ​​struktury strony źródłowej? Może ze skryptem?

Na przykład dysk A ma:

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

Podczas gdy dysk B ma:

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

Pliki, o których mowa, są ogromne (800 GB), więc nie chcę ich ponownie kopiować; Chcę tylko zsynchronizować strukturę, tworząc niezbędne katalogi i przenosząc pliki.

Myślałem o skrypcie rekurencyjnym, który znalazłby każdy plik źródłowy w miejscu docelowym, a następnie przeniósł go do odpowiedniego katalogu, tworząc go w razie potrzeby. Ale - to przekracza moje możliwości!

Inne eleganckie rozwiązanie podano tutaj: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086


Czy jesteś pewien, że nazwa jednoznacznie określa zawartość pliku, w przeciwnym razie powinieneś rozważyć porównanie plików według ich sum kontrolnych.
kasterma

Odpowiedzi:


11

Pójdę z Gillesem i wskażę ci Unisona, jak sugeruje hasen j . Unison był DropBox 20 lat przed DropBox. Twardy, solidny kod, z którego korzysta wiele osób (w tym ja) każdego dnia - warto się tego nauczyć. Mimo to joinpotrzebuje całej reklamy, jaką może uzyskać :)


To tylko połowa odpowiedzi, ale muszę wrócić do pracy :)

Zasadniczo chciałem zademonstrować mało znane joinnarzędzie, które właśnie to robi: łączy dwie tabele na pewnym polu.

Najpierw skonfiguruj przypadek testowy zawierający nazwy plików ze spacjami:

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

(edytuj niektóre nazwy katalogów i / lub plików w new).

Teraz chcemy zbudować mapę: skrót -> nazwa pliku dla każdego katalogu, a następnie użyć joindo dopasowania plików z tym samym skrótem. Aby wygenerować mapę, wpisz makemap.sh:

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

makemap.sh wyrzuca plik z wierszami formularza „hash” nazwa pliku ”, więc dołączamy tylko do pierwszej kolumny:

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

Generuje to, moves.txtktóre wygląda następująco:

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

Następnym krokiem byłoby wykonanie ruchów, ale moje próby utknęły w miejscu ... mv -ii mkdir -ppowinny się przydać.


Przepraszam, nic z tego nie rozumiem!
Dan

1
joinjest naprawdę interesujące. Dziękuję za zwrócenie mi na to uwagi.
Steven D

@Dan. Przepraszam. Problem polega na tym, że nie wiem, jakie mogę przyjąć założenia dotyczące nazw plików. Skrypty bez założeń nie są zabawne, szczególnie w tym przypadku, gdy postanowiłem wyprowadzić nazwy plików do pliku dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus

1
To prawdopodobnie marnuje dużo czasu (i obciążenia procesora), ponieważ te ogromne pliki należy całkowicie odczytać, aby utworzyć skróty MD5. Jeśli nazwa i rozmiar pliku są zgodne, to prawdopodobnie przesadzanie z haszowaniem plików. Hashowanie powinno odbywać się w drugim kroku i tylko dla plików, które pasują co najmniej do jednego (na tym samym dysku) pod względem nazwy lub rozmiaru.
Hauke ​​Laging

Nie musisz sortować plików, które wykorzystujesz jako joindane wejściowe?
cjm

8

Istnieje narzędzie o nazwie unison:

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

Opis ze strony:

Unison to narzędzie do synchronizacji plików dla systemów Unix i Windows. Umożliwia przechowywanie dwóch replik kolekcji plików i katalogów na różnych hostach (lub różnych dyskach na tym samym hoście), modyfikowanie ich osobno, a następnie aktualizowanie poprzez propagowanie zmian w każdej replice do drugiej.

Pamiętaj, że Unison wykrywa przeniesione pliki przy pierwszym uruchomieniu tylko wtedy, gdy przynajmniej jeden z katalogów głównych jest zdalny, więc nawet jeśli synchronizujesz pliki lokalne, użyj go ssh://localhost/path/to/dirjako jednego z katalogów głównych.


@Gilles: Jesteś pewien? Używam unison do wszystkiego i często widzę, że dostrzega pliki, których nazwy zostały zmienione i / lub zostały przeniesione daleko. Czy mówisz, że działa to tylko w przypadku już zsynchronizowanych plików, w których unison miał szansę nagrać numery i-węzłów (lub dowolne inne sztuczki, których używa)?
Janus

@Janus: Dzięki za korektę, mój komentarz był rzeczywiście błędny. Unison wykrywa pliki, które zostały przeniesione, nawet podczas pierwszego uruchomienia. (Nie robi tego, gdy oba korzenie są lokalne, dlatego nie zrobił tego w moim teście.) Tak więc unison jest bardzo dobrą sugestią.
Gilles „SO- przestań być zły”

@Gilles. Warto wiedzieć - wydaje się, że w algorytmie rozróżnia się synchronizację lokalną i zdalną. Właściwie nie sądziłem, że zadziała przy pierwszej synchronizacji. +1 za unisono!
Janus

4

Użyj Unison zgodnie z sugestią hasen j . Pozostawiam tę odpowiedź jako potencjalnie przydatny przykład skryptowy lub do użytku na serwerze z zainstalowanymi tylko podstawowymi narzędziami.


Zakładam, że nazwy plików są unikalne w całej hierarchii. Zakładam również, że żadna nazwa pliku nie zawiera nowego wiersza, a drzewa katalogów zawierają tylko katalogi i zwykłe pliki.

  1. Najpierw zbierz nazwy plików po stronie źródłowej.

    (cd /A && find . \! -type d) >A.find
  2. Następnie przenieś pliki na miejsce po stronie docelowej. Najpierw utwórz spłaszczone drzewo plików po stronie docelowej. Użyj lnzamiast, mvjeśli chcesz zachować twarde linki w starej hierarchii.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Jeśli brakuje niektórych plików w miejscu docelowym, utwórz podobnie spłaszczony /A.stagingi użyj rsync, aby skopiować dane ze źródła do miejsca docelowego.

    rsync -au /A.staging/ /B.staging/
  4. Teraz zmień nazwę plików na swoje miejsce.

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

    Równoważnie:

    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. Na koniec, jeśli zależy ci na metadanych katalogów, wywołaj rsync z plikami, które już istnieją.

    rsync -au /A/ /B.new/

Pamiętaj, że nie testowałem fragmentów w tym poście. Używaj na własne ryzyko. Zgłoś każdy błąd w komentarzu.


2

Szczególnie, jeśli przydałaby się ciągła synchronizacja, możesz spróbować znaleźć załącznik git .

Jest stosunkowo nowy; Sam nie próbowałem tego użyć.

Mogę to zasugerować, ponieważ unika się przechowywania drugiej kopii plików ... oznacza to, że musi oznaczać pliki jako tylko do odczytu („zablokowane”), podobnie jak niektóre systemy kontroli wersji inne niż Git.

Pliki są identyfikowane przez rozszerzenie pliku sha256sum + (domyślnie). Powinien więc być w stanie zsynchronizować dwa repozytoria z identyczną zawartością pliku, ale różnymi nazwami plików, bez konieczności wykonywania operacji zapisu (i w razie potrzeby w sieci o niskiej przepustowości). Będzie oczywiście musiał przeczytać wszystkie pliki, aby je zsumować.


1

Co powiesz na coś takiego:

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

Zakłada się, że nazwy plików, które chcesz zsynchronizować, są unikalne na całym dysku: w przeciwnym razie nie będzie możliwości pełnej automatyzacji (możesz jednak poprosić użytkownika o wybranie pliku do wyboru, jeśli jest więcej.)

Powyższy skrypt będzie działał w prostych przypadkach, ale może się nie powieść, jeśli namezawiera symbole mające specjalne znaczenie dla wyrażeń regularnych. grepNa liście plików może także zająć dużo czasu, jeśli jest dużo plików. Możesz rozważyć przetłumaczenie tego kodu na hashtable, który mapuje nazwy plików na ścieżki, np. W Ruby.


Wygląda to obiecująco - ale czy przenosi pliki, czy po prostu tworzy dowiązania symboliczne?
Dan

Myślę, że rozumiem większość z nich; ale co robi greplinia? Czy po prostu znajduje pełną ścieżkę pasującego pliku dstlist?
Dan

@ Dan: najwyraźniej dzięki lnniemu tworzy dowiązania symboliczne. Możesz użyć mvdo przeniesienia plików, ale uważaj na nadpisywanie istniejących. Ponadto możesz usunąć puste katalogi, jeśli takie istnieją, po przeniesieniu plików. Tak, to greppolecenie wyszukuje wiersz, który kończy się na nazwie pliku, tym samym ujawniając pełną ścieżkę do niego na dysku docelowym.
alex

1

Zakładając, że podstawowe nazwy plików są unikalne w drzewach, jest to dość proste:

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

Jeśli chcesz wyczyścić stare puste katalogi, użyj:

find B -depth -type d -delete

1

Też napotkałem ten problem. Rozwiązanie oparte na md5sum nie działało dla mnie, ponieważ synchronizuję moje pliki z webdavmontażem. Obliczanie sum md5 na miejscu webdavdocelowym oznaczałoby również operacje na dużych plikach.

Zrobiłem mały skrypt reorg_Remote_Dir_detect_moves.sh (na github), który próbuje wykryć najczęściej przenoszone pliki, a następnie tworzy nowy tymczasowy skrypt powłoki z kilkoma poleceniami do dostosowania katalogu zdalnego. Ponieważ dbam tylko o nazwy plików, skrypt nie jest idealnym rozwiązaniem.

Ze względów bezpieczeństwa kilka plików zostanie zignorowanych: A) Pliki o takich samych (tych samych początkowych) nazwach po każdej stronie i B) Pliki, które znajdują się tylko po stronie zdalnej. Zostaną zignorowani i pominięci.

Pominięte pliki będą następnie obsługiwane przez preferowane narzędzie synchronizacji (np. rsync, unison...), którego musisz użyć po uruchomieniu tymczasowego skryptu powłoki.

Więc może mój skrypt jest dla kogoś przydatny? Jeśli tak (aby było bardziej jasne), są trzy kroki:

  1. Uruchom skrypt powłoki reorg_Remote_Dir_detect_moves.sh (na github)
  2. Spowoduje to utworzenie tymczasowego skryptu powłoki /dev/shm/REORGRemoteMoveScript.sh=> uruchom to, aby wykonać ruchy (będzie szybko montowane webdav)
  3. Uruchom preferowane narzędzie do synchronizacji (np. rsync, unison...)

1

Oto moja próba odpowiedzi. Jako ostrzeżenie, całe moje doświadczenie związane ze skryptami pochodzi od bash, więc jeśli używasz innej powłoki, nazwy poleceń lub składnia mogą być inne.

To rozwiązanie wymaga utworzenia dwóch osobnych skryptów.

Ten pierwszy skrypt odpowiada za przeniesienie plików na dysk docelowy.

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

Drugi skrypt tworzy plik mapy md5 używany przez pierwszy skrypt, a następnie wywołuje pierwszy skrypt na każdym pliku na dysku docelowym.

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

Zasadniczo dwa skrypty symulują tablicę asocjacyjną $md5_map_file. Po pierwsze, wszystkie pliki md5 dla plików na dysku źródłowym są obliczane i przechowywane. Z md5 związane są ścieżki względne od katalogu głównego dysku. Następnie dla każdego pliku na dysku docelowym obliczane jest md5. Korzystając z tego md5, ścieżka tego pliku na dysku źródłowym jest sprawdzana. Plik na dysku docelowym jest następnie przenoszony zgodnie z ścieżką pliku na dysku źródłowym.

Istnieje kilka zastrzeżeń dotyczących tego skryptu:

  • Zakłada się, że każdy plik w $ dst jest również w $ src
  • Nie usuwa żadnych katalogów z $ dst, jedynie przenosi pliki. Obecnie nie jestem w stanie wymyślić bezpiecznego sposobu na zrobienie tego automatycznie

Obliczenie md5 musi zająć dużo czasu: cała zawartość musi zostać odczytana. Chociaż jeśli Dan jest pewien, że pliki są identyczne, po prostu przenoszenie ich w strukturze katalogów jest bardzo szybkie (bez odczytu). md5sumWydaje się , że nie należy go tutaj używać. (BTW, rsyncma tryb, w którym nie oblicza sum kontrolnych.)
imz - Ivan Zakharyaschev 29.01.11

Jest to kompromis między dokładnością a prędkością. Chciałem przedstawić metodę, która wykorzystywałaby wyższy stopień dokładności niż zwykłe nazwy plików.
cledoux 30.01.11
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.