На Mac с диском 256–512 ГБ место заканчивается регулярно. Обычный путь известен: открыть анализатор диска, найти самое большое, перетащить на внешний SSD и удалить оригинал. Если разобраться, что при этом ломается, окажется, что ломается много чего, и чаще всего тихо.

Ниже — грабли, которые я собрал, пока писал инструмент для такого переноса, и как их обойти. Всё это пригодится и при переносе руками.

«Скопировалось» ещё не значит «скопировалось правильно»

Finder и cp сообщают об успехе, когда данные отданы системе. Дошли ли они до диска целиком, никто не проверяет. На дешёвом USB-контроллере, плохом кабеле или выдернутом раньше времени диске копия может отличаться от оригинала, а оригинал вы уже удалили.

Надёжный перенос идёт в четыре шага:

  1. копирование;

  2. копия перечитывается с диска в обход кеша и сверяется по SHA-256 с оригиналом;

  3. проверка, что оригинал не менялся, пока шло копирование: размеры, время изменения, состав папок;

  4. только после этого — удаление оригинала.

Про второй шаг легко забыть: если сразу после копирования посчитать хеш, macOS отдаст данные из страничного кеша, и сверка пройдёт, даже если на диск записался мусор. Читать нужно в обход кеша — флагом F_NOCACHE:

let fd = open(path, O_RDONLY)
_ = fcntl(fd, F_NOCACHE, 1)   // читать с диска, а не из памяти

В командной строке ближе всего к этому — отключить диск, подключить снова и только потом сверять:

cd /Volumes/External/backup && shasum -a 256 -c checksums.txt

Список контрольных сумм стоит класть рядом с копией в формате shasum -c: тогда её можно проверить через пять лет на другом компьютере без всяких утилит.

exFAT теряет больше, чем кажется

Внешние диски почти всегда отформатированы в exFAT, чтобы работать и с Windows. exFAT не хранит права доступа и расширенные атрибуты. Скопируйте туда проект со скриптами, верните обратно — и ./build.sh перестанёт запускаться: бит исполнения потерян.

Что помогает: перед переносом сохранить права всех файлов в отдельный файл рядом с архивом и восстановить их при возврате. Если такого списка нет, разумные права по умолчанию: папки 755, файлы 644, а скрипты и бинарники (их можно узнать по #! в начале и по заголовку Mach-O) — исполняемые.

Вторая неприятность — файлы ._имя, которые macOS создаёт на exFAT для хранения атрибутов. После сверки их можно удалять. А если у вас есть настоящий файл, имя которого начинается с ._, на exFAT его лучше не класть вовсе: macOS молча перезапишет его своими данными.

Метки Finder, комментарии и жёсткие ссылки при таком переносе тоже не сохраняются. Если они важны, об этом надо знать до удаления оригинала, а не после.

Некоторые папки переносить нельзя совсем

Самый неприятный случай — UTM. Переносите папку с виртуальными машинами на внешний диск, возвращаете обратно, а UTM показывает машины как «Недоступно». Данные целы, но приложение хранит ссылки на файлы, и после переноса они ведут в никуда. То же самое с медиатекой «Фото».

Правило простое: пакеты со внутренними ссылками (.utm, .photoslibrary и подобные) не переносить, даже если они лежат глубоко внутри выбранной папки. Место под них освобождается в самом приложении: в UTM — его собственным удалением или переносом машины. ~/Library, ~/.ssh и ~/.config тоже лучше не трогать.

Docker умеет съесть диск, пока вы его чистите

Задача: заархивировать неиспользуемые тома Docker на внешний диск. Логичный способ — запустить контейнер, который делает tar тома в stdout:

docker run --rm -v myvolume:/data alpine tar -C /data -cf - . > /Volumes/External/myvolume.tar

Проблема: Docker по умолчанию пишет весь вывод контейнера в свой лог внутри Docker.raw. Поток архива на 30 ГБ сначала целиком ложится в этот лог, и диск заканчивается ровно в тот момент, когда вы пытаетесь его освободить.

Что нужно:

  • запускать контейнер с --log-driver none (и заодно --network none: сеть ему не нужна);

  • давать контейнеру имя и при отмене останавливать сам контейнер, а не только клиент docker: иначе контейнер продолжает писать;

  • следить за размером Docker.raw и прерывать операцию, если он начинает расти.

docker run --rm --name vol-archive --log-driver none --network none \
  -v myvolume:/data:ro alpine tar -C /data -cf - . > /Volumes/External/myvolume.tar

При обычной очистке тома лучше не трогать совсем: в них данные. Безопасно удаляются кеш сборки и образы без имени (<none>, остатки пересборок). Собранный вами и никуда не отправленный образ заново не скачать, поэтому его стоит удалять только осознанно.

Шифрованный контейнер без своей криптографии

Если на внешний диск уходят проекты и ключи, их стоит шифровать. Изобретать для этого ничего не нужно: штатный hdiutil создаёт образ AES-256 с APFS внутри, а шифрование написала и проверяет Apple.

hdiutil create -size 200g -type SPARSEBUNDLE -fs APFS -encryption AES-256 \
  -stdinpass -volname Vault /Volumes/External/Vault.sparsebundle

Пара деталей, которые оказались важными при работе с ним из программы:

  • пароль передаётся только через stdin (-stdinpass), не в аргументах: аргументы видны любому процессу через ps;

  • до открытия образа стоит проверить, что он действительно зашифрован (hdiutil isencrypted), а после — что система смонтировала его как зашифрованный (hdiutil info, поле image-encrypted). Иначе подменённый образ без шифрования откроется молча, и всё уйдёт в него открытым текстом;

  • некоторые вызовы hdiutil сами показывают системное окно с запросом пароля. Из программы их лучше не вызывать, чтобы пароль вводился в одном предсказуемом месте;

  • образ стоит закрывать при сне, блокировке экрана и простое.

Скрытых томов и правдоподобного отрицания у такого контейнера нет. Если нужно именно это — VeraCrypt.

Итого

Безопасный перенос — это не «скопировать и удалить», а:

  • сверка копии, перечитанной с диска;

  • проверка, что оригинал не менялся;

  • сохранённые права доступа рядом с архивом;

  • список того, что переносить нельзя;

  • для Docker — отключённый лог контейнера.

Всё это я собрал в открытой утилите для macOS, код и проверки — на GitHub: https://github.com/audit0/Offload

Расскажите в комментариях, на какие грабли с переносом данных наступали вы.