Un modo per sincronizzare la struttura delle directory quando i file sono già su entrambi i lati?


24

Ho due unità con gli stessi file, ma la struttura della directory è totalmente diversa.

Esiste un modo per "spostare" tutti i file sul lato di destinazione in modo che corrispondano alla struttura del lato di origine? Con una sceneggiatura forse?

Ad esempio, l'unità A ha:

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

Mentre l'unità B ha:

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

I file in questione sono enormi (800 GB), quindi non voglio copiarli di nuovo; Voglio solo sincronizzare la struttura creando le directory necessarie e spostando i file.

Stavo pensando a uno script ricorsivo che avrebbe trovato ogni file sorgente sulla destinazione, quindi lo avrebbe spostato in una directory corrispondente, creandolo se necessario. Ma - questo è oltre le mie capacità!

Un'altra elegante soluzione è stata data qui: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086


Sei sicuro che il nome determini in modo univoco il contenuto di un file, altrimenti dovresti considerare di confrontare i file con i loro checksum.
Kasterma,

Risposte:


11

Andrò con Gilles e puntare a Unison come suggerito da j hasen . Unison era DropBox 20 anni prima di DropBox. Codice solido che molte persone (me compreso) usano ogni giorno - molto utile per imparare. Tuttavia, ha joinbisogno di tutta la pubblicità che può ottenere :)


Questa è solo una mezza risposta, ma devo tornare al lavoro :)

Fondamentalmente, volevo dimostrare l' joinutilità poco nota che fa proprio questo: unisce due tabelle in un certo campo.

Innanzitutto, imposta un caso di test che includa nomi di file con spazi:

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

(modifica alcuni nomi di directory e / o file in new).

Ora, vogliamo costruire una mappa: hash -> nomefile per ogni directory e quindi utilizzare joinper abbinare i file con lo stesso hash. Per generare la mappa, inserisci quanto segue makemap.sh:

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

makemap.sh sputa un file con le linee del modulo, 'hash "nomefile"', quindi ci uniamo alla prima colonna:

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

Questo genera moves.txtche assomiglia a questo:

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

Il prossimo passo sarebbe effettivamente fare le mosse, ma i miei tentativi si sono bloccati nel citare ... mv -ie mkdir -pdovrebbero tornare utili.


Scusa, non capisco niente di tutto questo!
Dan,

1
joinè davvero interessante. Grazie per averlo portato alla mia attenzione.
Steven D,

@ Dan. Scusate. Il problema è che non so quali ipotesi posso fare sui nomi dei tuoi file. Lo scripting senza assunzioni non è divertente, specialmente in questo caso in cui ho scelto di inviare i nomi dei file a un file dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus,

1
Questo probabilmente fa perdere molto tempo (e carico della CPU) perché questi enormi file devono essere letti completamente per creare gli hash MD5. Se il nome del file e la dimensione del file corrispondono, probabilmente è eccessivo eseguire l'hash dei file. L'hash dovrebbe essere eseguito in un secondo passaggio e solo per i file che corrispondono ad almeno uno (sullo stesso disco) in nome o dimensione.
Hauke ​​Laging,

Non hai bisogno di ordinare i file che usi come joininput?
cjm

8

C'è un'utilità chiamata unisono:

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

Descrizione dal sito:

Unison è uno strumento di sincronizzazione dei file per Unix e Windows. Consente a due repliche di una raccolta di file e directory di essere memorizzate su host diversi (o dischi diversi sullo stesso host), modificate separatamente e quindi aggiornate propagando le modifiche in ciascuna replica all'altra.

Si noti che Unison rileva i file spostati alla prima esecuzione solo se almeno una delle radici è remota, quindi anche se si stanno sincronizzando i file locali, utilizzare ssh://localhost/path/to/dircome una delle radici.


@Gilles: sei sicuro? Uso l'unisono per tutto e spesso lo vedo individuare i file che sono stati rinominati e / o spostati lontano. Stai dicendo che questo funziona solo con file già sincronizzati in cui l'unisono ha avuto la possibilità di registrare numeri di inode (o qualunque altro trucco usi)?
Janus,

@Giovanni: grazie per la correzione, il mio commento era davvero sbagliato. Unison rileva i file che sono stati spostati, anche durante l'esecuzione iniziale. (Non lo fa quando entrambe le radici sono locali, motivo per cui non lo ha fatto nel mio test.) Quindi l'unisono è un ottimo suggerimento.
Gilles 'SO- smetti di essere malvagio' il

@Gilles. Buono a sapersi: sembra che ci siano alcuni posti in cui l'algoritmo distingue tra sincronizzazioni locali e remote. In realtà non pensavo che avrebbe funzionato per la prima sincronizzazione. +1 all'unisono!
Janus,

4

Usa Unison come suggerito da hasen j . Lascio questa risposta come un esempio di scripting potenzialmente utile o per l'uso su un server con solo le utility di base installate.


Presumo che i nomi dei file siano univoci in tutta la gerarchia. Presumo anche che nessun nome di file contenga una nuova riga e che gli alberi delle directory contengano solo directory e file regolari.

  1. Per prima cosa raccogli i nomi dei file sul lato sorgente.

    (cd /A && find . \! -type d) >A.find
  2. Quindi spostare i file in posizione sul lato di destinazione. Innanzitutto, crea un albero di file appiattito sul lato di destinazione. Utilizzare lninvece di mvse si desidera mantenere i collegamenti reali nella vecchia gerarchia.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Se nella destinazione potrebbero mancare alcuni file, creane uno appiattito in modo simile /A.staginge utilizza rsync per copiare i dati dall'origine alla destinazione.

    rsync -au /A.staging/ /B.staging/
  4. Ora rinominare i file in posizione.

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

    Equivalentemente:

    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. Infine, se ti interessano i metadati delle directory, chiama rsync con i file già presenti.

    rsync -au /A/ /B.new/

Tieni presente che non ho testato i frammenti in questo post. Utilizzare a proprio rischio. Si prega di segnalare eventuali errori in un commento.


2

In particolare, se la sincronizzazione in corso fosse utile, potresti provare a capire git-annex .

È relativamente nuovo; Non ho provato a usarlo da solo.

Sono in grado di suggerirlo perché evita di conservare una seconda copia dei file ... questo significa che deve contrassegnare i file come di sola lettura ("bloccati"), come alcuni sistemi di controllo della versione non Git.

I file sono identificati dall'estensione sha256sum + (per impostazione predefinita). Quindi dovrebbe essere in grado di sincronizzare due repository con contenuto di file identico ma nomi di file diversi, senza dover eseguire scritture (e su una rete a larghezza di banda ridotta, se lo si desidera). Dovrà ovviamente leggere tutti i file per poterli sommare.


1

Che ne dici di qualcosa del genere:

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

Ciò presuppone che i nomi dei file che si desidera sincronizzare siano univoci nell'intero disco: altrimenti non è possibile che sia completamente automatizzato (tuttavia, è possibile fornire all'utente la richiesta di scegliere quale file scegliere se ce n'è più di uno).

Lo script sopra funzionerà in casi semplici, ma potrebbe non riuscire se namecontiene simboli che hanno un significato speciale per regexps. L' grepelenco di file on può anche richiedere molto tempo se ci sono molti file. Puoi prendere in considerazione la traduzione di questo codice per utilizzare la tabella hash che mapperà i nomi dei file sui percorsi, ad esempio in Ruby.


Sembra promettente, ma sposta i file o crea semplicemente collegamenti simbolici?
Dan,

Penso di aver capito la maggior parte di questo; ma cosa fa la greplinea? Trova solo il percorso completo del file corrispondente in dstlist?
Dan,

@Dan: apparentemente con l'uso di lnesso crea collegamenti simbolici. È possibile utilizzare mvper spostare i file, ma attenzione a sovrascrivere quelli esistenti. Inoltre, potresti voler eliminare eventuali directory vuote dopo aver spostato i file. Sì, quel grepcomando cerca una linea che termina con il nome del file, rivelando così il percorso completo su di essa nell'unità di destinazione.
alex

1

Supponendo che i nomi dei file di base siano unici sugli alberi, è abbastanza semplice:

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

Se si desidera ripulire le vecchie directory vuote, utilizzare:

find B -depth -type d -delete

1

Ho anche affrontato questo problema. La soluzione basata su md5sum non ha funzionato per me, perché sincronizzo i miei file con awebdav mount. Il calcolo delle somme md5sum sulla webdavdestinazione significherebbe anche operazioni su file di grandi dimensioni.

Ho realizzato una piccola sceneggiatura reorg_Remote_Dir_detect_moves.sh (su github) che sta cercando di rilevare di più file spostati e quindi crea un nuovo script di shell temporaneo con diversi comandi per regolare la directory remota. Dal momento che mi occupo solo dei nomi dei file, lo script non è una soluzione perfetta.

Per sicurezza, diversi file verranno ignorati: A) File con gli stessi nomi (stesso inizio) su ogni lato e B) File che si trovano solo sul lato remoto. Saranno ignorati e saltati.

I file ignorati verranno quindi gestiti dal tuo strumento di sincronizzazione preferito (ad es rsync, unison , ...), che dovrai utilizzare dopo aver eseguito lo shell-script temporaneo.

Quindi forse la mia sceneggiatura è utile per qualcuno? In tal caso (per renderlo più chiaro) ci sono tre passaggi:

  1. Esegui lo script della shell reorg_Remote_Dir_detect_moves.sh (su github)
  2. Questo creerà il shell-script temporaneo /dev/shm/REORGRemoteMoveScript.sh=> esegui questo per fare le mosse (sarà veloce su montatowebdav )
  3. Esegui il tuo strumento di sincronizzazione preferito (ad es. rsync, unison, ...)

1

Ecco il mio tentativo di risposta. Come avvertimento, tutta la mia esperienza di scripting proviene da bash, quindi se stai usando una shell diversa, i nomi dei comandi o la sintassi potrebbero essere diversi.

Questa soluzione richiede la creazione di due script separati.

Questo primo script è responsabile dello spostamento effettivo dei file sull'unità di destinazione.

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

Il secondo script crea il file di mappa md5 utilizzato dal primo script e quindi chiama il primo script su ogni file nell'unità di destinazione.

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

Fondamentalmente, ciò che sta succedendo è che i due script simulano un array associativo $md5_map_file . Innanzitutto, tutti i md5 per i file sull'unità di origine vengono calcolati e memorizzati. Associati agli md5 sono i percorsi relativi dalla radice dell'unità. Quindi, per ogni file sull'unità di destinazione, viene calcolato md5. Usando questo md5, viene cercato il percorso di quel file sull'unità sorgente. Il file sull'unità di destinazione viene quindi spostato in modo che corrisponda al percorso del file sull'unità di origine.

Ci sono un paio di avvertenze con questo script:

  • Presuppone che ogni file in $ dst sia anche in $ src
  • Non rimuove alcuna directory da $ dst, sposta solo i file. Al momento non riesco a pensare a un modo sicuro per farlo automaticamente

Il calcolo degli md5 deve richiedere molto tempo: tutto il contenuto deve essere effettivamente letto. Mentre se Dan è sicuro che i file siano identici, semplicemente spostarli nella struttura della directory è molto veloce (nessuna lettura). Quindi, md5sumsembra non essere la cosa da usare qui. (A proposito, rsyncha una modalità in cui non calcola i checksum.)
imz - Ivan Zakharyaschev,

È un compromesso tra precisione e velocità. Volevo fornire un metodo che utilizzava un grado di precisione superiore rispetto ai semplici nomi di file.
Cledoux,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.