TL;DR
Разбираем клиентский подход к поиску дубликатов фотографий с использованием Web Crypto API, File System Access API и Web Workers. Полный контроль над данными, нулевые загрузки в облако и работа с большими библиотеками до 20k файлов.
Введение: зачем client-side deduplication?
Типичный сценарий: после экспорта из Google Photos, синхронизации через WhatsApp и резервного копирования получаем 3 копии одного фото с разными именами. Традиционные решения требуют загрузки всего контента на сервер, что неприемлемо для приватных данных.
Современные браузеры предоставляют все необходимые API для локальной обработки:
- File System Access API для работы с файловой системой
- Web Crypto API для хеширования
- Web Workers для выноса тяжелых вычислений
- Origin Private File System для хранения состояния
Основная часть: архитектура решения
1. File System Access API
const dirHandle = await window.showDirectoryPicker();
const imageFiles = await scanDirectory(dirHandle);
async function scanDirectory(dirHandle) {
const files = [];
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && isImageFile(entry.name)) {
files.push(entry);
} else if (entry.kind === 'directory') {
files.push(...await scanDirectory(entry));
}
}
return files;
}
Важные нюансы:
- Только HTTPS или localhost
- Safari/Firefox требуют fallback на
<input type="file" multiple> - Разрешения на чтение и запись запрашиваются отдельно
2. Хеширование через Web Crypto
async function getFileHash(fileHandle) {
const file = await fileHandle.getFile();
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
return bufferToHex(hashBuffer);
}
function bufferToHex(buffer) {
return [...new Uint8Array(buffer)]
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
Производительность:
- Хеш 10MB файла: ~50ms на M1 Mac
- Параллельная обработка через Web Workers
3. Оптимизации для больших коллекций
// Воркер для хеширования
self.onmessage = async (e) => {
const { fileHandle } = e.data;
const hash = await getFileHash(fileHandle);
self.postMessage({ hash, fileHandle });
};
// Главный поток
const workerPool = Array(4).fill(null).map(() => new Worker('hasher.js'));
files.forEach((file, i) => {
workerPool[i % 4].postMessage({ fileHandle: file });
});
Критические улучшения:
- Виртуализация списка результатов (react-window)
- Прогресс-бар с реальным ETA
- Оптимизация памяти через stream API для больших файлов
Практическое применение
Сценарии использования
- Pre-backup cleanup: уменьшение размера бэкапов на 10-30%
- Messenger exports: удаление дубликатов из WhatsApp/Telegram
- Фотоархивы: проверка перед передачей клиенту
Безопасность и приватность
Проверочный чеклист:
- [ ] Network tab показывает только метрики, не файлы
- [ ] Работает в offline-режиме после загрузки
- [ ] Явные запросы на запись в FS
- [ ] Четкое описание операций в privacy policy
Заключение
Client-side deduplication — жизнеспособная альтернатива облачным сервисам. Современные браузеры предоставляют все необходимые API, а производительность Web Crypto делает решение практичным даже для больших коллекций.
Главные преимущества:
- Полный контроль данных
- Нулевые загрузки в облако
- Поддержка нативных файловых операций
- Возможность тонкой настройки под свои нужды
Ограничения:
- Только exact matching (без perceptual hashing)
- Desktop-first подход
- Требуется современный браузер
Для проектов, работающих с приватными медиа, этот подход становится must-have, а не опциональной фичей.