Adakah cara untuk menyinkronkan struktur direktori ketika file sudah ada di kedua sisi?

24

Saya memiliki dua drive dengan file yang sama, tetapi struktur direktori sama sekali berbeda.

Apakah ada cara untuk 'memindahkan' semua file di sisi tujuan sehingga mereka cocok dengan struktur sisi sumber? Dengan skrip mungkin?

Misalnya, drive A memiliki:

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

Sedangkan drive B memiliki:

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

File yang dimaksud sangat besar (800GB), jadi saya tidak ingin menyalinnya kembali; Saya hanya ingin menyinkronkan struktur dengan membuat direktori yang diperlukan dan memindahkan file.

Saya sedang memikirkan skrip rekursif yang akan menemukan setiap file sumber pada tujuan, kemudian memindahkannya ke direktori yang cocok, membuatnya jika perlu. Tapi - itu di luar kemampuan saya!

Solusi elegan lain diberikan di sini: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086

Dan
sumber
Apakah Anda yakin nama secara unik menentukan konten file, jika tidak, Anda harus mempertimbangkan membandingkan file dengan checksum mereka.
kasterma

Jawaban:

11

Saya akan pergi dengan Gilles dan mengarahkan Anda ke Unison seperti yang disarankan oleh hasen j . Serentak adalah DropBox 20 tahun sebelum DropBox. Mengguncang kode solid yang digunakan banyak orang (termasuk saya) setiap hari - sangat berharga untuk dipelajari. Tetap saja, joinperlu semua publisitas yang bisa didapatnya :)


Ini hanya setengah jawaban, tetapi saya harus kembali bekerja :)

Pada dasarnya, saya ingin mendemonstrasikan joinutilitas yang sedikit diketahui yang melakukan hal itu: bergabung dengan dua tabel pada suatu bidang.

Pertama, siapkan tempat uji termasuk nama file dengan spasi:

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

(edit beberapa direktori dan / atau nama file dalam new).

Sekarang, kami ingin membuat peta: hash -> nama file untuk setiap direktori dan kemudian gunakan joinuntuk mencocokkan file dengan hash yang sama. Untuk menghasilkan peta, masukkan yang berikut ini di makemap.sh:

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

makemap.sh memuntahkan file dengan garis-garis bentuk, 'hash "nama file"', jadi kami hanya bergabung di kolom pertama:

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

Ini menghasilkan moves.txtyang terlihat seperti ini:

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

Langkah selanjutnya adalah benar-benar melakukan gerakan, tetapi upaya saya macet pada mengutip ... mv -idan mkdir -pharus berguna.

Janus
sumber
Maaf, saya tidak mengerti semua ini!
Dan
1
joinsangat menarik. Terima kasih telah menyampaikannya pada saya.
Steven D
@Dan. Maaf. Masalahnya adalah saya tidak tahu asumsi apa yang bisa saya buat tentang nama file Anda. Menulis skrip tanpa asumsi tidak menyenangkan, terutama dalam kasus ini di mana saya memilih untuk menampilkan nama file ke file dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus
1
Ini mungkin menghabiskan banyak waktu (dan beban CPU) karena file-file besar ini harus dibaca sepenuhnya untuk membuat hash MD5. Jika nama file dan ukuran file cocok maka mungkin hash untuk hash file. Hashing harus dilakukan pada langkah kedua dan hanya untuk file yang cocok dengan setidaknya satu (pada disk yang sama) dalam nama atau ukuran.
Hauke ​​Laging
Apakah Anda tidak perlu mengurutkan file yang Anda gunakan sebagai joininput?
cjm
8

Ada utilitas yang disebut serempak:

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

Deskripsi dari situs:

Unison adalah alat sinkronisasi file untuk Unix dan Windows. Ini memungkinkan dua replika kumpulan file dan direktori untuk disimpan pada host yang berbeda (atau disk berbeda pada host yang sama), dimodifikasi secara terpisah, dan kemudian dimutakhirkan dengan menyebarkan perubahan dalam setiap replika ke replika yang lain.

Perhatikan bahwa Unison hanya mendeteksi file yang dipindahkan saat dijalankan pertama kali jika setidaknya salah satu root berada jauh, jadi meskipun Anda menyinkronkan file lokal, gunakan ssh://localhost/path/to/dirsebagai salah satu root.

Hasen
sumber
@Gilles: Apakah Anda yakin? Saya menggunakan serempak untuk semuanya dan sering melihatnya melihat file yang telah diubah namanya dan / atau dipindahkan jauh. Apakah Anda mengatakan bahwa ini hanya berfungsi untuk file yang sudah disinkronkan di mana serentak memiliki kesempatan untuk merekam nomor inode (atau trik apa pun yang digunakannya)?
Janus
@ Janus: Terima kasih atas koreksi, komentar saya memang salah. Serentak mendeteksi file yang dipindahkan, bahkan saat dijalankan awal. (Itu tidak melakukan ini ketika kedua akar adalah lokal, itulah sebabnya mengapa tidak melakukannya dalam pengujian saya.) Jadi serempak adalah saran yang sangat bagus.
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles. Baik untuk diketahui - tampaknya ada beberapa tempat di mana algoritme membedakan antara sinkronisasi lokal dan jarak jauh. Saya sebenarnya tidak berpikir itu akan berhasil untuk sinkronisasi pertama. +1 untuk serempak!
Janus
4

Gunakan Unison seperti yang disarankan oleh hasen j . Saya meninggalkan jawaban ini sebagai contoh skrip yang berpotensi berguna atau untuk digunakan pada server dengan hanya utilitas dasar yang diinstal.


Saya akan menganggap bahwa nama file unik di seluruh hierarki. Saya juga akan berasumsi bahwa tidak ada nama file berisi baris baru, dan bahwa pohon direktori hanya berisi direktori dan file biasa.

  1. Pertama mengumpulkan nama file di sisi sumber.

    (cd /A && find . \! -type d) >A.find
  2. Kemudian pindahkan file ke tempatnya di sisi tujuan. Pertama, buat pohon file rata di sisi tujuan. Gunakan lnalih-alih mvjika Anda ingin menyimpan tautan keras di hierarki lama.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Jika beberapa file mungkin tidak ada di tempat tujuan, buat yang sama rata /A.stagingdan gunakan rsync untuk menyalin data dari sumber ke tempat tujuan.

    rsync -au /A.staging/ /B.staging/
  4. Sekarang ganti nama file ke tempatnya.

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

    Setara:

    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. Terakhir, jika Anda peduli dengan metadata direktori, panggil rsync dengan file yang sudah ada.

    rsync -au /A/ /B.new/

Perhatikan bahwa saya belum menguji cuplikan di pos ini. Gunakan dengan risiko Anda sendiri. Silakan laporkan kesalahan dalam komentar.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
2

Terutama jika sinkronisasi yang sedang berlangsung akan berguna, Anda bisa mencoba mencari tahu git-lampiran .

Ini relatif baru; Saya belum mencoba menggunakannya sendiri.

Saya dapat menyarankannya karena ia menghindari menyimpan salinan kedua file ... ini berarti ia harus menandai file sebagai hanya-baca ("terkunci"), seperti sistem kontrol versi non-Git tertentu.

File diidentifikasi oleh sha256sum + ekstensi file (secara default). Jadi itu harus dapat menyinkronkan dua repo dengan konten file yang identik tetapi nama file yang berbeda, tanpa harus melakukan penulisan (dan melalui jaringan bandwidth rendah, jika diinginkan). Tentu saja harus membaca semua file untuk checksum mereka.

sourcejedi
sumber
1

Bagaimana dengan sesuatu yang seperti ini:

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

Ini mengasumsikan bahwa nama file yang ingin Anda sinkronkan adalah unik di seluruh drive: jika tidak, tidak dapat sepenuhnya otomatis (namun, Anda dapat memberikan prompt bagi pengguna untuk memilih file mana yang akan diambil jika ada lebih dari itu.)

Skrip di atas akan berfungsi dalam kasus-kasus sederhana, tetapi mungkin gagal jika namekebetulan mengandung simbol yang memiliki arti khusus untuk regexps. The greppada daftar file juga dapat mengambil banyak waktu jika ada banyak file. Anda dapat mempertimbangkan menerjemahkan kode ini untuk menggunakan hashtable yang akan memetakan nama file ke jalur, misalnya di Ruby.

alex
sumber
Ini terlihat menjanjikan - tetapi apakah itu memindahkan file, atau hanya membuat symlink?
Dan
Saya pikir saya mengerti sebagian besar dari ini; tapi apa yang dilakukan grepgaris? Apakah itu hanya menemukan path lengkap file yang cocok dstlist?
Dan
@ Dan: rupanya dengan menggunakan lnitu menciptakan symlink. Anda dapat menggunakan mvuntuk memindahkan file, tetapi jangan menimpa yang sudah ada. Juga, Anda mungkin ingin membersihkan direktori kosong jika ada, setelah memindahkan file. Ya, grepperintah itu mencari baris yang berakhir pada nama file, sehingga mengungkapkan path lengkap ke sana di drive tujuan.
alex
1

Dengan asumsi nama file dasar unik di pohon, itu cukup mudah:

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

Jika Anda ingin membersihkan direktori kosong yang lama, gunakan:

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

Saya juga menghadapi masalah ini. Solusi berbasis md5sum tidak bekerja untuk saya, karena saya menyinkronkan file saya ke webdavmount. Menghitung jumlah md5sum pada webdavtujuan juga berarti operasi file besar.

Saya membuat skrip kecil reorg_Remote_Dir_detect_moves.sh (di github) yang mencoba mendeteksi file yang paling banyak dipindahkan dan kemudian membuat skrip shell sementara baru dengan beberapa perintah untuk menyesuaikan direktori jarak jauh. Karena saya hanya menangani nama file, skrip bukanlah solusi yang sempurna.

Untuk keamanan, beberapa file akan diabaikan: A) File dengan nama yang sama (awal yang sama) di setiap sisi, dan B) File yang hanya ada di sisi jarak jauh. Mereka akan diabaikan dan dilewati.

File yang dilewati kemudian akan ditangani oleh alat sinkronisasi pilihan Anda (misalnya rsync, unison, ...), yang harus Anda gunakan setelah menjalankan skrip shell sementara.

Jadi mungkin skrip saya berguna untuk seseorang? Jika demikian (untuk membuatnya lebih jelas) ada tiga langkah:

  1. Jalankan skrip shell reorg_Remote_Dir_detect_moves.sh (di github)
  2. Ini akan membuat shell-script sementara /dev/shm/REORGRemoteMoveScript.sh=> jalankan ini untuk melakukan gerakan (akan cepat dipasang webdav)
  3. Jalankan alat sinkronisasi pilihan Anda (mis. rsync, unison, ...)
Aex Oquare
sumber
1

Inilah usaha saya untuk menjawab. Sebagai peringatan, semua pengalaman skrip saya berasal dari bash, jadi jika Anda menggunakan shell yang berbeda, nama perintah atau sintaks mungkin berbeda.

Solusi ini membutuhkan pembuatan dua skrip terpisah.

Skrip pertama ini bertanggung jawab untuk benar-benar memindahkan file pada drive tujuan.

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

Skrip kedua membuat file peta MD5 yang digunakan oleh skrip pertama dan kemudian memanggil skrip pertama pada setiap file di drive tujuan.

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

Pada dasarnya, apa yang terjadi adalah dua skrip yang sama dengan array asosiatif $md5_map_file. Pertama, semua md5 untuk file pada drive sumber dihitung dan disimpan. Terkait dengan md5 adalah jalur relatif dari root drive. Kemudian, untuk setiap file di drive tujuan, md5 dihitung. Menggunakan md5 ini, path file itu pada drive sumber terlihat. File pada drive tujuan kemudian dipindahkan untuk mencocokkan jalur file pada drive sumber.

Ada beberapa peringatan dengan skrip ini:

  • Diasumsikan bahwa setiap file dalam $ dst juga dalam $ src
  • Itu tidak menghapus direktori dari $ dst, hanya memindahkan file. Saat ini saya tidak dapat memikirkan cara aman untuk melakukan ini secara otomatis
cledoux
sumber
Perlu waktu lama untuk menghitung md5's: semua konten harus benar-benar dibaca. Sementara jika Dan yakin file-file itu identik, hanya memindahkannya dalam struktur direktori sangat cepat (tanpa membaca). Jadi, md5sumsepertinya bukan hal yang harus digunakan di sini. (BTW, rsyncmemiliki mode di mana ia tidak menghitung checksum.)
imz - Ivan Zakharyaschev
Ini adalah tradeoff antara akurasi dan kecepatan. Saya ingin memberikan metode yang menggunakan tingkat akurasi yang lebih tinggi daripada hanya nama file.
cledoux