¿Alguna forma de sincronizar la estructura de directorios cuando los archivos ya están en ambos lados?


24

Tengo dos unidades con los mismos archivos, pero la estructura del directorio es totalmente diferente.

¿Hay alguna forma de 'mover' todos los archivos en el lado de destino para que coincidan con la estructura del lado de origen? ¿Con un guión quizás?

Por ejemplo, la unidad A tiene:

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

Mientras que la unidad B tiene:

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

Los archivos en cuestión son enormes (800 GB), por lo que no quiero volver a copiarlos; Solo quiero sincronizar la estructura creando los directorios necesarios y moviendo los archivos.

Estaba pensando en un script recursivo que encontraría cada archivo de origen en el destino, luego lo movería a un directorio coincidente, creándolo si fuera necesario. Pero, ¡eso está más allá de mis habilidades!

Aquí se dio otra solución elegante: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086


¿Está seguro de que el nombre determina de manera única el contenido de un archivo? De lo contrario, debería considerar comparar archivos por su suma de comprobación.
kasterma

Respuestas:


11

Voy a ir con Gilles y señalarle a Unison como sugiere j hasen . Unison fue DropBox 20 años antes de DropBox. Código sólido que muchas personas (incluido yo mismo) usamos todos los días, vale la pena aprenderlo. Aún así, joinnecesita toda la publicidad que pueda obtener :)


Esta es solo la mitad de una respuesta, pero tengo que volver al trabajo :)

Básicamente, quería demostrar la joinutilidad poco conocida que hace justamente eso: une dos tablas en algún campo.

Primero, configure un caso de prueba que incluya nombres de archivos con espacios:

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

(edite algunos nombres de directorio y / o archivo en new).

Ahora, queremos construir un mapa: hash -> nombre de archivo para cada directorio y luego usarlo joinpara hacer coincidir los archivos con el mismo hash. Para generar el mapa, ponga lo siguiente en makemap.sh:

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

makemap.sh escupe un archivo con líneas del formulario, 'hash "nombre de archivo"', así que solo nos unimos en la primera columna:

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

Esto genera lo moves.txtque se ve así:

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

El siguiente paso sería hacer los movimientos, pero mis intentos se atascaron en las citas ... mv -iy mkdir -pdeberían ser útiles.


Lo siento, no entiendo nada de esto!
Dan

1
joinEs realmente interesante. Gracias por llamar mi atención.
Steven D

@Dan. Lo siento. El problema es que no sé qué suposiciones puedo hacer sobre los nombres de sus archivos. Las secuencias de comandos sin suposiciones no son divertidas, especialmente en este caso donde elegí enviar los nombres de archivo a un archivo dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus

1
Esto probablemente desperdicia mucho tiempo (y carga de CPU) porque estos archivos enormes deben leerse por completo para crear los hash MD5. Si el nombre y el tamaño del archivo coinciden, entonces probablemente sea excesivo hacer hash los archivos. El hash debe hacerse en un segundo paso y solo para los archivos que coinciden con al menos uno (en el mismo disco) en nombre o tamaño.
Hauke ​​Laging

¿No necesita ordenar los archivos que usa como joinentrada?
cjm

8

Hay una utilidad llamada unísono:

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

Descripción del sitio:

Unison es una herramienta de sincronización de archivos para Unix y Windows. Permite que dos réplicas de una colección de archivos y directorios se almacenen en diferentes hosts (o diferentes discos en el mismo host), se modifiquen por separado y luego se actualicen propagando los cambios en cada réplica a la otra.

Tenga en cuenta que Unison solo detecta archivos movidos en la primera ejecución si al menos una de las raíces es remota, por lo que incluso si está sincronizando archivos locales, úsela ssh://localhost/path/to/dircomo una de las raíces.


@Gilles: ¿Estás seguro? Uso unísono para todo y, a menudo, veo que detecta archivos que han sido renombrados y / o movidos lejos. ¿Estás diciendo que esto solo funciona para archivos ya sincronizados donde el unísono ha tenido la oportunidad de registrar números de inodo (o cualquier otro truco que use)?
Janus

@ Janus: Gracias por la corrección, mi comentario fue realmente erróneo. Unison detecta archivos que se movieron, incluso en la ejecución inicial. (No hace esto cuando ambas raíces son locales, por lo que no lo hizo en mi prueba). Así que unísono es una muy buena sugerencia.
Gilles 'SO- deja de ser malvado'

@Gilles. Es bueno saberlo: parece haber bastantes lugares donde el algoritmo distingue entre sincronizaciones locales y remotas. En realidad, no pensé que funcionaría para la primera sincronización. +1 por unísono!
Janus

4

Use Unison como lo sugiere hasen j . Dejo esta respuesta como un ejemplo de scripts potencialmente útil o para usar en un servidor con solo utilidades básicas instaladas.


Asumiré que los nombres de los archivos son únicos en toda la jerarquía. También supondré que ningún nombre de archivo contiene una nueva línea, y que los árboles de directorios solo contienen directorios y archivos normales.

  1. Primero recopile los nombres de archivo en el lado de origen.

    (cd /A && find . \! -type d) >A.find
  2. Luego mueva los archivos a su lugar en el lado de destino. Primero, cree un árbol de archivos aplanado en el lado de destino. Úselo en lnlugar de mvsi desea mantener los enlaces duros en la antigua jerarquía.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Si faltan algunos archivos en el destino, cree un archivo plano similar /A.stagingy use rsync para copiar los datos del origen al destino.

    rsync -au /A.staging/ /B.staging/
  4. Ahora cambie el nombre de los archivos a su lugar.

    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. Finalmente, si le interesan los metadatos de los directorios, llame a rsync con los archivos que ya están en su lugar.

    rsync -au /A/ /B.new/

Tenga en cuenta que no he probado los fragmentos en esta publicación. Úselo bajo su propio riesgo. Por favor reporte cualquier error en un comentario.


2

Particularmente, si la sincronización continua sería útil, podría intentar descubrir git-annex .

Es relativamente nuevo; No he tratado de usarlo yo mismo.

Puedo sugerirlo porque evita mantener una segunda copia de los archivos ... esto significa que tiene que marcar los archivos como de solo lectura ("bloqueado"), como ciertos sistemas de control de versiones que no son Git.

Los archivos se identifican mediante la extensión de archivo sha256sum + (de forma predeterminada). Por lo tanto, debería poder sincronizar dos repositorios con contenido de archivo idéntico pero nombres de archivo diferentes, sin tener que realizar escrituras (y en una red de bajo ancho de banda, si lo desea). Por supuesto, tendrá que leer todos los archivos para poder sumarlos.


1

Qué tal algo como esto:

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

Esto supone que los nombres de los archivos que desea sincronizar son únicos en todo el disco: de lo contrario, no hay forma de que pueda automatizarse por completo (sin embargo, puede proporcionar un aviso para que el usuario elija qué archivo elegir si hay más).

La secuencia de comandos anterior funcionará en casos simples, pero puede fallar si namecontiene símbolos que tienen un significado especial para expresiones regulares. La greplista de archivos también puede llevar mucho tiempo si hay muchos archivos. Puede considerar traducir este código para usar hashtable que asignará nombres de archivos a rutas, por ejemplo, en Ruby.


Esto parece prometedor, pero ¿mueve los archivos o simplemente crea enlaces simbólicos?
Dan

Creo que entiendo la mayor parte de esto; ¿Pero qué hace la greplínea? ¿Solo encuentra la ruta completa del archivo correspondiente dstlist?
Dan

@Dan: aparentemente por el uso de lnesto crea enlaces simbólicos. Puede emplear mvpara mover los archivos, pero tenga cuidado de sobrescribir los existentes. Además, es posible que desee limpiar directorios vacíos, si los hay, después de alejar los archivos. Sí, ese grepcomando busca una línea que termina en el nombre del archivo, revelando así la ruta completa en la unidad de destino.
alex

1

Asumiendo que los nombres de los archivos base son únicos en los árboles, es bastante sencillo:

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 desea limpiar los viejos directorios vacíos, use:

find B -depth -type d -delete

1

También me enfrenté a este problema. La solución basada en md5sum no funcionó para mí, porque sincronizo mis archivos con un webdavmontaje. Calcular sumas md5sum en el webdavdestino también significaría operaciones de archivos grandes.

Hice un pequeño script reorg_Remote_Dir_detect_moves.sh (en github) que intenta detectar los archivos más movidos y luego crea un nuevo script de shell temporal con varios comandos para ajustar el directorio remoto. Como solo cuido los nombres de archivo, el script no es la solución perfecta.

Por seguridad, se ignorarán varios archivos: A) Archivos con los mismos nombres (del mismo comienzo) en cada lado, y B) Archivos que solo están en el lado remoto. Serán ignorados y omitidos.

Los archivos omitidos serán manejados por su herramienta de sincronización preferida (por ejemplo rsync, unison, ...), que debe usar después de ejecutar el script de shell temporal.

¿Entonces mi guión es útil para alguien? Si es así (para que quede más claro) hay tres pasos:

  1. Ejecute el script de shell reorg_Remote_Dir_detect_moves.sh (en github)
  2. Esto creará el script de shell temporal /dev/shm/REORGRemoteMoveScript.sh=> ejecutar esto para hacer los movimientos (será rápido en montado webdav)
  3. Ejecute su herramienta de sincronización preferida (por ejemplo rsync, unison, ...)

1

Aquí está mi intento de respuesta. Como advertencia, toda mi experiencia en scripts proviene de bash, por lo que si está utilizando un shell diferente, los nombres de comandos o la sintaxis pueden ser diferentes.

Esta solución requiere la creación de dos scripts separados.

Este primer script es responsable de mover los archivos en la unidad de destino.

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

El segundo script crea el archivo de mapa md5 usado por el primer script y luego llama al primer script en cada archivo en la unidad de destino.

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

Básicamente, lo que está sucediendo es que los dos scripts simulan una matriz asociativa con $md5_map_file. Primero, se calculan y almacenan todos los md5 para los archivos en la unidad fuente. Asociados con los md5 están las rutas relativas desde la raíz de la unidad. Luego, para cada archivo en la unidad de destino, se calcula el md5. Usando este md5, se busca la ruta de ese archivo en la unidad de origen. El archivo en la unidad de destino se mueve para que coincida con la ruta del archivo en la unidad de origen.

Hay un par de advertencias con este script:

  • Se supone que cada archivo en $ dst también está en $ src
  • No elimina ningún directorio de $ dst, solo mueve los archivos. Actualmente no puedo pensar en una forma segura de hacer esto automáticamente

Debe tomar mucho tiempo calcular los md5: todo el contenido debe leerse. Si bien Dan está seguro de que los archivos son idénticos, simplemente moverlos en la estructura del directorio es muy rápido (sin lectura). Entonces, md5sumparece que no es lo que hay que usar aquí. (Por cierto, rsynctiene un modo en el que no calcula sumas de verificación.)
imz - Ivan Zakharyaschev

Es una compensación entre precisión y velocidad. Quería proporcionar un método que utilizara un mayor grado de precisión que simplemente nombres de archivo.
cledoux
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.