ente/src/services/fileService.ts

374 lines
10 KiB
TypeScript
Raw Normal View History

2021-05-30 16:56:48 +00:00
import { getEndpoint } from 'utils/common/apiUtil';
2021-03-12 06:58:27 +00:00
import localForage from 'utils/storage/localForage';
2021-05-30 16:56:48 +00:00
import { getToken } from 'utils/common/key';
import {
DataStream,
EncryptionResult,
MetadataObject,
} from './upload/uploadService';
2021-05-30 16:56:48 +00:00
import { Collection } from './collectionService';
2021-05-29 06:27:52 +00:00
import HTTPService from './HTTPService';
2021-06-12 17:14:21 +00:00
import { logError } from 'utils/sentry';
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
import CryptoWorker from 'utils/crypto';
2020-09-27 17:18:57 +00:00
const ENDPOINT = getEndpoint();
const FILES_TABLE = 'files';
2021-02-09 05:43:05 +00:00
2021-11-03 14:36:00 +00:00
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date();
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
2021-11-01 18:23:34 +00:00
2020-10-19 03:01:34 +00:00
export interface fileAttribute {
2021-03-04 12:14:45 +00:00
encryptedData?: DataStream | Uint8Array;
2021-02-16 09:44:25 +00:00
objectKey?: string;
decryptionHeader: string;
}
2020-10-19 03:01:34 +00:00
2021-08-13 03:19:48 +00:00
export enum FILE_TYPE {
IMAGE,
VIDEO,
LIVE_PHOTO,
OTHERS,
}
/* Build error occurred
ReferenceError: Cannot access 'FILE_TYPE' before initialization
when it was placed in readFileService
*/
// list of format that were missed by type-detection for some files.
export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpeg' },
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg' },
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm' },
];
2021-09-21 07:21:26 +00:00
export enum VISIBILITY_STATE {
VISIBLE,
ARCHIVED,
}
export interface MagicMetadataCore {
2021-09-21 07:21:26 +00:00
version: number;
count: number;
header: string;
data: Record<string, any>;
}
export interface EncryptedMagicMetadataCore
extends Omit<MagicMetadataCore, 'data'> {
data: string;
}
export interface MagicMetadataProps {
visibility?: VISIBILITY_STATE;
2021-09-21 07:21:26 +00:00
}
export interface MagicMetadata extends Omit<MagicMetadataCore, 'data'> {
data: MagicMetadataProps;
}
export interface PublicMagicMetadataProps {
editedTime?: number;
}
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
data: PublicMagicMetadataProps;
}
export interface File {
id: number;
collectionID: number;
ownerID: number;
file: fileAttribute;
thumbnail: fileAttribute;
2021-02-16 09:44:25 +00:00
metadata: MetadataObject;
2021-09-21 07:21:26 +00:00
magicMetadata: MagicMetadata;
pubMagicMetadata: PublicMagicMetadata;
encryptedKey: string;
keyDecryptionNonce: string;
2021-01-18 02:26:34 +00:00
key: string;
src: string;
msrc: string;
html: string;
w: number;
h: number;
isDeleted: boolean;
isTrashed?: boolean;
2021-10-04 14:57:45 +00:00
deleteBy?: number;
dataIndex: number;
updationTime: number;
}
2021-09-21 10:37:53 +00:00
interface UpdateMagicMetadataRequest {
metadataList: UpdateMagicMetadata[];
}
2021-09-21 10:37:53 +00:00
interface UpdateMagicMetadata {
id: number;
magicMetadata: EncryptedMagicMetadataCore;
2021-09-21 10:37:53 +00:00
}
export const NEW_MAGIC_METADATA: MagicMetadataCore = {
2021-09-21 10:37:53 +00:00
version: 0,
data: {},
header: null,
count: 0,
};
2021-09-28 07:09:59 +00:00
interface TrashRequest {
items: TrashRequestItems[];
}
interface TrashRequestItems {
fileID: number;
collectionID: number;
}
export const getLocalFiles = async () => {
const files: Array<File> =
(await localForage.getItem<File[]>(FILES_TABLE)) || [];
return files;
};
export const setLocalFiles = async (files: File[]) => {
await localForage.setItem(FILES_TABLE, files);
};
2021-08-13 02:38:38 +00:00
export const syncFiles = async (
collections: Collection[],
setFiles: (files: File[]) => void
) => {
const localFiles = await getLocalFiles();
let files = await removeDeletedCollectionFiles(collections, localFiles);
2021-05-29 06:27:52 +00:00
if (files.length !== localFiles.length) {
await setLocalFiles(files);
setFiles(sortFiles(mergeMetadata(files)));
}
2021-05-29 06:27:52 +00:00
for (const collection of collections) {
if (!getToken()) {
continue;
}
2021-08-13 02:38:38 +00:00
const lastSyncTime =
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
if (collection.updationTime === lastSyncTime) {
2021-02-08 07:33:46 +00:00
continue;
2021-02-08 11:01:05 +00:00
}
2021-08-13 02:38:38 +00:00
const fetchedFiles =
2021-10-30 02:56:30 +00:00
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
2021-02-08 10:56:47 +00:00
files.push(...fetchedFiles);
2021-05-29 06:27:52 +00:00
const latestVersionFiles = new Map<string, File>();
2021-02-08 10:56:47 +00:00
files.forEach((file) => {
2021-03-16 09:41:11 +00:00
const uid = `${file.collectionID}-${file.id}`;
if (
2021-03-16 09:41:11 +00:00
!latestVersionFiles.has(uid) ||
latestVersionFiles.get(uid).updationTime < file.updationTime
) {
2021-03-16 09:41:11 +00:00
latestVersionFiles.set(uid, file);
2021-02-08 10:56:47 +00:00
}
});
files = [];
2021-05-29 06:27:52 +00:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2021-02-08 10:56:47 +00:00
for (const [_, file] of latestVersionFiles) {
if (file.isDeleted) {
2021-02-08 10:56:47 +00:00
continue;
}
files.push(file);
2021-02-06 07:54:36 +00:00
}
await setLocalFiles(files);
await localForage.setItem(
`${collection.id}-time`,
2021-08-13 02:38:38 +00:00
collection.updationTime
);
files = sortFiles(mergeMetadata(files));
setFiles(files);
}
return mergeMetadata(files);
};
export const getFiles = async (
collection: Collection,
2021-02-09 05:07:46 +00:00
sinceTime: number,
2021-06-01 20:42:44 +00:00
files: File[],
2021-08-13 02:38:38 +00:00
setFiles: (files: File[]) => void
): Promise<File[]> => {
2021-01-25 07:05:45 +00:00
try {
const decryptedFiles: File[] = [];
2021-08-13 02:38:38 +00:00
let time =
sinceTime ||
2021-02-09 05:07:46 +00:00
(await localForage.getItem<number>(`${collection.id}-time`)) ||
0;
let resp;
do {
const token = getToken();
if (!token) {
break;
}
resp = await HTTPService.get(
2021-10-30 02:56:30 +00:00
`${ENDPOINT}/collections/v2/diff`,
{
2021-02-17 08:35:19 +00:00
collectionID: collection.id,
sinceTime: time,
},
{
'X-Auth-Token': token,
2021-08-13 02:38:38 +00:00
}
);
decryptedFiles.push(
...(await Promise.all(
resp.data.diff.map(async (file: File) => {
if (!file.isDeleted) {
file = await decryptFile(file, collection);
}
return file;
}) as Promise<File>[]
))
);
if (resp.data.diff.length) {
2021-02-17 08:35:19 +00:00
time = resp.data.diff.slice(-1)[0].updationTime;
}
2021-08-13 02:38:38 +00:00
setFiles(
sortFiles(
2021-10-30 15:05:13 +00:00
mergeMetadata(
[...(files || []), ...decryptedFiles].filter(
(item) => !item.isDeleted
)
2021-08-13 02:38:38 +00:00
)
)
2021-08-13 02:38:38 +00:00
);
2021-10-30 02:56:30 +00:00
} while (resp.data.hasMore);
return decryptedFiles;
2021-01-25 07:05:45 +00:00
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'Get files failed');
}
};
const removeDeletedCollectionFiles = async (
collections: Collection[],
2021-08-13 02:38:38 +00:00
files: File[]
) => {
const syncedCollectionIds = new Set<number>();
2021-05-29 06:27:52 +00:00
for (const collection of collections) {
syncedCollectionIds.add(collection.id);
}
files = files.filter((file) => syncedCollectionIds.has(file.collectionID));
2021-02-09 05:07:46 +00:00
return files;
};
2021-03-21 06:53:41 +00:00
2021-09-28 07:09:59 +00:00
export const trashFiles = async (filesToTrash: File[]) => {
2021-03-21 06:53:41 +00:00
try {
const token = getToken();
if (!token) {
2021-03-30 05:09:43 +00:00
return;
2021-03-21 06:53:41 +00:00
}
2021-09-28 07:09:59 +00:00
const trashRequest: TrashRequest = {
items: filesToTrash.map((file) => ({
fileID: file.id,
collectionID: file.collectionID,
})),
};
await HTTPService.post(`${ENDPOINT}/files/trash`, trashRequest, null, {
'X-Auth-Token': token,
});
2021-03-21 06:53:41 +00:00
} catch (e) {
2021-09-28 07:09:59 +00:00
logError(e, 'trash file failed');
2021-05-31 10:58:33 +00:00
throw e;
2021-03-21 06:53:41 +00:00
}
};
2021-09-21 08:03:15 +00:00
export const deleteFromTrash = async (filesToDelete: number[]) => {
2021-03-21 06:53:41 +00:00
try {
const token = getToken();
if (!token) {
2021-03-30 05:09:43 +00:00
return;
2021-03-21 06:53:41 +00:00
}
await HTTPService.post(
`${ENDPOINT}/trash/delete`,
2021-05-30 16:56:48 +00:00
{ fileIDs: filesToDelete },
2021-03-21 06:53:41 +00:00
null,
{
'X-Auth-Token': token,
2021-08-13 02:38:38 +00:00
}
2021-03-21 06:53:41 +00:00
);
} catch (e) {
logError(e, 'delete from trash failed');
2021-05-31 10:58:33 +00:00
throw e;
2021-03-21 06:53:41 +00:00
}
};
2021-09-21 08:03:15 +00:00
2021-09-21 10:37:53 +00:00
export const updateMagicMetadata = async (files: File[]) => {
2021-09-21 08:03:15 +00:00
const token = getToken();
if (!token) {
return;
}
2021-09-21 10:37:53 +00:00
const reqBody: UpdateMagicMetadataRequest = { metadataList: [] };
const worker = await new CryptoWorker();
2021-09-21 10:37:53 +00:00
for (const file of files) {
const { file: encryptedMagicMetadata }: EncryptionResult =
await worker.encryptMetadata(file.magicMetadata.data, file.key);
2021-09-21 10:37:53 +00:00
reqBody.metadataList.push({
id: file.id,
magicMetadata: {
2021-10-30 15:09:52 +00:00
version: file.magicMetadata.version,
count: file.magicMetadata.count,
data: encryptedMagicMetadata.encryptedData as unknown as string,
header: encryptedMagicMetadata.decryptionHeader,
},
2021-09-21 10:37:53 +00:00
});
}
await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, {
'X-Auth-Token': token,
});
return files.map(
(file): File => ({
...file,
magicMetadata: {
...file.magicMetadata,
version: file.magicMetadata.version + 1,
},
})
);
2021-09-21 08:03:15 +00:00
};
export const updatePublicMagicMetadata = async (files: File[]) => {
const token = getToken();
if (!token) {
return;
}
const reqBody: UpdateMagicMetadataRequest = { metadataList: [] };
const worker = await new CryptoWorker();
for (const file of files) {
const { file: encryptedPubMagicMetadata }: EncryptionResult =
await worker.encryptMetadata(file.pubMagicMetadata.data, file.key);
reqBody.metadataList.push({
id: file.id,
magicMetadata: {
version: file.pubMagicMetadata.version,
count: file.pubMagicMetadata.count,
data: encryptedPubMagicMetadata.encryptedData as unknown as string,
header: encryptedPubMagicMetadata.decryptionHeader,
},
});
}
await HTTPService.put(
`${ENDPOINT}/files/public-magic-metadata`,
reqBody,
null,
{
'X-Auth-Token': token,
}
);
2021-11-02 06:22:13 +00:00
return files.map(
(file): File => ({
...file,
pubMagicMetadata: {
...file.pubMagicMetadata,
version: file.pubMagicMetadata.version + 1,
},
})
);
};