import { SetFiles } from 'types/gallery'; import { Collection } from 'types/collection'; import { getEndpoint } from 'utils/common/apiUtil'; import { getToken } from 'utils/common/key'; import { decryptFile, mergeMetadata, preservePhotoswipeProps, sortFiles, } from 'utils/file'; import { logError } from 'utils/sentry'; import localForage from 'utils/storage/localForage'; import { getCollection } from './collectionService'; import { EnteFile } from 'types/file'; import HTTPService from './HTTPService'; import { Trash, TrashItem } from 'types/trash'; const TRASH = 'file-trash'; const TRASH_TIME = 'trash-time'; const DELETED_COLLECTION = 'deleted-collection'; const ENDPOINT = getEndpoint(); export async function getLocalTrash() { const trash = (await localForage.getItem(TRASH)) || []; return trash; } export async function getLocalDeletedCollections() { const trashedCollections: Array = (await localForage.getItem(DELETED_COLLECTION)) || []; return trashedCollections; } export async function cleanTrashCollections(fileTrash: Trash) { const trashedCollections = await getLocalDeletedCollections(); const neededTrashCollections = new Set( fileTrash.map((item) => item.file.collectionID) ); const filterCollections = trashedCollections.filter((item) => neededTrashCollections.has(item.id) ); await localForage.setItem(DELETED_COLLECTION, filterCollections); } async function getLastSyncTime() { return (await localForage.getItem(TRASH_TIME)) ?? 0; } export async function syncTrash( collections: Collection[], setFiles: SetFiles, files: EnteFile[] ): Promise { const trash = await getLocalTrash(); collections = [...collections, ...(await getLocalDeletedCollections())]; const collectionMap = new Map( collections.map((collection) => [collection.id, collection]) ); if (!getToken()) { return trash; } const lastSyncTime = await getLastSyncTime(); const updatedTrash = await updateTrash( collectionMap, lastSyncTime, setFiles, files, trash ); cleanTrashCollections(updatedTrash); return updatedTrash; } export const updateTrash = async ( collections: Map, sinceTime: number, setFiles: SetFiles, files: EnteFile[], currentTrash: Trash ): Promise => { try { let updatedTrash: Trash = [...currentTrash]; let time = sinceTime; let resp; do { const token = getToken(); if (!token) { break; } resp = await HTTPService.get( `${ENDPOINT}/trash/v2/diff`, { sinceTime: time, }, { 'X-Auth-Token': token, } ); for (const trashItem of resp.data.diff as TrashItem[]) { const collectionID = trashItem.file.collectionID; let collection = collections.get(collectionID); if (!collection) { collection = await getCollection(collectionID); collections.set(collectionID, collection); localForage.setItem(DELETED_COLLECTION, [ ...collections.values(), ]); } if (!trashItem.isDeleted && !trashItem.isRestored) { trashItem.file = await decryptFile( trashItem.file, collection.key ); } updatedTrash.push(trashItem); } if (resp.data.diff.length) { time = resp.data.diff.slice(-1)[0].updatedAt; } updatedTrash = removeDuplicates(updatedTrash); updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash); setFiles( preservePhotoswipeProps( sortFiles([ ...(files ?? []).map((file) => ({ ...file, isTrashed: false, })), ...getTrashedFiles(updatedTrash), ]) ) ); await localForage.setItem(TRASH, updatedTrash); await localForage.setItem(TRASH_TIME, time); } while (resp.data.hasMore); return updatedTrash; } catch (e) { logError(e, 'Get trash files failed'); } return currentTrash; }; function removeDuplicates(trash: Trash) { const latestVersionTrashItems = new Map(); trash.forEach(({ file, updatedAt, ...rest }) => { if ( !latestVersionTrashItems.has(file.id) || latestVersionTrashItems.get(file.id).updatedAt < updatedAt ) { latestVersionTrashItems.set(file.id, { file, updatedAt, ...rest }); } }); trash = []; // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [_, trashedFile] of latestVersionTrashItems) { trash.push(trashedFile); } return trash; } function removeRestoredOrDeletedTrashItems(trash: Trash) { return trash.filter((item) => !item.isDeleted && !item.isRestored); } export function getTrashedFiles(trash: Trash) { return mergeMetadata( trash.map((trashedFile) => ({ ...trashedFile.file, updationTime: trashedFile.updatedAt, isTrashed: true, deleteBy: trashedFile.deleteBy, })) ); } export const emptyTrash = async () => { try { const token = getToken(); if (!token) { return; } const lastUpdatedAt = await getLastSyncTime(); await HTTPService.post( `${ENDPOINT}/trash/empty`, { lastUpdatedAt }, null, { 'X-Auth-Token': token, } ); } catch (e) { logError(e, 'empty trash failed'); throw e; } }; export const clearLocalTrash = async () => { await localForage.setItem(TRASH, []); };