Будь-який спосіб синхронізувати структуру каталогів, коли файли вже є з обох сторін?


24

У мене є два диски з однаковими файлами, але структура каталогів зовсім інша.

Чи є спосіб "перемістити" всі файли на стороні призначення, щоб вони відповідали структурі сторони джерела? Можливо, зі сценарієм?

Наприклад, привід A має:

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

Тоді як привід B має:

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

Файли, про які йдеться, величезні (800 Гб), тому я не хочу їх копіювати повторно; Я просто хочу синхронізувати структуру, створивши необхідні каталоги та перемістивши файли.

Я думав про рекурсивний скрипт, який би знайшов кожен вихідний файл у пункті призначення, а потім перемістив його у відповідний каталог, створивши при необхідності. Але - це понад мої здібності!

Ще одне елегантне рішення було надано тут: /superuser/237387/any-way-to-sync-directory-structure-when-the-files-are-already-on-both-sides/238086


Ви впевнені, що ім'я однозначно визначає вміст файлу, інакше вам слід розглянути можливість порівняння файлів за їх контрольними сумами.
kasterma

Відповіді:


11

Я піду з Жиллом і вкажу на Унісон, як це запропонував hasen j . Юнісон був DropBox за 20 років до DropBox. Солідний код, який багато людей (включаючи мене) використовують щодня - дуже варто вчитися. Все-таки joinпотрібна вся реклама, яку вона може отримати :)


Це лише половина відповіді, але я повинен повернутися до роботи :)

В основному, я хотів продемонструвати маловідому joinкорисність, яка робить саме це: об'єднує дві таблиці на якомусь полі.

Спочатку встановіть тестовий випадок, що включає назви файлів з пробілами:

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

(відредагуйте імена каталогів та / або файлів у new).

Тепер ми хочемо створити карту: hash -> ім'я файлу для кожної директорії, а потім використовувати joinдля зіставлення файлів з тим самим хешем. Щоб створити карту, введіть таке makemap.sh:

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

makemap.sh випиляє файл з рядками форми "хеш" ім'я файлу "', тому ми просто приєднуємося до першого стовпця:

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

Це генерує, moves.txtщо виглядає приблизно так:

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

Наступним кроком було б насправді зробити рухи, але мої спроби зациклювалися на цитуванні ... mv -iі mkdir -pповинні стати в нагоді.


Вибачте, я нічого з цього не розумію!
День

1
joinдійсно цікаво. Дякуємо за те, що ви звернули на це увагу.
Стівен Д

@Dan. Вибачте. Проблема полягає в тому, що я не знаю, які припущення я можу зробити щодо ваших імен файлів. Сценарій без припущень не є цікавим, особливо в цьому випадку, коли я вирішив вивести імена файлів у файл dwheeler.com/essays/fixing-unix-linux-filenames.html .
Janus

1
Це, ймовірно, витрачає багато часу (і завантаження процесора), оскільки ці величезні файли повинні бути прочитані повністю для створення хешей MD5. Якщо ім'я та розмір файлу збігаються, то, ймовірно, надмірно хеш-файли. Хеширование слід робити на другому кроці і тільки для файлів, які відповідають імені або розміру принаймні одного (на одному диску).
Hauke ​​Laging

Чи не потрібно сортувати файли, які ви використовуєте як joinвхідні дані?
cjm

8

Є утиліта під назвою unison:

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

Опис сайту:

Unison - це інструмент синхронізації файлів для Unix та Windows. Це дозволяє зберігати дві репліки колекції файлів і каталогів на різних хостах (або різних дисках на одному хості), змінювати їх окремо, а потім оновлювати, поширюючи зміни кожної репліки на інші.

Зауважте, що Unison виявляє переміщені файли лише під час першого запуску, якщо принаймні один із коренів віддалений, тому навіть якщо ви синхронізуєте локальні файли, використовуйте ssh://localhost/path/to/dirяк один із коренів.


@Gilles: Ви впевнені? Я використовую унісон для всього і часто бачу, як вони помічають файли, перейменовані та / або переміщені далеко. Ви хочете сказати, що це працює лише для вже синхронізованих файлів, коли унісон мав можливість записувати числа inode (або будь-які інші хитрощі, які він використовує)?
Янусь

@Janus: Дякую за виправлення, мій коментар був дійсно неправильним. Unison виявляє файли, які були переміщені, навіть під час початкового запуску. (Це не робиться, коли обидва коріння є локальними, тому це не було в моєму тесті.) Отож, унісон - дуже гарна пропозиція.
Жил "ТАК - перестань бути злим"

@Gilles. Добре знати - мабуть, існує досить багато місць, де алгоритм розрізняє локальні та віддалені синхронізації. Я насправді не думав, що це буде працювати для першої синхронізації. +1 за унісон!
Янусь

4

Використовуйте Unison, як це запропонував hasen j . Я залишаю цю відповідь як потенційно корисний сценарій сценарію або для використання на сервері з встановленими лише основними утилітами.


Я припускаю, що назви файлів є унікальними у всій ієрархії. Я також припускаю, що жодне ім'я файлу не містить нового рядка і що дерева каталогів містять лише каталоги та звичайні файли.

  1. Спочатку збирайте імена файлів на стороні джерела.

    (cd /A && find . \! -type d) >A.find
  2. Потім перемістіть файли на місце призначення. Спочатку створіть згладжене дерево файлів на стороні призначення. Використовуйте lnзамість того, mvякщо ви хочете зберігати жорсткі посилання у старій ієрархії.

    mkdir /B.staging /B.new
    find /B.old -type f -exec sh -c 'mv -- "$@" "$0"' /B.staging {} +
  3. Якщо у файлі призначення можуть бути відсутні деякі файли, створіть аналогічно згладжені /A.stagingта використовуйте rsync для копіювання даних з джерела до пункту призначення.

    rsync -au /A.staging/ /B.staging/
  4. Тепер перейменуйте файли на свої місця.

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

    Рівнозначно:

    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. Нарешті, якщо ви дбаєте про метадані каталогів, зателефонуйте rsync з уже наявними файлами.

    rsync -au /A/ /B.new/

Зауважте, що я не перевіряв фрагменти в цій публікації. Використовуйте на свій страх і ризик. Повідомте про будь-яку помилку в коментарі.


2

Особливо, якщо триваюча синхронізація була б корисною, ви можете спробувати розібрати git-annex .

Він відносно новий; Я сам не намагався його використовувати.

Я можу запропонувати це, оскільки це дозволяє уникнути збереження другої копії файлів ... це означає, що він повинен позначати файли як лише для читання ("заблоковані"), як і деякі системи контролю версій не Git.

Файли ідентифікуються розширенням файлу sha256sum + (за замовчуванням). Таким чином, він повинен мати можливість синхронізувати дві репозиції з однаковим вмістом файлу, але різними іменами файлів, не виконуючи запис (і за бажанням мережі з низькою пропускною здатністю, якщо потрібно). Звичайно, доведеться прочитати всі файли, щоб перевірити їх.


1

Як щодо щось подібне:

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

Це передбачає, що назви файлів, які ви хочете синхронізувати, є унікальними на всьому диску: інакше немає можливості його повністю автоматизувати (однак, ви можете надати користувачеві підказку вибрати, який саме файл вибрати, якщо його буде більше.)

Сценарій, описаний вище, буде працювати в простих випадках, але може не вдатися, якщо nameтрапляється, що він містить символи, які мають спеціальне значення для регулярних виразів. grepУ списку файлів також може зайняти багато часу , якщо є багато файлів. Ви можете розглянути можливість перекладу цього коду на використання хештету, який буде відображати назви файлів до шляхів, наприклад у Ruby


Це виглядає багатообіцяюче - але чи переміщуються вони файли чи просто створюються символьні посилання?
День

Я думаю, що я розумію більшість цього; але що робить grepлінія? Чи просто знайде повний шлях відповідного файлу dstlist?
День

@Dan: мабуть, завдяки його використанню lnстворюються символьні посилання. Ви можете використовувати mvдля переміщення файлів, але остерігайтеся перезапису існуючих. Також, можливо, ви захочете очистити порожні бруди, якщо такі є, після переміщення файлів. Так, ця grepкоманда шукає рядок, який закінчується на імені файлу, тим самим розкриваючи повний шлях до нього на приводному диску.
alex

1

Якщо припустити, що основні назви файлів є унікальними на деревах, це досить просто:

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

Якщо ви хочете очистити старі порожні каталоги, скористайтеся:

find B -depth -type d -delete

1

Я також зіткнувся з цією проблемою. Рішення на базі md5sum не працювало для мене, оскільки я синхронізував свої файли з webdavмонтом. Обчислення сум md5sum за webdavпунктом призначення також означатиме великі операції з файлами.

Я створив невеликий скрипт reorg_Remote_Dir_detect_moves.sh (на github), який намагається виявити найбільш переміщені файли, а потім створює новий тимчасовий скрипт оболонки з кількома командами для налаштування віддаленого каталогу. Оскільки я дбаю лише про назви файлів, сценарій не є ідеальним рішенням.

Для безпеки буде проігноровано декілька файлів: A) Файли з однаковими (однаковими початком) іменами з усіх боків та B) Файли, які знаходяться лише на віддаленій стороні. Вони будуть ігноровані та пропущені.

Пропущені файли потім обробляються вашим уподобаним інструментом синхронізації (наприклад rsync, unison, ...), який ви повинні використовувати після запуску тимчасового скрипта оболонки.

То, може, мій сценарій комусь корисний? Якщо так (щоб було зрозуміліше), є три кроки:

  1. Запустіть скрипт оболонки reorg_Remote_Dir_detect_moves.sh (на github)
  2. Це створить тимчасовий оболонку-скрипт /dev/shm/REORGRemoteMoveScript.sh=> запустити це, щоб зробити рухи (буде швидко на встановленому webdav)
  3. Запустіть бажаний інструмент синхронізації (наприклад rsync, unison, ...)

1

Ось моя спроба відповіді. Як попередження, весь мій досвід написання сценарію походить від bash, тому якщо ви використовуєте іншу оболонку, імена команд або синтаксис можуть бути різними.

Це рішення вимагає створення двох окремих сценаріїв.

Цей перший скрипт відповідає за фактичне переміщення файлів на цільовому диску.

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

Другий скрипт створює файл карти md5, який використовується першим сценарієм, а потім викликає перший скрипт у кожному файлі на приводному диску.

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

В основному, два сценарії моделюють асоціативний масив $md5_map_file. По-перше, всі md5 для файлів на вихідному диску обчислюються та зберігаються. Пов’язані з md5s відносні шляхи від кореня диска. Потім для кожного файлу на цільовому диску, md5 обчислюється. Використовуючи цей md5, шукається шлях до цього файлу на вихідному диску. Файл на приводному диску потім переміщується, щоб відповідати шляху файлу на вихідному диску.

Є кілька застережень із цим сценарієм:

  • Він передбачає, що кожен файл у $ dst також знаходиться в $ src
  • Він не видаляє жодних каталогів з $ dst, лише переміщує файли. Наразі я не можу придумати безпечний спосіб зробити це автоматично

Щоб обчислити md5, потрібно багато часу: весь вміст повинен бути прочитаний. Хоча якщо Дан впевнений, що файли однакові, просто переміщення їх у структурі каталогів відбувається дуже швидко (не читається). Тож, md5sumздається, тут не використовується річ. (У BTW rsyncє режим, в якому він не розраховує контрольні суми.)
imz - Іван Захарящев

Це компроміс між точністю та швидкістю. Я хотів запропонувати метод, який використовував більш високий ступінь точності, ніж просто назви файлів.
cledoux
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.