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';
|
2021-10-29 11:45:06 +00:00
|
|
|
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';
|
2021-11-04 13:07:35 +00:00
|
|
|
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
2021-10-29 11:45:06 +00:00
|
|
|
import CryptoWorker from 'utils/crypto';
|
2020-09-27 17:18:57 +00:00
|
|
|
|
2020-09-19 21:20:10 +00:00
|
|
|
const ENDPOINT = getEndpoint();
|
|
|
|
|
2021-11-04 13:07:35 +00:00
|
|
|
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;
|
2021-01-08 03:51:59 +00:00
|
|
|
decryptionHeader: string;
|
2021-01-05 10:23:28 +00:00
|
|
|
}
|
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,
|
|
|
|
}
|
|
|
|
|
2021-08-30 05:18:28 +00:00
|
|
|
/* 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' },
|
2021-08-31 11:59:33 +00:00
|
|
|
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg' },
|
2021-08-30 05:18:28 +00:00
|
|
|
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm' },
|
|
|
|
];
|
|
|
|
|
2021-09-21 07:21:26 +00:00
|
|
|
export enum VISIBILITY_STATE {
|
|
|
|
VISIBLE,
|
|
|
|
ARCHIVED,
|
|
|
|
}
|
2021-10-29 11:32:28 +00:00
|
|
|
|
|
|
|
export interface MagicMetadataCore {
|
2021-09-21 07:21:26 +00:00
|
|
|
version: number;
|
|
|
|
count: number;
|
|
|
|
header: string;
|
2021-10-29 11:32:28 +00:00
|
|
|
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
|
|
|
}
|
2021-10-29 11:32:28 +00:00
|
|
|
|
|
|
|
export interface MagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
|
|
|
data: MagicMetadataProps;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface PublicMagicMetadataProps {
|
2021-10-30 13:16:40 +00:00
|
|
|
editedTime?: number;
|
2021-10-29 11:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
|
|
|
data: PublicMagicMetadataProps;
|
|
|
|
}
|
|
|
|
|
2021-04-27 09:17:38 +00:00
|
|
|
export interface File {
|
2021-01-08 03:51:59 +00:00
|
|
|
id: number;
|
|
|
|
collectionID: number;
|
2021-09-21 06:37:39 +00:00
|
|
|
ownerID: number;
|
2021-01-08 03:51:59 +00:00
|
|
|
file: fileAttribute;
|
|
|
|
thumbnail: fileAttribute;
|
2021-02-16 09:44:25 +00:00
|
|
|
metadata: MetadataObject;
|
2021-09-21 07:21:26 +00:00
|
|
|
magicMetadata: MagicMetadata;
|
2021-10-29 11:32:28 +00:00
|
|
|
pubMagicMetadata: PublicMagicMetadata;
|
2021-01-08 03:51:59 +00:00
|
|
|
encryptedKey: string;
|
|
|
|
keyDecryptionNonce: string;
|
2021-01-18 02:26:34 +00:00
|
|
|
key: string;
|
2021-01-08 03:51:59 +00:00
|
|
|
src: string;
|
|
|
|
msrc: string;
|
|
|
|
html: string;
|
|
|
|
w: number;
|
|
|
|
h: number;
|
|
|
|
isDeleted: boolean;
|
2021-10-01 06:46:07 +00:00
|
|
|
isTrashed?: boolean;
|
2021-10-04 14:57:45 +00:00
|
|
|
deleteBy?: number;
|
2021-01-08 03:51:59 +00:00
|
|
|
dataIndex: number;
|
2021-01-20 13:51:22 +00:00
|
|
|
updationTime: number;
|
2021-01-05 10:23:28 +00:00
|
|
|
}
|
2020-09-19 21:20:10 +00:00
|
|
|
|
2021-09-21 10:37:53 +00:00
|
|
|
interface UpdateMagicMetadataRequest {
|
|
|
|
metadataList: UpdateMagicMetadata[];
|
|
|
|
}
|
2021-10-29 11:32:28 +00:00
|
|
|
|
2021-09-21 10:37:53 +00:00
|
|
|
interface UpdateMagicMetadata {
|
|
|
|
id: number;
|
2021-10-29 11:32:28 +00:00
|
|
|
magicMetadata: EncryptedMagicMetadataCore;
|
2021-09-21 10:37:53 +00:00
|
|
|
}
|
|
|
|
|
2021-10-29 11:32:28 +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;
|
|
|
|
}
|
2021-04-23 19:16:27 +00:00
|
|
|
export const getLocalFiles = async () => {
|
2021-11-04 13:07:35 +00:00
|
|
|
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-02-08 16:17:17 +00:00
|
|
|
};
|
2021-01-27 06:14:02 +00:00
|
|
|
|
2021-11-04 13:55:19 +00:00
|
|
|
const getCollectionLastSyncTime = async (collection: Collection) =>
|
2021-11-04 13:23:35 +00:00
|
|
|
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
|
|
|
|
|
2021-08-13 02:38:38 +00:00
|
|
|
export const syncFiles = async (
|
|
|
|
collections: Collection[],
|
|
|
|
setFiles: (files: File[]) => void
|
|
|
|
) => {
|
2021-04-23 19:16:27 +00:00
|
|
|
const localFiles = await getLocalFiles();
|
|
|
|
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
2021-05-29 06:27:52 +00:00
|
|
|
if (files.length !== localFiles.length) {
|
2021-11-04 13:07:35 +00:00
|
|
|
await setLocalFiles(files);
|
2021-04-23 19:16:27 +00:00
|
|
|
}
|
2021-11-09 11:04:44 +00:00
|
|
|
files = sortFiles(mergeMetadata(files));
|
|
|
|
setFiles([...files]);
|
2021-05-29 06:27:52 +00:00
|
|
|
for (const collection of collections) {
|
2021-03-16 06:49:34 +00:00
|
|
|
if (!getToken()) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-11-04 13:23:35 +00:00
|
|
|
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
2021-02-08 17:07:17 +00:00
|
|
|
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}`;
|
2021-02-08 16:17:17 +00:00
|
|
|
if (
|
2021-03-16 09:41:11 +00:00
|
|
|
!latestVersionFiles.has(uid) ||
|
|
|
|
latestVersionFiles.get(uid).updationTime < file.updationTime
|
2021-02-08 16:17:17 +00:00
|
|
|
) {
|
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) {
|
2021-02-08 16:17:17 +00:00
|
|
|
if (file.isDeleted) {
|
2021-02-08 10:56:47 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
files.push(file);
|
2021-02-06 07:54:36 +00:00
|
|
|
}
|
2021-11-04 13:07:35 +00:00
|
|
|
await setLocalFiles(files);
|
2021-02-08 16:17:17 +00:00
|
|
|
await localForage.setItem(
|
|
|
|
`${collection.id}-time`,
|
2021-08-13 02:38:38 +00:00
|
|
|
collection.updationTime
|
|
|
|
);
|
2021-11-09 11:04:44 +00:00
|
|
|
files = sortFiles(mergeMetadata(files));
|
|
|
|
setFiles([...files]);
|
2021-02-05 18:51:14 +00:00
|
|
|
}
|
2021-11-09 11:04:44 +00:00
|
|
|
return files;
|
2021-01-14 11:31:20 +00:00
|
|
|
};
|
|
|
|
|
2021-02-08 16:17:17 +00:00
|
|
|
export const getFiles = async (
|
2021-04-23 07:14:07 +00:00
|
|
|
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
|
2021-04-27 09:17:38 +00:00
|
|
|
): Promise<File[]> => {
|
2021-01-25 07:05:45 +00:00
|
|
|
try {
|
2021-04-27 09:17:38 +00:00
|
|
|
const decryptedFiles: File[] = [];
|
2021-11-04 13:23:35 +00:00
|
|
|
let time = sinceTime;
|
2021-02-05 18:51:14 +00:00
|
|
|
let resp;
|
|
|
|
do {
|
2021-03-16 06:49:34 +00:00
|
|
|
const token = getToken();
|
|
|
|
if (!token) {
|
|
|
|
break;
|
|
|
|
}
|
2021-02-08 16:17:17 +00:00
|
|
|
resp = await HTTPService.get(
|
2021-10-30 02:56:30 +00:00
|
|
|
`${ENDPOINT}/collections/v2/diff`,
|
2021-02-08 16:17:17 +00:00
|
|
|
{
|
2021-02-17 08:35:19 +00:00
|
|
|
collectionID: collection.id,
|
|
|
|
sinceTime: time,
|
2021-02-08 16:17:17 +00:00
|
|
|
},
|
2021-02-05 18:51:14 +00:00
|
|
|
{
|
2021-02-08 16:17:17 +00:00
|
|
|
'X-Auth-Token': token,
|
2021-08-13 02:38:38 +00:00
|
|
|
}
|
2021-02-08 16:17:17 +00:00
|
|
|
);
|
2021-09-22 09:22:50 +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>[]
|
|
|
|
))
|
|
|
|
);
|
2021-02-05 18:51:14 +00:00
|
|
|
|
|
|
|
if (resp.data.diff.length) {
|
2021-02-17 08:35:19 +00:00
|
|
|
time = resp.data.diff.slice(-1)[0].updationTime;
|
2021-02-05 18:51:14 +00:00
|
|
|
}
|
2021-08-13 02:38:38 +00:00
|
|
|
setFiles(
|
2021-10-29 12:09:57 +00:00
|
|
|
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-10-29 12:09:57 +00:00
|
|
|
)
|
2021-08-13 02:38:38 +00:00
|
|
|
);
|
2021-10-30 02:56:30 +00:00
|
|
|
} while (resp.data.hasMore);
|
2021-04-23 07:14:07 +00:00
|
|
|
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');
|
2021-01-14 11:31:20 +00:00
|
|
|
}
|
2021-02-08 16:17:17 +00:00
|
|
|
};
|
2021-01-05 10:23:28 +00:00
|
|
|
|
2021-02-08 17:07:17 +00:00
|
|
|
const removeDeletedCollectionFiles = async (
|
2021-04-23 07:14:07 +00:00
|
|
|
collections: Collection[],
|
2021-08-13 02:38:38 +00:00
|
|
|
files: File[]
|
2021-02-08 17:07:17 +00:00
|
|
|
) => {
|
2021-02-08 16:17:17 +00:00
|
|
|
const syncedCollectionIds = new Set<number>();
|
2021-05-29 06:27:52 +00:00
|
|
|
for (const collection of collections) {
|
2021-02-08 16:17:17 +00:00
|
|
|
syncedCollectionIds.add(collection.id);
|
|
|
|
}
|
|
|
|
files = files.filter((file) => syncedCollectionIds.has(file.collectionID));
|
2021-02-09 05:07:46 +00:00
|
|
|
return files;
|
2021-02-08 16:17:17 +00:00
|
|
|
};
|
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
|
|
|
|
2021-10-04 05:22:12 +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(
|
2021-10-04 05:22:12 +00:00
|
|
|
`${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) {
|
2021-10-04 05:22:12 +00:00
|
|
|
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: [] };
|
2021-10-29 11:45:06 +00:00
|
|
|
const worker = await new CryptoWorker();
|
2021-09-21 10:37:53 +00:00
|
|
|
for (const file of files) {
|
2021-10-29 11:45:06 +00:00
|
|
|
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,
|
2021-10-29 11:45:06 +00:00
|
|
|
magicMetadata: {
|
2021-10-30 15:09:52 +00:00
|
|
|
version: file.magicMetadata.version,
|
|
|
|
count: file.magicMetadata.count,
|
2021-10-29 11:45:06 +00:00
|
|
|
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,
|
|
|
|
});
|
2021-11-02 06:52:38 +00:00
|
|
|
return files.map(
|
|
|
|
(file): File => ({
|
|
|
|
...file,
|
|
|
|
magicMetadata: {
|
|
|
|
...file.magicMetadata,
|
|
|
|
version: file.magicMetadata.version + 1,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
2021-09-21 08:03:15 +00:00
|
|
|
};
|
2021-10-29 11:52:08 +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,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
2021-10-29 11:52:08 +00:00
|
|
|
};
|