Bất kỳ cách nào để đồng bộ cấu trúc thư mục khi các tệp đã ở cả hai bên?


24

Tôi có hai ổ đĩa với cùng một tệp, nhưng cấu trúc thư mục hoàn toàn khác nhau.

Có cách nào để 'di chuyển' tất cả các tệp ở phía đích để chúng khớp với cấu trúc của phía nguồn không? Với một kịch bản có lẽ?

Ví dụ: ổ A có:

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

Trong khi ổ B có:

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

Các tệp trong câu hỏi rất lớn (800 GB), vì vậy tôi không muốn sao chép lại chúng; Tôi chỉ muốn đồng bộ cấu trúc bằng cách tạo các thư mục cần thiết và di chuyển các tệp.

Tôi đã nghĩ đến một tập lệnh đệ quy sẽ tìm thấy từng tệp nguồn trên đích, sau đó di chuyển nó vào một thư mục phù hợp, tạo nó nếu cần thiết. Nhưng - điều đó vượt quá khả năng của tôi!

Một giải pháp tao nhã khác đã được đưa ra ở đây: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-al yet-on-both-sides / 218086


Bạn có chắc chắn rằng tên xác định duy nhất nội dung của một tệp, nếu không, bạn nên xem xét so sánh các tệp bằng tổng kiểm tra của chúng.
kasterma

Câu trả lời:


11

Tôi sẽ đi cùng Gilles và chỉ cho bạn Unison theo đề xuất của hasen j . Unison là DropBox 20 năm trước DropBox. Mã rắn mà rất nhiều người (bao gồm cả tôi) sử dụng hàng ngày - rất đáng để học hỏi. Tuy nhiên, joincần tất cả các công khai nó có thể nhận được :)


Đây chỉ là một nửa câu trả lời, nhưng tôi phải quay lại làm việc :)

Về cơ bản, tôi muốn chứng minh jointiện ích ít được biết đến đó là: tham gia hai bảng trên một trường nào đó.

Đầu tiên, thiết lập một trường hợp thử nghiệm bao gồm tên tệp có khoảng trắng:

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

(chỉnh sửa một số thư mục và / hoặc tên tệp trong new).

Bây giờ, chúng tôi muốn xây dựng một bản đồ: hash -> tên tệp cho mỗi thư mục và sau đó sử dụng joinđể khớp các tệp có cùng hàm băm. Để tạo bản đồ, hãy đặt như sau makemap.sh:

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

makemap.sh tạo ra một tệp có các dòng có dạng, 'băm "tên tệp"', vì vậy chúng tôi chỉ cần tham gia vào cột đầu tiên:

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

Điều này tạo ra moves.txttrông như thế này:

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

Bước tiếp theo sẽ là thực sự thực hiện các động tác, nhưng những nỗ lực của tôi đã bị kẹt khi trích dẫn ... mv -imkdir -psẽ trở nên hữu ích.


Xin lỗi, tôi không hiểu gì về điều này!
Dan

1
jointhực sự thú vị Cám ơn bạn đã khiến tôi chú ý tới việc này.
Steven D

@Dan. Lấy làm tiếc. Vấn đề là tôi không biết những giả định nào tôi có thể đưa ra về tên tệp của bạn. Viết kịch bản mà không có giả định là không vui, đặc biệt trong trường hợp này tôi đã chọn xuất tên tệp thành tệp dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus

1
Điều này có thể lãng phí rất nhiều thời gian (và tải CPU) vì những tệp khổng lồ này phải được đọc hoàn toàn để tạo băm MD5. Nếu tên tệp và kích thước tệp khớp nhau thì có lẽ quá mức cần thiết để băm các tệp. Băm nên được thực hiện trong bước thứ hai và chỉ cho các tệp khớp với ít nhất một (trên cùng một đĩa) về tên hoặc kích thước.
Hauke ​​Laging

Bạn không cần phải sắp xếp các tập tin bạn sử dụng làm joinđầu vào?
cjm

8

Có một tiện ích gọi là unison:

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

Mô tả từ trang web:

Unison là một công cụ đồng bộ hóa tệp cho Unix và Windows. Nó cho phép hai bản sao của một tập hợp các tệp và thư mục được lưu trữ trên các máy chủ khác nhau (hoặc các đĩa khác nhau trên cùng một máy chủ), được sửa đổi riêng lẻ, sau đó được cập nhật bằng cách truyền các thay đổi trong từng bản sao sang các bản sao khác.

Lưu ý rằng Unison chỉ phát hiện các tệp đã di chuyển trong lần chạy đầu tiên nếu ít nhất một trong số các gốc là từ xa, vì vậy ngay cả khi bạn đang đồng bộ hóa các tệp cục bộ, hãy sử dụng ssh://localhost/path/to/dirlàm một trong các gốc.


@Gilles: Bạn có chắc không? Tôi sử dụng unison cho tất cả mọi thứ và thường thấy nó phát hiện các tệp đã được đổi tên và / hoặc di chuyển ra xa. Bạn có nói rằng điều này chỉ hoạt động đối với các tệp đã được đồng bộ hóa trong đó unison đã có cơ hội ghi lại các số inode (hoặc bất kỳ thủ thuật nào khác mà nó sử dụng)?
Janus

@Janus: Cảm ơn đã sửa chữa, nhận xét của tôi thực sự sai. Unison không phát hiện các tệp đã được di chuyển, ngay cả trên lần chạy đầu tiên. (Nó không làm điều này khi cả hai gốc là cục bộ, đó là lý do tại sao nó không làm điều đó trong thử nghiệm của tôi.) Vì vậy, unison là một gợi ý rất tốt.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles. Điều cần biết - dường như có khá nhiều nơi thuật toán phân biệt giữa đồng bộ cục bộ và từ xa. Tôi thực sự không nghĩ rằng nó sẽ hoạt động cho đồng bộ hóa đầu tiên. +1 cho unison!
Janus

4

Sử dụng Unison theo đề xuất của hasen j . Tôi để lại câu trả lời này như một ví dụ kịch bản có khả năng hữu ích hoặc để sử dụng trên máy chủ chỉ cài đặt các tiện ích cơ bản.


Tôi sẽ giả sử rằng tên tệp là duy nhất trong toàn bộ phân cấp. Tôi cũng sẽ giả sử rằng không có tên tệp nào chứa dòng mới và cây thư mục chỉ chứa các thư mục và tệp thông thường.

  1. Đầu tiên thu thập tên tập tin ở phía nguồn.

    (cd /A && find . \! -type d) >A.find
  2. Sau đó di chuyển các tập tin vào vị trí ở phía đích. Đầu tiên, tạo một cây dẹt các tập tin ở phía đích. Sử dụng lnthay vì mvnếu bạn muốn giữ các liên kết cứng xung quanh trong hệ thống phân cấp cũ.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Nếu một số tệp có thể bị thiếu ở đích, hãy tạo một mặt phẳng tương tự /A.stagingvà sử dụng rsync để sao chép dữ liệu từ nguồn vào đích.

    rsync -au /A.staging/ /B.staging/
  4. Bây giờ đổi tên các tập tin vào vị trí.

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

    Tương đương:

    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. Cuối cùng, nếu bạn quan tâm đến siêu dữ liệu của các thư mục, hãy gọi rsync với các tệp đã có.

    rsync -au /A/ /B.new/

Lưu ý rằng tôi đã không kiểm tra các đoạn trong bài viết này. Sử dụng có nguy cơ của riêng bạn. Vui lòng báo cáo bất kỳ lỗi trong một bình luận.


2

Đặc biệt nếu đồng bộ hóa đang diễn ra sẽ hữu ích, bạn có thể thử tìm ra git-annex .

Nó tương đối mới; Tôi đã không cố gắng sử dụng nó cho mình.

Tôi có thể đề xuất nó vì nó tránh giữ một bản sao thứ hai của các tệp ... điều này có nghĩa là nó phải đánh dấu các tệp là chỉ đọc ("bị khóa"), giống như các hệ thống kiểm soát phiên bản không phải Git nhất định.

Các tệp được xác định bởi phần mở rộng tệp sha256sum + (theo mặc định). Vì vậy, nó có thể đồng bộ hai repos với nội dung tệp giống hệt nhau nhưng tên tệp khác nhau, mà không phải thực hiện ghi (và qua mạng băng thông thấp, nếu muốn). Tất nhiên nó sẽ phải đọc tất cả các tập tin để kiểm tra chúng.


1

Còn những thứ như thế này thì sao:

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

Điều này giả định rằng tên của các tệp bạn muốn đồng bộ hóa là duy nhất trên toàn bộ ổ đĩa: nếu không, không có cách nào nó có thể hoàn toàn tự động (tuy nhiên, bạn có thể cung cấp lời nhắc cho người dùng chọn tệp nào để chọn nếu có thêm tệp đó.)

Kịch bản trên sẽ hoạt động trong các trường hợp đơn giản, nhưng có thể thất bại nếu namecó chứa các biểu tượng có ý nghĩa đặc biệt đối với biểu thức chính quy. Các grepdanh sách các tập tin cũng có thể mất rất nhiều thời gian nếu có nhiều file. Bạn có thể xem xét dịch mã này để sử dụng hashtable sẽ ánh xạ tên tệp thành đường dẫn, ví dụ như trong Ruby.


Điều này có vẻ đầy hứa hẹn - nhưng nó di chuyển các tập tin, hoặc chỉ tạo liên kết tượng trưng?
Dan

Tôi nghĩ rằng tôi hiểu hầu hết điều này; nhưng grepdòng này làm gì? Có phải nó chỉ tìm thấy đường dẫn đầy đủ của tập tin phù hợp dstlist?
Dan

@Dan: rõ ràng bằng cách sử dụng lnnó tạo ra các liên kết tượng trưng. Bạn có thể sử dụng mvđể di chuyển các tệp, nhưng hãy cẩn thận với việc ghi đè lên các tệp hiện có. Ngoài ra, bạn có thể muốn dọn sạch các thư mục trống nếu có, sau khi di chuyển các tệp đi. Có, greplệnh đó tìm kiếm một dòng kết thúc trên tên tệp, do đó tiết lộ đường dẫn đầy đủ đến nó trong ổ đĩa đích.
alex

1

Giả sử tên tệp cơ sở là duy nhất trong cây, điều đó khá đơn giản:

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

Nếu bạn muốn dọn sạch các thư mục trống cũ, hãy sử dụng:

find B -depth -type d -delete

1

Tôi cũng phải đối mặt với vấn đề này. Giải pháp dựa trên md5sum không hoạt động đối với tôi, vì tôi đồng bộ hóa các tệp của mình với một webdavmount. Tính toán tổng md5sum trên webdavđích cũng có nghĩa là các thao tác tệp lớn.

Tôi đã tạo một tập lệnh nhỏ reorg_Remote_Dir_detect_moves.sh (trên github) đang cố gắng phát hiện các tệp được di chuyển nhiều nhất và sau đó tạo một tập lệnh shell tạm thời mới với một số lệnh để điều chỉnh thư mục từ xa. Vì tôi chỉ quan tâm đến tên tệp, kịch bản không phải là giải pháp hoàn hảo.

Để đảm bảo an toàn, một số tệp sẽ bị bỏ qua: A) Các tệp có cùng tên (cùng bắt đầu) ở mọi phía và B) Các tệp chỉ ở phía xa. Họ sẽ bị bỏ qua và bỏ qua.

Các tệp bị bỏ qua sau đó sẽ được xử lý bởi công cụ đồng bộ ưa thích của bạn (ví dụ: rsync, unison...), mà bạn phải sử dụng sau khi chạy tập lệnh shell tạm thời.

Vì vậy, có lẽ kịch bản của tôi là hữu ích cho một ai đó? Nếu vậy (để làm cho rõ ràng hơn), có ba bước:

  1. Chạy kịch bản shell reorg_Remote_Dir_detect_moves.sh (trên github)
  2. Điều này sẽ tạo shell-script tạm thời /dev/shm/REORGRemoteMoveScript.sh=> chạy cái này để thực hiện các động tác (sẽ được gắn kết nhanh webdav)
  3. Chạy công cụ đồng bộ ưa thích của bạn (ví dụ: rsync, unison...)

1

Đây là nỗ lực của tôi tại một câu trả lời. Như một lời cảnh báo trước, tất cả kinh nghiệm về kịch bản của tôi đến từ bash, vì vậy nếu bạn đang sử dụng một shell khác, tên lệnh hoặc cú pháp có thể khác nhau.

Giải pháp này yêu cầu tạo hai tập lệnh riêng biệt.

Kịch bản đầu tiên này chịu trách nhiệm thực sự di chuyển các tệp trên ổ đĩa đích.

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

Kịch bản thứ hai tạo tệp bản đồ md5 được sử dụng bởi tập lệnh đầu tiên và sau đó gọi tập lệnh đầu tiên trên mỗi tệp trong ổ đĩa đích.

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

Về cơ bản, những gì đang diễn ra là hai tập lệnh mô phỏng một mảng kết hợp với $md5_map_file. Đầu tiên, tất cả các md5s cho các tệp trên ổ đĩa nguồn được tính toán và lưu trữ. Liên kết với md5 là các đường dẫn tương đối từ gốc của ổ đĩa. Sau đó, đối với mỗi tệp trên ổ đĩa đích, md5 được tính toán. Sử dụng md5 này, đường dẫn của tệp đó trên ổ đĩa nguồn được tra cứu. Sau đó, tệp trên ổ đĩa đích được di chuyển để khớp với đường dẫn của tệp trên ổ đĩa nguồn.

Có một vài cảnh báo với kịch bản này:

  • Nó giả định rằng mọi tệp trong $ dst cũng nằm trong $ src
  • Nó không xóa bất kỳ thư mục nào khỏi $ dst, chỉ di chuyển các tệp. Tôi hiện không thể nghĩ ra một cách an toàn để làm điều này tự động

Phải mất một thời gian dài để tính toán md5: tất cả nội dung phải thực sự được đọc. Trong khi nếu Dan chắc chắn các tệp giống hệt nhau, chỉ cần di chuyển chúng trong cấu trúc thư mục là rất nhanh (không đọc). Vì vậy, md5sumdường như không phải là thứ để sử dụng ở đây. (BTW, rsynccó một chế độ trong đó nó không tính toán tổng.)
imz - Ivan Zakharyaschev

Đó là sự đánh đổi giữa độ chính xác và tốc độ. Tôi muốn cung cấp một phương pháp sử dụng mức độ chính xác cao hơn so với tên tập tin đơn giản.
cledoux
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.