Девятнадцатый день NAS. Семейную медиатеку на ящик везут двое: приложение на телефоне и сам ящик, который выкачивает старое прямо из iCloud. Эта запись — почему одного пути не хватило и как сделать, чтобы два пути не положили один снимок дважды и не затёрли друг друга.
Почему не через телефон
Сначала план был простой: приложение выгружает всё, что видит «Фото». Но у больших медиатек оригиналы лежат в iCloud, а на телефоне — только уменьшенные копии.
Чтобы выгрузить оригинал, телефон сначала качает его себе. «Фото» оставляет каждый скачанный файл кэшем и не отдаёт место, пока его не станет не хватать, а выгнать кэш приложению нечем. На медиатеке в 1,6 ТБ телефон или Mac забивается примерно за час.
У второго телефона в семье всё наоборот: место в iCloud кончилось, и с марта новые снимки лежат только на телефоне. «Выгрузить всё» дошло бы до старого и стало бы качать его на телефон с 28 ГБ свободного места.
Решение — разделить работу. Старое из iCloud забирает сам ящик, сервер к серверу. Телефон везёт только то, что лежит на нём самом: режим «Только с устройства» запрещает «Фото» сеть и пропускает снимки, которых на телефоне нет.
Но тогда один и тот же снимок часто есть у обоих: телефон его уже выгрузил, а в iCloud он тоже лежит. У двух источников одной медиатеки три способа ошибиться: положить снимок дважды, затереть чужой файл своим и скачать то, что уже лежит.
Одна раскладка на двоих
Главное решение: ящик кладёт файлы из iCloud в тот же бакет и под те же ключи, что и приложение. Ключ собирается из момента съёмки и начала хеша самого файла.
Источник правды — функция приложения на Rust. Ящик повторяет её на Python и сверен с ней на самых скользких местах: летнее время тех лет, когда снят кадр, и имена без расширения. Тест сверки живёт в проверках ящика.
Бакет — неизменяемый: положенное нельзя перезаписать ни телефону, ни ящику. Даже если в логике одного из них ошибка, она закончится отказом, а не потерянным оригиналом.
Из iCloud берётся ровно то, что берёт приложение: оригинал, видео Live Photo, RAW из пары и текущая правка, каждый файл своим ключом. Сверх этого — «Скрытые» и общая медиатека: приложение их не видит, поэтому они включаются переключателями.
Как ящик узнаёт выгруженное телефоном
Хеш в ключе создаёт ловушку: чтобы посчитать ключ файла из iCloud, его надо скачать. А смысл как раз в том, чтобы не качать уже выгруженное.
Поэтому до скачивания ящик спрашивает свой индекс по тому, что iCloud сообщает сразу: есть ли в бакете файл той же секунды съёмки, с тем же расширением и ровно тем же размером в байтах. Есть — файл отмечается «уже был» и не качается вовсе.
Если совпадения нет, файл качается, и дальше решает полный ключ с хешем:
Ключ свободен. Файл ложится, рядом — подпись .json* с полями приложения и пометкой
source: icloud.Под ключом тот же файл. Совпал sha256* — ничего не трогаем, «уже был».
Под ключом другой файл.* Конфликт в журнал, а не перезапись. Разбирает человек.
К файлу, который выгрузил телефон, ящик свою подпись не пишет: у него уже есть подпись приложения, и бакет неизменяемый.
Срок переноса из-за этого считается не по байтам, а по снимкам за последние две минуты. Пока перенос идёт по уже выгруженному, байт почти нет, и по скорости скачивания срок выходил неделями.
Цена входа в iCloud и почему ей можно доверять
У iCloud нет официального серверного доступа к фото. Ящик ходит туда библиотекой pyicloud — и получает полный доступ к сессии Apple ID, вместе с удалением.
Поэтому только чтение держится не на нашей аккуратности, а на трёх замках:
Пароль нужен один раз и нигде не остаётся: пароль, код с устройства — и дальше живёт сессия, около двух месяцев. Когда она кончится, придёт пуш «войдите заново», а перенос продолжит с того же места.
Каждый скачанный файл сверяется по размеру с тем, что заявил iCloud, а sha256 считается при приёме. Не сошлось — файл не принимается и уходит в список ошибок. Обрыв, пауза или перезапуск ящика не теряют сделанного: журнал помнит каждый файл.
Один снимок — одна плитка
Даже без дублей в бакете один кадр может состоять из файлов разного происхождения: оригинал привёз телефон, а правку или видео Live Photo — ящик из iCloud.
Лента «Фото» на ящике склеивает их в одну плитку. Внутри одного источника — по секунде съёмки и общему идентификатору, между телефоном и iCloud — по имени из «Фото» вроде IMG_1234.
То же имя ящик отдаёт приложению. Раньше Mac показывал снимок, перенесённый из iCloud, как «ещё не на NAS» — и весь день в ленте шёл дублями: он судил только по своему списку выгруженного. Теперь приложение прячет локальный снимок, если на ящике есть снимок того же вида, с тем же именем и моментом ± 2 с.
Оба пути видны в истории синхронизаций под переносом. Прогон из iCloud пишет итог сам: сколько занял, скачано, «уже были», скорость, ошибки. Выгрузки с телефонов ящик собирает в сеансы по ключу доступа: перерыв дольше 30 минут — новый сеанс.
Скорость: напрямую из iCloud — в 20 раз быстрее
Официальным путём оригиналы из iCloud уходят медленно: приложение Apple отдаёт 4–5 МБ/с, и это в лучшем случае. Наше приложение с телефона везёт 30–35 МБ/с. А ящик, который ходит в iCloud сам, сервер к серверу, берёт 100 МБ/с и больше.
Вся медиатека официальным путём ушла бы сутками, а напрямую — за несколько часов. На мелких фото упор не в канал, а в задержку каждого запроса — поэтому перенос умеет до 24 потоков, а ползунок потоков действует на ходу, без паузы.
И даже на такой скорости из iCloud ящик качает только то, чего у него ещё нет: всё, что уже привёз телефон, пропускается без скачивания.
Что проверено, а что нет
| Что | Чем |
|---|---|
| Раскладка ключей, летнее время | тест против функции приложения |
| Уже выгруженное телефоном | стенд |
| Конфликт и неполный файл | стенд |
| Пауза посреди закачки, повтор без дублей | стенд |
| Запись в iCloud заблокирована | стенд: 12 методов и 4 запроса |
| Вход в Apple и живая медиатека | два семейных аккаунта, живые медиатеки |
Сколько снимков перенёс живой прогон и сколько из них оказались «уже были», здесь не приводим: эти цифры мы не снимали.
Что осталось в панели
- «Хранилище → Перенос из iCloud»: вход паролем и кодом, пробный прогон, пауза, ползунок потоков, срок по ходу в снимках.
- Сверка «в iCloud — на ящике» и списки ошибок и конфликтов.
- История синхронизаций: прогоны переноса и сеансы телефонов в одном списке.
- Лента «Фото», где снимок из двух источников — одна плитка.
- В приложении — режим «Только с устройства».
