Qualquer maneira de sincronizar a estrutura de diretórios quando os arquivos já estão em ambos os lados?

24

Eu tenho duas unidades com os mesmos arquivos, mas a estrutura de diretórios é totalmente diferente.

Existe alguma maneira de "mover" todos os arquivos no lado do destino para que eles correspondam à estrutura do lado da fonte? Com um script talvez?

Por exemplo, a unidade A tem:

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

Considerando que a unidade B tem:

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

Os arquivos em questão são enormes (800 GB), então não quero copiá-los novamente; Eu só quero sincronizar a estrutura, criando os diretórios necessários e movendo os arquivos.

Eu estava pensando em um script recursivo que encontraria cada arquivo de origem no destino e, em seguida, movê-lo para um diretório correspondente, criando-o, se necessário. Mas isso está além das minhas habilidades!

Outra solução elegante foi dada aqui: link

    
por Ze'ev 25.01.2011 / 02:33

8 respostas

11

Eu irei com Gilles e apontarei você para o Unison, como sugerido por hasen j . Unison foi DropBox 20 anos antes do DropBox. Código sólido de rock que muitas pessoas (inclusive eu) usam todos os dias - vale muito a pena aprender. Ainda assim, join precisa de toda a publicidade possível:

Esta é apenas meia resposta, mas tenho que voltar ao trabalho:)

Basicamente, eu queria demonstrar o pouco conhecido join utility que faz exatamente isso: junta duas tabelas em algum campo.

Primeiro, configure um caso de teste incluindo nomes de arquivos com espaços:

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

(edite alguns nomes de diretório e / ou arquivos em new ).

Agora, queremos criar um mapa: hash - > nome de arquivo para cada diretório e, em seguida, use join para corresponder arquivos com o mesmo hash. Para gerar o mapa, coloque o seguinte em makemap.sh :

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

makemap.sh exibe um arquivo com linhas no formulário, 'hash "nome do arquivo"', portanto, apenas participamos da primeira coluna:

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

Isso gera moves.txt , que é assim:

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

O próximo passo seria realmente fazer as jogadas, mas minhas tentativas ficaram nas citações ... mv -i e mkdir -p devem ser úteis.

    
por 25.01.2011 / 09:55
8

Existe um utilitário chamado unison:

link

Descrição do site:

Unison is a file-synchronization tool for Unix and Windows. It allows two replicas of a collection of files and directories to be stored on different hosts (or different disks on the same host), modified separately, and then brought up to date by propagating the changes in each replica to the other.

Observe que o Unison detecta apenas arquivos movidos na primeira execução se pelo menos uma das raízes for remota, portanto, mesmo que você esteja sincronizando arquivos locais, use ssh://localhost/path/to/dir como uma das raízes.

    
por 27.01.2011 / 05:28
4

Use o Unison como sugerido por hasen j . Estou deixando esta resposta como um exemplo de script potencialmente útil ou para uso em um servidor com apenas utilitários básicos instalados.

Assumirei que os nomes dos arquivos são exclusivos em toda a hierarquia. Também assumirei que nenhum nome de arquivo contém uma nova linha e que as árvores de diretórios contêm apenas diretórios e arquivos regulares.

  1. Primeiro, colete os nomes dos arquivos no lado da fonte.

    (cd /A && find . \! -type d) >A.find
    
  2. Em seguida, mova os arquivos para o local de destino. Primeiro, crie uma árvore de arquivos achatada no lado do destino. Use ln em vez de mv se você quiser manter os links físicos na hierarquia antiga.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
    
  3. Se alguns arquivos estiverem faltando no destino, crie um /A.staging semelhante e use o rsync para copiar os dados da origem para o destino.

    rsync -au /A.staging/ /B.staging/
    
  4. Agora renomeie os arquivos para o 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/$_: $!"
    '
    

    Equivalente:

    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, se você se preocupa com os metadados dos diretórios, chame o rsync com os arquivos já existentes.

    rsync -au /A/ /B.new/
    

Observe que eu não testei os snippets nesta postagem. Use por sua conta e risco. Por favor, informe qualquer erro em um comentário.

    
por 26.01.2011 / 21:58
2

Particularmente, se a sincronização contínua for útil, você pode tentar descobrir git-annex .

É relativamente novo; Eu não tentei usar isso sozinho.

Eu sou capaz de sugerir isso porque evita manter uma segunda cópia dos arquivos ... isso significa que ele tem que marcar os arquivos como somente leitura ("bloqueado"), como certos sistemas de controle de versão não-Git.

Os arquivos são identificados pela extensão de arquivo sha256sum + (por padrão). Portanto, ele deve ser capaz de sincronizar dois repositórios com conteúdo de arquivo idêntico, mas nomes de arquivo diferentes, sem precisar executar gravações (e em uma rede de baixa largura de banda, se desejado). Obviamente, terá que ler todos os arquivos para poder verificá-los.

    
por 27.02.2013 / 14:20
1

Que tal algo como isto:

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

Isso pressupõe que os nomes dos arquivos que você deseja sincronizar sejam exclusivos em toda a unidade: caso contrário, não há como ser totalmente automatizado (no entanto, você pode fornecer uma solicitação para o usuário escolher qual arquivo escolher se houver mais um.)

O script acima funcionará em casos simples, mas poderá falhar se name contiver símbolos que tenham significado especial para expressões regulares. A grep na lista de arquivos também pode levar muito tempo se houver muitos arquivos. Você pode considerar a tradução deste código para usar hashtable que mapeará os nomes dos arquivos para os caminhos, por exemplo, em Ruby.

    
por 25.01.2011 / 08:10
1

Aqui está minha tentativa de resposta. Como um aviso prévio, toda a minha experiência de script vem do bash, então se você estiver usando um shell diferente, os nomes dos comandos ou a sintaxe podem ser diferentes.

Esta solução requer a criação de dois scripts separados.

Este primeiro script é responsável por realmente mover os arquivos na unidade 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

O segundo script cria o arquivo de mapeamento md5 usado pelo primeiro script e, em seguida, chama o primeiro script em todos os arquivos da unidade 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 '{}' \; 

Basicamente, o que está acontecendo é que os dois scripts similam um array associativo com $md5_map_file . Primeiro, todos os md5s para os arquivos na unidade de origem são computados e armazenados. Associado ao md5s estão os caminhos relativos da raiz da unidade. Então, para cada arquivo na unidade de destino, o md5 é calculado. Usando esse md5, o caminho desse arquivo na unidade de origem é consultado. O arquivo na unidade de destino é então movido para corresponder ao caminho do arquivo na unidade de origem.

Há algumas ressalvas com este script:

  • Ele assume que todos os arquivos em $ dst também estão em $ src
  • Ele não remove nenhum diretório de $ dst, apenas move os arquivos. No momento, não consigo pensar em uma maneira segura de fazer isso automaticamente

Boa sorte e espero que isso tenha ajudado.

    
por 26.01.2011 / 19:04
1

Supondo que os nomes de arquivos base sejam únicos nas árvores, é bastante simples:

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 você quiser limpar os antigos diretórios vazios, use:

find B -depth -type d -delete
    
por 03.03.2016 / 13:27
1

Eu também enfrentei esse problema. A solução baseada em md5sum não funcionou para mim, porque eu sincronizo meus arquivos com uma webdav mount. A computação de somas do md5sum no destino webdav também significaria operações de arquivos grandes.

Eu fiz um pequeno script reorg_Remote_Dir_detect_moves.sh (no github) que está tentando detectar os arquivos mais mais movidos e, em seguida, cria um novo script de shell temporário com vários comandos para ajustar o diretório remoto. Como eu cuido apenas dos nomes dos arquivos, o script não é uma solução perfeita.

Por motivos de segurança, vários arquivos serão ignorados: A) Arquivos com os mesmos nomes (do mesmo começo) em todos os lados, e B) Arquivos que estão apenas no lado remoto. Eles serão ignorados e ignorados.

Os arquivos ignorados serão manipulados pela sua ferramenta de sincronização preferida (por exemplo, rsync, unison , ...), que você deve usar depois de executar o script de shell temporário.

Então, talvez meu roteiro seja útil para alguém? Se sim (para deixar mais claro), existem três etapas:

  1. Execute o script de shell reorg_Remote_Dir_detect_moves.sh (no github)
  2. Isso criará o script de shell temporário /dev/shm/REORGRemoteMoveScript.sh = > execute isto para fazer os movimentos (será rápido no montado webdav )
  3. Execute sua ferramenta de sincronização preferida (por exemplo, rsync, unison , ...)
por 03.03.2016 / 11:48