Alguma maneira de sincronizar a estrutura de diretórios quando os arquivos já estiverem nos dois 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 de destino para que eles correspondam à estrutura do lado de origem? Com um roteiro, talvez?

Por exemplo, a unidade A possui:

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

Enquanto a unidade B possui:

/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 encontrasse cada arquivo de origem no destino e depois o movesse para um diretório correspondente, criando-o, se necessário. Mas - isso está além das minhas habilidades!

Outra solução elegante foi fornecida aqui: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086

Dan
fonte
Você tem certeza de que o nome determina exclusivamente o conteúdo de um arquivo; caso contrário, considere comparar os arquivos por suas somas de verificação.
kasterma

Respostas:

11

Vou com Gilles e aponto para Unison, como sugerido por hasen j . O Unison foi o DropBox 20 anos antes do DropBox. Código sólido que muitas pessoas (inclusive eu) usam todos os dias - vale a pena aprender. Ainda assim, joinprecisa de toda a publicidade que puder obter :)


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

Basicamente, eu queria demonstrar o joinutilitário pouco conhecido que faz exatamente isso: junta duas tabelas em um determinado 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 arquivo new).

Agora, queremos criar um mapa: hash -> nome do arquivo para cada diretório e, em seguida, usar joinpara combinar 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}\/\(.*\)/\1 \"\2\"/" \

makemap.sh cospe um arquivo com as linhas do formulário, 'hash "filename"', então juntamos a primeira coluna:

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

Isso gera o moves.txtseguinte:

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 presas na citação ... mv -ie mkdir -pdevem ser úteis.

Janus
fonte
Desculpe, eu não entendo nada disso!
Dan
11
joiné realmente interessante. Obrigado por chamar minha atenção.
Steven D
@Dan. Desculpe. O problema é que não sei quais suposições posso fazer sobre os nomes dos seus arquivos. Criar scripts sem suposições não é divertido, especialmente neste caso em que escolhi enviar os nomes dos arquivos para um arquivo dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus
11
Isso provavelmente desperdiça muito tempo (e carga da CPU) porque esses arquivos enormes precisam ser lidos completamente para criar os hashes MD5. Se o nome e o tamanho do arquivo corresponderem, provavelmente será um exagero fazer o hash dos arquivos. O hash deve ser feito em uma segunda etapa e apenas para os arquivos que correspondem a pelo menos um (no mesmo disco) em nome ou tamanho.
Hauke ​​Laging
Você não precisa classificar os arquivos que você usa como joinentrada?
Cjm 08/10/2013
8

Há um utilitário chamado uníssono:

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

Descrição do site:

Unison é uma ferramenta de sincronização de arquivos para Unix e Windows. Ele permite que duas réplicas de uma coleção de arquivos e diretórios sejam armazenadas em hosts diferentes (ou discos diferentes no mesmo host), modificadas separadamente e atualizadas, propagando as alterações em cada réplica para a outra.

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-as ssh://localhost/path/to/dircomo uma das raízes.

hasen
fonte
@Gilles: Você tem certeza? Eu uso o uníssono para tudo e frequentemente o vejo detectando arquivos que foram renomeados e / ou movidos para longe. Você está dizendo que isso funciona apenas para arquivos já sincronizados, onde o uníssono teve a chance de gravar números de inode (ou quaisquer outros truques que ele usa)?
Janus
@ Janus: Obrigado pela correção, meu comentário estava realmente errado. O Unison detecta arquivos que foram movidos, mesmo na execução inicial. (Não faz isso quando as duas raízes são locais, e é por isso que não o fez no meu teste.) Portanto, o uníssono é uma sugestão muito boa.
Gilles 'SO- stop be evil'
@Gilles. É bom saber - parece haver alguns lugares em que o algoritmo distingue entre sincronizações locais e remotas. Na verdade, não achei que funcionaria para a primeira sincronização. +1 por uníssono!
Janus
4

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


Suponho que os nomes dos arquivos sejam únicos em toda a hierarquia. Também assumirei que nenhum nome de arquivo contém uma nova linha e que as árvores de diretório 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 no lugar no lado do destino. Primeiro, crie uma árvore de arquivos achatada no lado do destino. Use em lnvez de mvse você deseja 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 ausentes no destino, crie um nivelamento similar /A.staginge use o rsync para copiar os dados da origem para o destino.

    rsync -au /A.staging/ /B.staging/
  4. Agora renomeie os arquivos no 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. Por fim, se você se importa com os metadados dos diretórios, chame rsync com os arquivos já existentes.

    rsync -au /A/ /B.new/

Observe que não testei os trechos desta postagem. Use por sua conta e risco. Por favor, reporte qualquer erro em um comentário.

Gilles 'SO- parar de ser mau'
fonte
2

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

É relativamente novo; Eu não tentei usá-lo sozinho.

Eu posso sugerir isso porque evita manter uma segunda cópia dos arquivos ... isso significa que ele precisa marcar os arquivos como somente leitura ("bloqueados"), como certos sistemas de controle de versão que não são do Git.

Os arquivos são identificados pela extensão sha256sum + (por padrão). Portanto, ele deve ser capaz de sincronizar dois repositórios com conteúdo de arquivo idêntico, mas com nomes de arquivos 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 soma de verificação.

sourcejedi
fonte
1

Que tal algo como isso:

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 únicos em toda a unidade: caso contrário, não há como ele ser totalmente automatizado (no entanto, você pode fornecer um prompt para o usuário escolher qual arquivo escolher, se houver mais).

O script acima funcionará em casos simples, mas poderá falhar se namecontiver símbolos que tenham um significado especial para regexps. A greplista de arquivos também pode levar muito tempo se houver muitos arquivos. Você pode traduzir esse código para usar hashtable, que mapeará nomes de arquivos para caminhos, por exemplo, em Ruby.

alex
fonte
Parece promissor - mas move os arquivos ou apenas cria links simbólicos?
Dan
Eu acho que entendi a maior parte disso; mas o que a greplinha faz? Apenas encontra o caminho completo do arquivo correspondente dstlist?
Dan
@ Dan: aparentemente, pelo uso lndele cria links simbólicos. Você pode empregar mvpara mover os arquivos, mas cuidado com a substituição dos arquivos existentes. Além disso, convém limpar os diretórios vazios, se houver, depois de mover os arquivos. Sim, esse grepcomando procura uma linha que termine no nome do arquivo, revelando assim o caminho completo para ele na unidade de destino.
alex
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ê deseja limpar os diretórios vazios antigos, use:

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

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

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

Por 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 tratados pela sua ferramenta de sincronização preferida (por exemplo rsync, unison, ...), que você deverá usar após executar o shell-script temporário.

Talvez meu script seja útil para alguém? Nesse caso (para tornar mais claro), existem três etapas:

  1. Execute o script de shell reorg_Remote_Dir_detect_moves.sh (no github)
  2. Isso criará o shell-script temporário /dev/shm/REORGRemoteMoveScript.sh=> execute isso para executar as movimentações (será rápido na montagem webdav)
  3. Execute sua ferramenta de sincronização preferida (por exemplo rsync, unison, ...)
Aex Oquare
fonte
1

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

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

Esse primeiro script é responsável por 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 mapa md5 usado pelo primeiro script e chama o primeiro script em todos os arquivos na 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 simulam uma matriz associativa $md5_map_file. Primeiro, todos os MD5s para os arquivos na unidade de origem são calculados e armazenados. Associados aos md5s estão os caminhos relativos da raiz da unidade. Em seguida, para cada arquivo na unidade de destino, o md5 é calculado. Usando esse md5, o caminho desse arquivo na unidade de origem é pesquisado. O arquivo na unidade de destino é então movido para corresponder ao caminho do arquivo na unidade de origem.

Existem algumas ressalvas neste script:

  • Ele pressupõe que todos os arquivos em $ dst também estejam em $ src
  • Ele não remove nenhum diretório do $ dst, apenas move os arquivos. No momento, não consigo pensar em uma maneira segura de fazer isso automaticamente
cledoux
fonte
Demora um longo tempo para calcular os md5: todo o conteúdo deve ser realmente lido. Embora Dan tenha certeza de que os arquivos são idênticos, simplesmente movê-los na estrutura de diretórios é muito rápido (sem leitura). Então, md5sumparece não ser o item a ser usado aqui. (BTW, rsynctem um modo no qual não calcula somas de verificação).
imz - Ivan Zakharyaschev
É uma troca entre precisão e velocidade. Eu queria fornecer um método que usasse um grau de precisão mais alto do que simplesmente nomes de arquivos.
Cledoux