Clean up
This commit is contained in:
parent
36cad03c71
commit
ac12ce7a19
8 changed files with 2 additions and 2996 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,415 +0,0 @@
|
|||
import {
|
||||
generateStreamFromArrayBuffer,
|
||||
getRenderableFileURL,
|
||||
createTypedObjectURL,
|
||||
} from 'utils/file';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { CacheStorageService } from './cache/cacheStorageService';
|
||||
import { CACHES } from 'constants/cache';
|
||||
import { Remote } from 'comlink';
|
||||
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||
import { LimitedCache } from 'types/cache';
|
||||
import { retryAsyncFunction } from 'utils/network';
|
||||
import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
||||
import { getFileURL, getThumbnailURL } from '@ente/shared/network/api';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { addLogLine } from '@ente/shared/logging';
|
||||
|
||||
class DownloadManager {
|
||||
private fileObjectURLPromise = new Map<
|
||||
string,
|
||||
Promise<{ original: string[]; converted: string[] }>
|
||||
>();
|
||||
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
|
||||
|
||||
private fileDownloadProgress = new Map<number, number>();
|
||||
|
||||
private progressUpdater: (value: Map<number, number>) => void = () => {};
|
||||
|
||||
private thumbnailCache: LimitedCache;
|
||||
|
||||
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
|
||||
this.progressUpdater = progressUpdater;
|
||||
}
|
||||
|
||||
private async getThumbnailCache() {
|
||||
try {
|
||||
if (!this.thumbnailCache) {
|
||||
this.thumbnailCache = await CacheStorageService.open(
|
||||
CACHES.THUMBS
|
||||
);
|
||||
}
|
||||
return this.thumbnailCache;
|
||||
} catch (e) {
|
||||
return null;
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async getCachedThumbnail(file: EnteFile) {
|
||||
try {
|
||||
const thumbnailCache = await this.getThumbnailCache();
|
||||
const cacheResp: Response = await thumbnailCache?.match(
|
||||
file.id.toString()
|
||||
);
|
||||
|
||||
if (cacheResp) {
|
||||
return URL.createObjectURL(await cacheResp.blob());
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
logError(e, 'failed to get cached thumbnail');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async getThumbnail(
|
||||
file: EnteFile,
|
||||
tokenOverride?: string,
|
||||
usingWorker?: Remote<DedicatedCryptoWorker>,
|
||||
timeout?: number
|
||||
) {
|
||||
try {
|
||||
const token = tokenOverride || getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
if (!this.thumbnailObjectURLPromise.has(file.id)) {
|
||||
const downloadPromise = async () => {
|
||||
const thumbnailCache = await this.getThumbnailCache();
|
||||
const cachedThumb = await this.getCachedThumbnail(file);
|
||||
if (cachedThumb) {
|
||||
return cachedThumb;
|
||||
}
|
||||
const thumb = await this.downloadThumb(
|
||||
token,
|
||||
file,
|
||||
usingWorker,
|
||||
timeout
|
||||
);
|
||||
const thumbBlob = new Blob([thumb]);
|
||||
|
||||
thumbnailCache
|
||||
?.put(file.id.toString(), new Response(thumbBlob))
|
||||
.catch((e) => {
|
||||
logError(e, 'cache put failed');
|
||||
// TODO: handle storage full exception.
|
||||
});
|
||||
return URL.createObjectURL(thumbBlob);
|
||||
};
|
||||
this.thumbnailObjectURLPromise.set(file.id, downloadPromise());
|
||||
}
|
||||
|
||||
return await this.thumbnailObjectURLPromise.get(file.id);
|
||||
} catch (e) {
|
||||
this.thumbnailObjectURLPromise.delete(file.id);
|
||||
logError(e, 'get DownloadManager preview Failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
downloadThumb = async (
|
||||
token: string,
|
||||
file: EnteFile,
|
||||
usingWorker?: Remote<DedicatedCryptoWorker>,
|
||||
timeout?: number
|
||||
) => {
|
||||
const resp = await HTTPService.get(
|
||||
getThumbnailURL(file.id),
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ responseType: 'arraybuffer', timeout }
|
||||
);
|
||||
if (typeof resp.data === 'undefined') {
|
||||
throw Error(CustomError.REQUEST_FAILED);
|
||||
}
|
||||
const cryptoWorker =
|
||||
usingWorker || (await ComlinkCryptoWorker.getInstance());
|
||||
const decrypted = await cryptoWorker.decryptThumbnail(
|
||||
new Uint8Array(resp.data),
|
||||
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.key
|
||||
);
|
||||
return decrypted;
|
||||
};
|
||||
|
||||
getFile = async (file: EnteFile, forPreview = false) => {
|
||||
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
||||
try {
|
||||
const getFilePromise = async () => {
|
||||
const fileStream = await this.downloadFile(file);
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
if (forPreview) {
|
||||
return await getRenderableFileURL(file, fileBlob);
|
||||
} else {
|
||||
const fileURL = await createTypedObjectURL(
|
||||
fileBlob,
|
||||
file.metadata.title
|
||||
);
|
||||
return { converted: [fileURL], original: [fileURL] };
|
||||
}
|
||||
};
|
||||
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
||||
}
|
||||
const fileURLs = await this.fileObjectURLPromise.get(fileKey);
|
||||
return fileURLs;
|
||||
} catch (e) {
|
||||
this.fileObjectURLPromise.delete(fileKey);
|
||||
logError(e, 'download manager Failed to get File');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
public async getCachedOriginalFile(file: EnteFile) {
|
||||
return (await this.fileObjectURLPromise.get(file.id.toString()))
|
||||
?.original;
|
||||
}
|
||||
|
||||
async downloadFile(
|
||||
file: EnteFile,
|
||||
tokenOverride?: string,
|
||||
usingWorker?: Remote<DedicatedCryptoWorker>,
|
||||
timeout?: number
|
||||
) {
|
||||
try {
|
||||
const cryptoWorker =
|
||||
usingWorker || (await ComlinkCryptoWorker.getInstance());
|
||||
const token = tokenOverride || getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
const onDownloadProgress = this.trackDownloadProgress(
|
||||
file.id,
|
||||
file.info?.fileSize
|
||||
);
|
||||
if (
|
||||
file.metadata.fileType === FILE_TYPE.IMAGE ||
|
||||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
|
||||
) {
|
||||
const resp = await retryAsyncFunction(() =>
|
||||
HTTPService.get(
|
||||
getFileURL(file.id),
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
timeout,
|
||||
onDownloadProgress,
|
||||
}
|
||||
)
|
||||
);
|
||||
this.clearDownloadProgress(file.id);
|
||||
if (typeof resp.data === 'undefined') {
|
||||
throw Error(CustomError.REQUEST_FAILED);
|
||||
}
|
||||
try {
|
||||
const decrypted = await cryptoWorker.decryptFile(
|
||||
new Uint8Array(resp.data),
|
||||
await cryptoWorker.fromB64(file.file.decryptionHeader),
|
||||
file.key
|
||||
);
|
||||
return generateStreamFromArrayBuffer(decrypted);
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.PROCESSING_FAILED) {
|
||||
logError(e, 'Failed to process file', {
|
||||
fileID: file.id,
|
||||
fromMobile:
|
||||
!!file.metadata.localID ||
|
||||
!!file.metadata.deviceFolder ||
|
||||
!!file.metadata.version,
|
||||
});
|
||||
addLogLine(
|
||||
`Failed to process file with fileID:${file.id}, localID: ${file.metadata.localID}, version: ${file.metadata.version}, deviceFolder:${file.metadata.deviceFolder} with error: ${e.message}`
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const resp = await retryAsyncFunction(() =>
|
||||
fetch(getFileURL(file.id), {
|
||||
headers: {
|
||||
'X-Auth-Token': token,
|
||||
},
|
||||
})
|
||||
);
|
||||
const reader = resp.body.getReader();
|
||||
|
||||
const contentLength = +resp.headers.get('Content-Length') ?? 0;
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
const decryptionHeader = await cryptoWorker.fromB64(
|
||||
file.file.decryptionHeader
|
||||
);
|
||||
const fileKey = await cryptoWorker.fromB64(file.key);
|
||||
const { pullState, decryptionChunkSize } =
|
||||
await cryptoWorker.initChunkDecryption(
|
||||
decryptionHeader,
|
||||
fileKey
|
||||
);
|
||||
let data = new Uint8Array();
|
||||
// The following function handles each data chunk
|
||||
const push = () => {
|
||||
// "done" is a Boolean and value a "Uint8Array"
|
||||
reader.read().then(async ({ done, value }) => {
|
||||
try {
|
||||
// Is there more data to read?
|
||||
if (!done) {
|
||||
downloadedBytes += value.byteLength;
|
||||
onDownloadProgress({
|
||||
loaded: downloadedBytes,
|
||||
total: contentLength,
|
||||
});
|
||||
const buffer = new Uint8Array(
|
||||
data.byteLength + value.byteLength
|
||||
);
|
||||
buffer.set(new Uint8Array(data), 0);
|
||||
buffer.set(
|
||||
new Uint8Array(value),
|
||||
data.byteLength
|
||||
);
|
||||
if (
|
||||
buffer.length > decryptionChunkSize
|
||||
) {
|
||||
const fileData = buffer.slice(
|
||||
0,
|
||||
decryptionChunkSize
|
||||
);
|
||||
try {
|
||||
const { decryptedData } =
|
||||
await cryptoWorker.decryptFileChunk(
|
||||
fileData,
|
||||
pullState
|
||||
);
|
||||
controller.enqueue(
|
||||
decryptedData
|
||||
);
|
||||
data =
|
||||
buffer.slice(
|
||||
decryptionChunkSize
|
||||
);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message ===
|
||||
CustomError.PROCESSING_FAILED
|
||||
) {
|
||||
logError(
|
||||
e,
|
||||
'Failed to process file',
|
||||
{
|
||||
fileID: file.id,
|
||||
fromMobile:
|
||||
!!file.metadata
|
||||
.localID ||
|
||||
!!file.metadata
|
||||
.deviceFolder ||
|
||||
!!file.metadata
|
||||
.version,
|
||||
}
|
||||
);
|
||||
addLogLine(
|
||||
`Failed to process file ${file.id} from localID: ${file.metadata.localID} version: ${file.metadata.version} deviceFolder:${file.metadata.deviceFolder} with error: ${e.message}`
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
data = buffer;
|
||||
}
|
||||
push();
|
||||
} else {
|
||||
if (data) {
|
||||
try {
|
||||
const { decryptedData } =
|
||||
await cryptoWorker.decryptFileChunk(
|
||||
data,
|
||||
pullState
|
||||
);
|
||||
controller.enqueue(
|
||||
decryptedData
|
||||
);
|
||||
data = null;
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message ===
|
||||
CustomError.PROCESSING_FAILED
|
||||
) {
|
||||
logError(
|
||||
e,
|
||||
'Failed to process file',
|
||||
{
|
||||
fileID: file.id,
|
||||
fromMobile:
|
||||
!!file.metadata
|
||||
.localID ||
|
||||
!!file.metadata
|
||||
.deviceFolder ||
|
||||
!!file.metadata
|
||||
.version,
|
||||
}
|
||||
);
|
||||
addLogLine(
|
||||
`Failed to process file ${file.id} from localID: ${file.metadata.localID} version: ${file.metadata.version} deviceFolder:${file.metadata.deviceFolder} with error: ${e.message}`
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to process file chunk');
|
||||
controller.error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
push();
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to process file stream');
|
||||
controller.error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
return stream;
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to download file');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
trackDownloadProgress = (fileID: number, fileSize: number) => {
|
||||
return (event: { loaded: number; total: number }) => {
|
||||
if (isNaN(event.total) || event.total === 0) {
|
||||
if (!fileSize) {
|
||||
return;
|
||||
}
|
||||
event.total = fileSize;
|
||||
}
|
||||
if (event.loaded === event.total) {
|
||||
this.fileDownloadProgress.delete(fileID);
|
||||
} else {
|
||||
this.fileDownloadProgress.set(
|
||||
fileID,
|
||||
Math.round((event.loaded * 100) / event.total)
|
||||
);
|
||||
}
|
||||
this.progressUpdater(new Map(this.fileDownloadProgress));
|
||||
};
|
||||
};
|
||||
|
||||
clearDownloadProgress = (fileID: number) => {
|
||||
this.fileDownloadProgress.delete(fileID);
|
||||
this.progressUpdater(new Map(this.fileDownloadProgress));
|
||||
};
|
||||
}
|
||||
|
||||
export default new DownloadManager();
|
|
@ -1,312 +0,0 @@
|
|||
import { getEndpoint } from '@ente/shared/network/api';
|
||||
|
||||
import { getToken } from '@ente/shared/storage/localStorage/helpers';
|
||||
import { Collection } from 'types/collection';
|
||||
|
||||
import {
|
||||
decryptFile,
|
||||
getLatestVersionFiles,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { eventBus, Events } from './events';
|
||||
import {
|
||||
EnteFile,
|
||||
EncryptedEnteFile,
|
||||
TrashRequest,
|
||||
FileWithUpdatedMagicMetadata,
|
||||
FileWithUpdatedPublicMagicMetadata,
|
||||
} from 'types/file';
|
||||
import { SetFiles } from 'types/gallery';
|
||||
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import {
|
||||
getCollectionLastSyncTime,
|
||||
setCollectionLastSyncTime,
|
||||
} from './collectionService';
|
||||
import { REQUEST_BATCH_SIZE } from 'constants/api';
|
||||
import { batch } from '@ente/shared/batch';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import localForage from '@ente/shared/storage/localForage';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { addLogLine } from '@ente/shared/logging';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const FILES_TABLE = 'files';
|
||||
const HIDDEN_FILES_TABLE = 'hidden-files';
|
||||
|
||||
export const getLocalFiles = async (type: 'normal' | 'hidden' = 'normal') => {
|
||||
const tableName = type === 'normal' ? FILES_TABLE : HIDDEN_FILES_TABLE;
|
||||
const files: Array<EnteFile> =
|
||||
(await localForage.getItem<EnteFile[]>(tableName)) || [];
|
||||
return files;
|
||||
};
|
||||
|
||||
const setLocalFiles = async (type: 'normal' | 'hidden', files: EnteFile[]) => {
|
||||
try {
|
||||
const tableName = type === 'normal' ? FILES_TABLE : HIDDEN_FILES_TABLE;
|
||||
await localForage.setItem(tableName, files);
|
||||
try {
|
||||
eventBus.emit(Events.LOCAL_FILES_UPDATED);
|
||||
} catch (e) {
|
||||
logError(e, 'Error in localFileUpdated handlers');
|
||||
}
|
||||
} catch (e1) {
|
||||
try {
|
||||
const storageEstimate = await navigator.storage.estimate();
|
||||
logError(e1, 'failed to save files to indexedDB', {
|
||||
storageEstimate,
|
||||
});
|
||||
addLogLine(`storage estimate ${JSON.stringify(storageEstimate)}`);
|
||||
} catch (e2) {
|
||||
logError(e1, 'failed to save files to indexedDB');
|
||||
logError(e2, 'failed to get storage stats');
|
||||
}
|
||||
throw e1;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllLocalFiles = async () => {
|
||||
const normalFiles = await getLocalFiles('normal');
|
||||
const hiddenFiles = await getLocalFiles('hidden');
|
||||
return [...normalFiles, ...hiddenFiles];
|
||||
};
|
||||
|
||||
export const syncFiles = async (
|
||||
type: 'normal' | 'hidden',
|
||||
collections: Collection[],
|
||||
setFiles: SetFiles
|
||||
) => {
|
||||
const localFiles = await getLocalFiles(type);
|
||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||
if (files.length !== localFiles.length) {
|
||||
await setLocalFiles(type, files);
|
||||
setFiles(sortFiles(mergeMetadata(files)));
|
||||
}
|
||||
for (const collection of collections) {
|
||||
if (!getToken()) {
|
||||
continue;
|
||||
}
|
||||
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
||||
if (collection.updationTime === lastSyncTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newFiles = await getFiles(collection, lastSyncTime, setFiles);
|
||||
files = getLatestVersionFiles([...files, ...newFiles]);
|
||||
await setLocalFiles(type, files);
|
||||
setCollectionLastSyncTime(collection, collection.updationTime);
|
||||
}
|
||||
return files;
|
||||
};
|
||||
|
||||
const getFiles = async (
|
||||
collection: Collection,
|
||||
sinceTime: number,
|
||||
setFiles: SetFiles
|
||||
): Promise<EnteFile[]> => {
|
||||
try {
|
||||
let decryptedFiles: EnteFile[] = [];
|
||||
let time = sinceTime;
|
||||
let resp;
|
||||
do {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
break;
|
||||
}
|
||||
resp = await HTTPService.get(
|
||||
`${ENDPOINT}/collections/v2/diff`,
|
||||
{
|
||||
collectionID: collection.id,
|
||||
sinceTime: time,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
|
||||
const newDecryptedFilesBatch = await Promise.all(
|
||||
resp.data.diff.map(async (file: EncryptedEnteFile) => {
|
||||
if (!file.isDeleted) {
|
||||
return await decryptFile(file, collection.key);
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}) as Promise<EnteFile>[]
|
||||
);
|
||||
decryptedFiles = [...decryptedFiles, ...newDecryptedFilesBatch];
|
||||
|
||||
setFiles((files) =>
|
||||
sortFiles(
|
||||
mergeMetadata(
|
||||
getLatestVersionFiles([
|
||||
...(files || []),
|
||||
...decryptedFiles,
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||
}
|
||||
} while (resp.data.hasMore);
|
||||
return decryptedFiles;
|
||||
} catch (e) {
|
||||
logError(e, 'Get files failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const removeDeletedCollectionFiles = async (
|
||||
collections: Collection[],
|
||||
files: EnteFile[]
|
||||
) => {
|
||||
const syncedCollectionIds = new Set<number>();
|
||||
for (const collection of collections) {
|
||||
syncedCollectionIds.add(collection.id);
|
||||
}
|
||||
files = files.filter((file) => syncedCollectionIds.has(file.collectionID));
|
||||
return files;
|
||||
};
|
||||
|
||||
export const trashFiles = async (filesToTrash: EnteFile[]) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const batchedFilesToTrash = batch(filesToTrash, REQUEST_BATCH_SIZE);
|
||||
for (const batch of batchedFilesToTrash) {
|
||||
const trashRequest: TrashRequest = {
|
||||
items: batch.map((file) => ({
|
||||
fileID: file.id,
|
||||
collectionID: file.collectionID,
|
||||
})),
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/files/trash`,
|
||||
trashRequest,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'trash file failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFromTrash = async (filesToDelete: number[]) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const batchedFilesToDelete = batch(filesToDelete, REQUEST_BATCH_SIZE);
|
||||
|
||||
for (const batch of batchedFilesToDelete) {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/trash/delete`,
|
||||
{ fileIDs: batch },
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'deleteFromTrash failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFileMagicMetadata = async (
|
||||
fileWithUpdatedMagicMetadataList: FileWithUpdatedMagicMetadata[]
|
||||
) => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
for (const {
|
||||
file,
|
||||
updatedMagicMetadata,
|
||||
} of fileWithUpdatedMagicMetadataList) {
|
||||
const { file: encryptedMagicMetadata } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
file.key
|
||||
);
|
||||
reqBody.metadataList.push({
|
||||
id: file.id,
|
||||
magicMetadata: {
|
||||
version: updatedMagicMetadata.version,
|
||||
count: updatedMagicMetadata.count,
|
||||
data: encryptedMagicMetadata.encryptedData,
|
||||
header: encryptedMagicMetadata.decryptionHeader,
|
||||
},
|
||||
});
|
||||
}
|
||||
await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
return fileWithUpdatedMagicMetadataList.map(
|
||||
({ file, updatedMagicMetadata }): EnteFile => ({
|
||||
...file,
|
||||
magicMetadata: {
|
||||
...updatedMagicMetadata,
|
||||
version: updatedMagicMetadata.version + 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const updateFilePublicMagicMetadata = async (
|
||||
fileWithUpdatedPublicMagicMetadataList: FileWithUpdatedPublicMagicMetadata[]
|
||||
): Promise<EnteFile[]> => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
for (const {
|
||||
file,
|
||||
updatedPublicMagicMetadata: updatePublicMagicMetadata,
|
||||
} of fileWithUpdatedPublicMagicMetadataList) {
|
||||
const { file: encryptedPubMagicMetadata } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatePublicMagicMetadata.data,
|
||||
file.key
|
||||
);
|
||||
reqBody.metadataList.push({
|
||||
id: file.id,
|
||||
magicMetadata: {
|
||||
version: updatePublicMagicMetadata.version,
|
||||
count: updatePublicMagicMetadata.count,
|
||||
data: encryptedPubMagicMetadata.encryptedData,
|
||||
header: encryptedPubMagicMetadata.decryptionHeader,
|
||||
},
|
||||
});
|
||||
}
|
||||
await HTTPService.put(
|
||||
`${ENDPOINT}/files/public-magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
return fileWithUpdatedPublicMagicMetadataList.map(
|
||||
({ file, updatedPublicMagicMetadata }): EnteFile => ({
|
||||
...file,
|
||||
pubMagicMetadata: {
|
||||
...updatedPublicMagicMetadata,
|
||||
version: updatedPublicMagicMetadata.version + 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
|
@ -1,373 +0,0 @@
|
|||
import { EXIFLESS_FORMATS, NULL_LOCATION } from 'constants/upload';
|
||||
import { Location } from 'types/upload';
|
||||
import exifr from 'exifr';
|
||||
import piexif from 'piexifjs';
|
||||
import { FileTypeInfo } from 'types/upload';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { validateAndGetCreationUnixTimeInMicroSeconds } from 'utils/time';
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
|
||||
const EXIFR_UNSUPPORTED_FILE_FORMAT_MESSAGE = 'Unknown file format';
|
||||
|
||||
type ParsedEXIFData = Record<string, any> &
|
||||
Partial<{
|
||||
DateTimeOriginal: Date;
|
||||
CreateDate: Date;
|
||||
ModifyDate: Date;
|
||||
DateCreated: Date;
|
||||
MetadataDate: Date;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
}>;
|
||||
|
||||
type RawEXIFData = Record<string, any> &
|
||||
Partial<{
|
||||
DateTimeOriginal: string;
|
||||
CreateDate: string;
|
||||
ModifyDate: string;
|
||||
DateCreated: string;
|
||||
MetadataDate: string;
|
||||
GPSLatitude: number[];
|
||||
GPSLongitude: number[];
|
||||
GPSLatitudeRef: string;
|
||||
GPSLongitudeRef: string;
|
||||
ImageWidth: number;
|
||||
ImageHeight: number;
|
||||
}>;
|
||||
|
||||
export async function getParsedExifData(
|
||||
receivedFile: File,
|
||||
fileTypeInfo: FileTypeInfo,
|
||||
tags?: string[]
|
||||
): Promise<ParsedEXIFData> {
|
||||
try {
|
||||
if (EXIFLESS_FORMATS.includes(fileTypeInfo.exactType)) {
|
||||
return null;
|
||||
}
|
||||
const exifData: RawEXIFData = await exifr.parse(receivedFile, {
|
||||
reviveValues: false,
|
||||
tiff: true,
|
||||
xmp: true,
|
||||
icc: true,
|
||||
iptc: true,
|
||||
jfif: true,
|
||||
ihdr: true,
|
||||
});
|
||||
if (!exifData) {
|
||||
return null;
|
||||
}
|
||||
const filteredExifData = tags
|
||||
? Object.fromEntries(
|
||||
Object.entries(exifData).filter(([key]) => tags.includes(key))
|
||||
)
|
||||
: exifData;
|
||||
return parseExifData(filteredExifData);
|
||||
} catch (e) {
|
||||
if (e.message === EXIFR_UNSUPPORTED_FILE_FORMAT_MESSAGE) {
|
||||
logError(e, 'exif library unsupported format', {
|
||||
fileType: fileTypeInfo.exactType,
|
||||
});
|
||||
} else {
|
||||
logError(e, 'get parsed exif data failed', {
|
||||
fileType: fileTypeInfo.exactType,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseExifData(exifData: RawEXIFData): ParsedEXIFData {
|
||||
if (!exifData) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
DateTimeOriginal,
|
||||
CreateDate,
|
||||
ModifyDate,
|
||||
DateCreated,
|
||||
ImageHeight,
|
||||
ImageWidth,
|
||||
ExifImageHeight,
|
||||
ExifImageWidth,
|
||||
PixelXDimension,
|
||||
PixelYDimension,
|
||||
MetadataDate,
|
||||
...rest
|
||||
} = exifData;
|
||||
const parsedExif: ParsedEXIFData = { ...rest };
|
||||
if (DateTimeOriginal) {
|
||||
parsedExif.DateTimeOriginal = parseEXIFDate(exifData.DateTimeOriginal);
|
||||
}
|
||||
if (CreateDate) {
|
||||
parsedExif.CreateDate = parseEXIFDate(exifData.CreateDate);
|
||||
}
|
||||
if (ModifyDate) {
|
||||
parsedExif.ModifyDate = parseEXIFDate(exifData.ModifyDate);
|
||||
}
|
||||
if (DateCreated) {
|
||||
parsedExif.DateCreated = parseEXIFDate(exifData.DateCreated);
|
||||
}
|
||||
if (MetadataDate) {
|
||||
parsedExif.MetadataDate = parseEXIFDate(exifData.MetadataDate);
|
||||
}
|
||||
if (exifData.GPSLatitude && exifData.GPSLongitude) {
|
||||
const parsedLocation = parseEXIFLocation(
|
||||
exifData.GPSLatitude,
|
||||
exifData.GPSLatitudeRef,
|
||||
exifData.GPSLongitude,
|
||||
exifData.GPSLongitudeRef
|
||||
);
|
||||
parsedExif.latitude = parsedLocation.latitude;
|
||||
parsedExif.longitude = parsedLocation.longitude;
|
||||
}
|
||||
if (ImageWidth && ImageHeight) {
|
||||
if (typeof ImageWidth === 'number' && typeof ImageHeight === 'number') {
|
||||
parsedExif.imageWidth = ImageWidth;
|
||||
parsedExif.imageHeight = ImageHeight;
|
||||
} else {
|
||||
logError(
|
||||
new Error('ImageWidth or ImageHeight is not a number'),
|
||||
'Image dimension parsing failed',
|
||||
{
|
||||
ImageWidth,
|
||||
ImageHeight,
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (ExifImageWidth && ExifImageHeight) {
|
||||
if (
|
||||
typeof ExifImageWidth === 'number' &&
|
||||
typeof ExifImageHeight === 'number'
|
||||
) {
|
||||
parsedExif.imageWidth = ExifImageWidth;
|
||||
parsedExif.imageHeight = ExifImageHeight;
|
||||
} else {
|
||||
logError(
|
||||
new Error('ExifImageWidth or ExifImageHeight is not a number'),
|
||||
'Image dimension parsing failed',
|
||||
{
|
||||
ExifImageWidth,
|
||||
ExifImageHeight,
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (PixelXDimension && PixelYDimension) {
|
||||
if (
|
||||
typeof PixelXDimension === 'number' &&
|
||||
typeof PixelYDimension === 'number'
|
||||
) {
|
||||
parsedExif.imageWidth = PixelXDimension;
|
||||
parsedExif.imageHeight = PixelYDimension;
|
||||
} else {
|
||||
logError(
|
||||
new Error('PixelXDimension or PixelYDimension is not a number'),
|
||||
'Image dimension parsing failed',
|
||||
{
|
||||
PixelXDimension,
|
||||
PixelYDimension,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return parsedExif;
|
||||
}
|
||||
|
||||
function parseEXIFDate(dateTimeString: string) {
|
||||
try {
|
||||
if (typeof dateTimeString !== 'string' || dateTimeString === '') {
|
||||
throw Error(CustomError.NOT_A_DATE);
|
||||
}
|
||||
|
||||
// Check and parse date in the format YYYYMMDD
|
||||
if (dateTimeString.length === 8) {
|
||||
const year = Number(dateTimeString.slice(0, 4));
|
||||
const month = Number(dateTimeString.slice(4, 6));
|
||||
const day = Number(dateTimeString.slice(6, 8));
|
||||
if (
|
||||
!Number.isNaN(year) &&
|
||||
!Number.isNaN(month) &&
|
||||
!Number.isNaN(day)
|
||||
) {
|
||||
const date = new Date(year, month - 1, day);
|
||||
if (!Number.isNaN(+date)) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
const [year, month, day, hour, minute, second] = dateTimeString
|
||||
.match(/\d+/g)
|
||||
.map(Number);
|
||||
|
||||
if (
|
||||
typeof year === 'undefined' ||
|
||||
Number.isNaN(year) ||
|
||||
typeof month === 'undefined' ||
|
||||
Number.isNaN(month) ||
|
||||
typeof day === 'undefined' ||
|
||||
Number.isNaN(day)
|
||||
) {
|
||||
throw Error(CustomError.NOT_A_DATE);
|
||||
}
|
||||
let date: Date;
|
||||
if (
|
||||
typeof hour === 'undefined' ||
|
||||
Number.isNaN(hour) ||
|
||||
typeof minute === 'undefined' ||
|
||||
Number.isNaN(minute) ||
|
||||
typeof second === 'undefined' ||
|
||||
Number.isNaN(second)
|
||||
) {
|
||||
date = new Date(year, month - 1, day);
|
||||
} else {
|
||||
date = new Date(year, month - 1, day, hour, minute, second);
|
||||
}
|
||||
if (Number.isNaN(+date)) {
|
||||
throw Error(CustomError.NOT_A_DATE);
|
||||
}
|
||||
return date;
|
||||
} catch (e) {
|
||||
logError(e, 'parseEXIFDate failed', {
|
||||
dateTimeString,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEXIFLocation(
|
||||
gpsLatitude: number[],
|
||||
gpsLatitudeRef: string,
|
||||
gpsLongitude: number[],
|
||||
gpsLongitudeRef: string
|
||||
) {
|
||||
try {
|
||||
if (
|
||||
!Array.isArray(gpsLatitude) ||
|
||||
!Array.isArray(gpsLongitude) ||
|
||||
gpsLatitude.length !== 3 ||
|
||||
gpsLongitude.length !== 3
|
||||
) {
|
||||
throw Error(CustomError.NOT_A_LOCATION);
|
||||
}
|
||||
const latitude = convertDMSToDD(
|
||||
gpsLatitude[0],
|
||||
gpsLatitude[1],
|
||||
gpsLatitude[2],
|
||||
gpsLatitudeRef
|
||||
);
|
||||
const longitude = convertDMSToDD(
|
||||
gpsLongitude[0],
|
||||
gpsLongitude[1],
|
||||
gpsLongitude[2],
|
||||
gpsLongitudeRef
|
||||
);
|
||||
return { latitude, longitude };
|
||||
} catch (e) {
|
||||
logError(e, 'parseEXIFLocation failed', {
|
||||
gpsLatitude,
|
||||
gpsLatitudeRef,
|
||||
gpsLongitude,
|
||||
gpsLongitudeRef,
|
||||
});
|
||||
return NULL_LOCATION;
|
||||
}
|
||||
}
|
||||
|
||||
function convertDMSToDD(
|
||||
degrees: number,
|
||||
minutes: number,
|
||||
seconds: number,
|
||||
direction: string
|
||||
) {
|
||||
let dd = degrees + minutes / 60 + seconds / (60 * 60);
|
||||
if (direction === 'S' || direction === 'W') dd *= -1;
|
||||
return dd;
|
||||
}
|
||||
|
||||
export function getEXIFLocation(exifData: ParsedEXIFData): Location {
|
||||
if (!exifData || (!exifData.latitude && exifData.latitude !== 0)) {
|
||||
return NULL_LOCATION;
|
||||
}
|
||||
return { latitude: exifData.latitude, longitude: exifData.longitude };
|
||||
}
|
||||
|
||||
export function getEXIFTime(exifData: ParsedEXIFData): number {
|
||||
if (!exifData) {
|
||||
return null;
|
||||
}
|
||||
const dateTime =
|
||||
exifData.DateTimeOriginal ??
|
||||
exifData.DateCreated ??
|
||||
exifData.CreateDate ??
|
||||
exifData.MetadataDate ??
|
||||
exifData.ModifyDate;
|
||||
if (!dateTime) {
|
||||
return null;
|
||||
}
|
||||
return validateAndGetCreationUnixTimeInMicroSeconds(dateTime);
|
||||
}
|
||||
|
||||
export async function updateFileCreationDateInEXIF(
|
||||
reader: FileReader,
|
||||
fileBlob: Blob,
|
||||
updatedDate: Date
|
||||
) {
|
||||
try {
|
||||
let imageDataURL = await convertImageToDataURL(reader, fileBlob);
|
||||
imageDataURL =
|
||||
'data:image/jpeg;base64' +
|
||||
imageDataURL.slice(imageDataURL.indexOf(','));
|
||||
const exifObj = piexif.load(imageDataURL);
|
||||
if (!exifObj['Exif']) {
|
||||
exifObj['Exif'] = {};
|
||||
}
|
||||
exifObj['Exif'][piexif.ExifIFD.DateTimeOriginal] =
|
||||
convertToExifDateFormat(updatedDate);
|
||||
|
||||
const exifBytes = piexif.dump(exifObj);
|
||||
const exifInsertedFile = piexif.insert(exifBytes, imageDataURL);
|
||||
return dataURIToBlob(exifInsertedFile);
|
||||
} catch (e) {
|
||||
logError(e, 'updateFileModifyDateInEXIF failed');
|
||||
return fileBlob;
|
||||
}
|
||||
}
|
||||
|
||||
async function convertImageToDataURL(reader: FileReader, blob: Blob) {
|
||||
const dataURL = await new Promise<string>((resolve) => {
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
function dataURIToBlob(dataURI: string) {
|
||||
// convert base64 to raw binary data held in a string
|
||||
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
|
||||
const byteString = atob(dataURI.split(',')[1]);
|
||||
|
||||
// separate out the mime component
|
||||
const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||
|
||||
// write the bytes of the string to an ArrayBuffer
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
|
||||
// create a view into the buffer
|
||||
const ia = new Uint8Array(ab);
|
||||
|
||||
// set the bytes of the buffer to the correct values
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// write the ArrayBuffer to a blob, and you're done
|
||||
const blob = new Blob([ab], { type: mimeString });
|
||||
return blob;
|
||||
}
|
||||
|
||||
function convertToExifDateFormat(date: Date) {
|
||||
return `${date.getFullYear()}:${
|
||||
date.getMonth() + 1
|
||||
}:${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
getNonEmptyCollections,
|
||||
updateCollectionMagicMetadata,
|
||||
updatePublicCollectionMagicMetadata,
|
||||
} from 'services/collectionService';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
@ -8,25 +7,16 @@ import { logError } from '@ente/shared/sentry';
|
|||
import {
|
||||
COLLECTION_ROLE,
|
||||
Collection,
|
||||
CollectionMagicMetadataProps,
|
||||
CollectionPublicMagicMetadataProps,
|
||||
CollectionSummaries,
|
||||
} from 'types/collection';
|
||||
import {
|
||||
CollectionSummaryType,
|
||||
CollectionType,
|
||||
HIDE_FROM_COLLECTION_BAR_TYPES,
|
||||
OPTIONS_NOT_HAVING_COLLECTION_TYPES,
|
||||
SYSTEM_COLLECTION_TYPES,
|
||||
MOVE_TO_NOT_ALLOWED_COLLECTION,
|
||||
ADD_TO_NOT_ALLOWED_COLLECTION,
|
||||
} from 'constants/collection';
|
||||
import { getUnixTimeInMicroSecondsWithDelta } from 'utils/time';
|
||||
import { SUB_TYPE, VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import { isArchivedCollection, updateMagicMetadata } from 'utils/magicMetadata';
|
||||
import bs58 from 'bs58';
|
||||
import { t } from 'i18next';
|
||||
import { getAlbumsURL } from '@ente/shared/network/api';
|
||||
import { updateMagicMetadata } from 'utils/magicMetadata';
|
||||
import { User } from '@ente/shared/user/types';
|
||||
import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
|
||||
|
||||
|
@ -45,59 +35,6 @@ export function getSelectedCollection(
|
|||
return collections.find((collection) => collection.id === collectionID);
|
||||
}
|
||||
|
||||
export function appendCollectionKeyToShareURL(
|
||||
url: string,
|
||||
collectionKey: string
|
||||
) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sharableURL = new URL(url);
|
||||
const albumsURL = new URL(getAlbumsURL());
|
||||
|
||||
sharableURL.protocol = albumsURL.protocol;
|
||||
sharableURL.host = albumsURL.host;
|
||||
sharableURL.pathname = albumsURL.pathname;
|
||||
|
||||
const bytes = Buffer.from(collectionKey, 'base64');
|
||||
sharableURL.hash = bs58.encode(bytes);
|
||||
return sharableURL.href;
|
||||
}
|
||||
|
||||
const _intSelectOption = (i: number) => {
|
||||
const label = i === 0 ? t('NO_DEVICE_LIMIT') : i.toString();
|
||||
return { label, value: i };
|
||||
};
|
||||
|
||||
export function getDeviceLimitOptions() {
|
||||
return [0, 2, 5, 10, 25, 50].map((i) => _intSelectOption(i));
|
||||
}
|
||||
|
||||
export const shareExpiryOptions = () => [
|
||||
{ label: t('NEVER'), value: () => 0 },
|
||||
{
|
||||
label: t('AFTER_TIME.HOUR'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ hours: 1 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.DAY'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ days: 1 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.WEEK'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ days: 7 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.MONTH'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ months: 1 }),
|
||||
},
|
||||
{
|
||||
label: t('AFTER_TIME.YEAR'),
|
||||
value: () => getUnixTimeInMicroSecondsWithDelta({ years: 1 }),
|
||||
},
|
||||
];
|
||||
|
||||
export const changeCollectionSortOrder = async (
|
||||
collection: Collection,
|
||||
asc: boolean
|
||||
|
@ -123,85 +60,6 @@ export const changeCollectionSortOrder = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const changeCollectionOrder = async (
|
||||
collection: Collection,
|
||||
order: number
|
||||
) => {
|
||||
try {
|
||||
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||
order,
|
||||
};
|
||||
|
||||
const updatedMagicMetadata = await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
collection.magicMetadata,
|
||||
collection.key
|
||||
);
|
||||
|
||||
await updateCollectionMagicMetadata(collection, updatedMagicMetadata);
|
||||
} catch (e) {
|
||||
logError(e, 'change collection order failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const changeCollectionSubType = async (
|
||||
collection: Collection,
|
||||
subType: SUB_TYPE
|
||||
) => {
|
||||
try {
|
||||
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||
subType: subType,
|
||||
};
|
||||
|
||||
const updatedMagicMetadata = await updateMagicMetadata(
|
||||
updatedMagicMetadataProps,
|
||||
collection.magicMetadata,
|
||||
collection.key
|
||||
);
|
||||
await updateCollectionMagicMetadata(collection, updatedMagicMetadata);
|
||||
} catch (e) {
|
||||
logError(e, 'change collection subType failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getArchivedCollections = (collections: Collection[]) => {
|
||||
return new Set<number>(
|
||||
collections
|
||||
.filter(isArchivedCollection)
|
||||
.map((collection) => collection.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const getDefaultHiddenCollectionIDs = (collections: Collection[]) => {
|
||||
return new Set<number>(
|
||||
collections
|
||||
.filter(isDefaultHiddenCollection)
|
||||
.map((collection) => collection.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const hasNonSystemCollections = (
|
||||
collectionSummaries: CollectionSummaries
|
||||
) => {
|
||||
for (const collectionSummary of collectionSummaries.values()) {
|
||||
if (!isSystemCollection(collectionSummary.type)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isMoveToAllowedCollection = (type: CollectionSummaryType) => {
|
||||
return !MOVE_TO_NOT_ALLOWED_COLLECTION.has(type);
|
||||
};
|
||||
|
||||
export const isAddToAllowedCollection = (type: CollectionSummaryType) => {
|
||||
return !ADD_TO_NOT_ALLOWED_COLLECTION.has(type);
|
||||
};
|
||||
|
||||
export const isSystemCollection = (type: CollectionSummaryType) => {
|
||||
return SYSTEM_COLLECTION_TYPES.has(type);
|
||||
};
|
||||
|
||||
export const shouldShowOptions = (type: CollectionSummaryType) => {
|
||||
return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type);
|
||||
};
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import { NULL_LOCATION } from 'constants/upload';
|
||||
import { ParsedExtractedMetadata } from 'types/upload';
|
||||
import { validateAndGetCreationUnixTimeInMicroSeconds } from 'utils/time';
|
||||
|
||||
enum MetadataTags {
|
||||
CREATION_TIME = 'creation_time',
|
||||
APPLE_CONTENT_IDENTIFIER = 'com.apple.quicktime.content.identifier',
|
||||
APPLE_LIVE_PHOTO_IDENTIFIER = 'com.apple.quicktime.live-photo.auto',
|
||||
APPLE_CREATION_DATE = 'com.apple.quicktime.creationdate',
|
||||
APPLE_LOCATION_ISO = 'com.apple.quicktime.location.ISO6709',
|
||||
LOCATION = 'location',
|
||||
}
|
||||
|
||||
export function parseFFmpegExtractedMetadata(encodedMetadata: Uint8Array) {
|
||||
const metadataString = new TextDecoder().decode(encodedMetadata);
|
||||
const metadataPropertyArray = metadataString.split('\n');
|
||||
const metadataKeyValueArray = metadataPropertyArray.map((property) =>
|
||||
property.split('=')
|
||||
);
|
||||
const validKeyValuePairs = metadataKeyValueArray.filter(
|
||||
(keyValueArray) => keyValueArray.length === 2
|
||||
) as Array<[string, string]>;
|
||||
|
||||
const metadataMap = Object.fromEntries(validKeyValuePairs);
|
||||
|
||||
const location = parseAppleISOLocation(
|
||||
metadataMap[MetadataTags.APPLE_LOCATION_ISO] ??
|
||||
metadataMap[MetadataTags.LOCATION]
|
||||
);
|
||||
|
||||
const creationTime = parseCreationTime(
|
||||
metadataMap[MetadataTags.APPLE_CREATION_DATE] ??
|
||||
metadataMap[MetadataTags.CREATION_TIME]
|
||||
);
|
||||
const parsedMetadata: ParsedExtractedMetadata = {
|
||||
creationTime,
|
||||
location: {
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
},
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
return parsedMetadata;
|
||||
}
|
||||
|
||||
function parseAppleISOLocation(isoLocation: string) {
|
||||
let location = NULL_LOCATION;
|
||||
if (isoLocation) {
|
||||
const [latitude, longitude] = isoLocation
|
||||
.match(/(\+|-)\d+\.*\d+/g)
|
||||
.map((x) => parseFloat(x));
|
||||
|
||||
location = { latitude, longitude };
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
function parseCreationTime(creationTime: string) {
|
||||
let dateTime = null;
|
||||
if (creationTime) {
|
||||
dateTime = validateAndGetCreationUnixTimeInMicroSeconds(
|
||||
new Date(creationTime)
|
||||
);
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
export function splitFilenameAndExtension(filename: string): [string, string] {
|
||||
const lastDotPosition = filename.lastIndexOf('.');
|
||||
if (lastDotPosition === -1) return [filename, null];
|
||||
else
|
||||
return [
|
||||
filename.slice(0, lastDotPosition),
|
||||
filename.slice(lastDotPosition + 1),
|
||||
];
|
||||
}
|
|
@ -10,7 +10,6 @@ import {
|
|||
import { decodeLivePhoto } from 'services/livePhotoService';
|
||||
import { getFileType } from 'services/typeDetectionService';
|
||||
import { logError } from '@ente/shared/sentry';
|
||||
import { updateFileCreationDateInEXIF } from 'services/upload/exifService';
|
||||
import {
|
||||
TYPE_JPEG,
|
||||
TYPE_JPG,
|
||||
|
@ -28,15 +27,10 @@ import { isArchivedFile, updateMagicMetadata } from 'utils/magicMetadata';
|
|||
|
||||
import { CustomError } from '@ente/shared/error';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import {
|
||||
deleteFromTrash,
|
||||
trashFiles,
|
||||
updateFileMagicMetadata,
|
||||
} from 'services/fileService';
|
||||
import { updateFileMagicMetadata } from 'services/fileService';
|
||||
import isElectron from 'is-electron';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
import { FileTypeInfo } from 'types/upload';
|
||||
import { moveToHiddenCollection } from 'services/collectionService';
|
||||
import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
|
||||
import { User } from '@ente/shared/user/types';
|
||||
import { addLogLine, addLocalLog } from '@ente/shared/logging';
|
||||
|
@ -85,75 +79,6 @@ export async function getUpdatedEXIFFileForDownload(
|
|||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(
|
||||
file: EnteFile,
|
||||
token?: string,
|
||||
passwordToken?: string
|
||||
) {
|
||||
try {
|
||||
let fileBlob: Blob;
|
||||
const fileReader = new FileReader();
|
||||
const fileURL = await CastDownloadManager.getCachedOriginalFile(
|
||||
file
|
||||
)[0];
|
||||
if (!fileURL) {
|
||||
fileBlob = await new Response(
|
||||
await CastDownloadManager.downloadFile(
|
||||
token,
|
||||
passwordToken,
|
||||
file
|
||||
)
|
||||
).blob();
|
||||
} else {
|
||||
fileBlob = await (await fetch(fileURL)).blob();
|
||||
}
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const image = new File([livePhoto.image], livePhoto.imageNameTitle);
|
||||
const imageType = await getFileType(image);
|
||||
const tempImageURL = URL.createObjectURL(
|
||||
new Blob([livePhoto.image], { type: imageType.mimeType })
|
||||
);
|
||||
const video = new File([livePhoto.video], livePhoto.videoNameTitle);
|
||||
const videoType = await getFileType(video);
|
||||
const tempVideoURL = URL.createObjectURL(
|
||||
new Blob([livePhoto.video], { type: videoType.mimeType })
|
||||
);
|
||||
downloadUsingAnchor(tempImageURL, livePhoto.imageNameTitle);
|
||||
downloadUsingAnchor(tempVideoURL, livePhoto.videoNameTitle);
|
||||
} else {
|
||||
const fileType = await getFileType(
|
||||
new File([fileBlob], file.metadata.title)
|
||||
);
|
||||
fileBlob = await new Response(
|
||||
await getUpdatedEXIFFileForDownload(
|
||||
fileReader,
|
||||
file,
|
||||
fileBlob.stream()
|
||||
)
|
||||
).blob();
|
||||
fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
|
||||
const tempURL = URL.createObjectURL(fileBlob);
|
||||
downloadUsingAnchor(tempURL, file.metadata.title);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to download file');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadUsingAnchor(link: string, name: string) {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = link;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(link);
|
||||
a.remove();
|
||||
}
|
||||
|
||||
export function groupFilesBasedOnCollectionID(files: EnteFile[]) {
|
||||
const collectionWiseFiles = new Map<number, EnteFile[]>();
|
||||
for (const file of files) {
|
||||
|
@ -544,106 +469,6 @@ export function getUniqueFiles(files: EnteFile[]) {
|
|||
return uniqueFiles;
|
||||
}
|
||||
|
||||
export async function downloadFiles(
|
||||
files: EnteFile[],
|
||||
progressBarUpdater?: {
|
||||
increaseSuccess: () => void;
|
||||
increaseFailed: () => void;
|
||||
isCancelled: () => boolean;
|
||||
}
|
||||
) {
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (progressBarUpdater?.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
await downloadFile(file, false);
|
||||
progressBarUpdater?.increaseSuccess();
|
||||
} catch (e) {
|
||||
logError(e, 'download fail for file');
|
||||
progressBarUpdater?.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFilesDesktop(
|
||||
files: EnteFile[],
|
||||
progressBarUpdater: {
|
||||
increaseSuccess: () => void;
|
||||
increaseFailed: () => void;
|
||||
isCancelled: () => boolean;
|
||||
},
|
||||
downloadPath: string
|
||||
) {
|
||||
const fileReader = new FileReader();
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (progressBarUpdater?.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
await downloadFileDesktop(fileReader, file, downloadPath);
|
||||
progressBarUpdater?.increaseSuccess();
|
||||
} catch (e) {
|
||||
logError(e, 'download fail for file');
|
||||
progressBarUpdater?.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFileDesktop(
|
||||
fileReader: FileReader,
|
||||
file: EnteFile,
|
||||
downloadPath: string
|
||||
) {
|
||||
console.log(fileReader, file, downloadPath);
|
||||
// let fileStream: ReadableStream<Uint8Array>;
|
||||
// const fileURL = await DownloadManager.getCachedOriginalFile(file)[0];
|
||||
// if (!fileURL) {
|
||||
// fileStream = await DownloadManager.downloadFile(file);
|
||||
// } else {
|
||||
// fileStream = await fetch(fileURL).then((res) => res.body);
|
||||
// }
|
||||
|
||||
// if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
// const fileBlob = await new Response(updatedFileStream).blob();
|
||||
// const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
// const imageExportName = getUniqueFileExportName(
|
||||
// downloadPath,
|
||||
// livePhoto.imageNameTitle
|
||||
// );
|
||||
// const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
|
||||
// await ElectronFSService.saveMediaFile(
|
||||
// getFileExportPath(downloadPath, imageExportName),
|
||||
// imageStream
|
||||
// );
|
||||
// try {
|
||||
// const videoExportName = getUniqueFileExportName(
|
||||
// downloadPath,
|
||||
// livePhoto.videoNameTitle
|
||||
// );
|
||||
// const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
|
||||
// await ElectronFSService.saveMediaFile(
|
||||
// getFileExportPath(downloadPath, videoExportName),
|
||||
// videoStream
|
||||
// );
|
||||
// } catch (e) {
|
||||
// ElectronFSService.deleteFile(
|
||||
// getFileExportPath(downloadPath, imageExportName)
|
||||
// );
|
||||
// throw e;
|
||||
// }
|
||||
// } else {
|
||||
// const fileExportName = getUniqueFileExportName(
|
||||
// downloadPath,
|
||||
// file.metadata.title
|
||||
// );
|
||||
// await ElectronFSService.saveMediaFile(
|
||||
// getFileExportPath(downloadPath, fileExportName),
|
||||
// updatedFileStream
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
export const isImageOrVideo = (fileType: FILE_TYPE) =>
|
||||
[FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType);
|
||||
|
||||
|
@ -764,71 +589,6 @@ export const shouldShowAvatar = (file: EnteFile, user: User) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const handleFileOps = async (
|
||||
ops: FILE_OPS_TYPE,
|
||||
files: EnteFile[],
|
||||
setDeletedFileIds: (
|
||||
deletedFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void,
|
||||
setHiddenFileIds: (
|
||||
hiddenFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void,
|
||||
setFixCreationTimeAttributes: (
|
||||
fixCreationTimeAttributes:
|
||||
| {
|
||||
files: EnteFile[];
|
||||
}
|
||||
| ((prev: { files: EnteFile[] }) => { files: EnteFile[] })
|
||||
) => void
|
||||
) => {
|
||||
switch (ops) {
|
||||
case FILE_OPS_TYPE.TRASH:
|
||||
await deleteFileHelper(files, false, setDeletedFileIds);
|
||||
break;
|
||||
case FILE_OPS_TYPE.DELETE_PERMANENTLY:
|
||||
await deleteFileHelper(files, true, setDeletedFileIds);
|
||||
break;
|
||||
case FILE_OPS_TYPE.HIDE:
|
||||
await hideFilesHelper(files, setHiddenFileIds);
|
||||
break;
|
||||
case FILE_OPS_TYPE.DOWNLOAD:
|
||||
await downloadFiles(files);
|
||||
break;
|
||||
case FILE_OPS_TYPE.FIX_TIME:
|
||||
fixTimeHelper(files, setFixCreationTimeAttributes);
|
||||
break;
|
||||
case FILE_OPS_TYPE.ARCHIVE:
|
||||
await changeFilesVisibility(files, VISIBILITY_STATE.ARCHIVED);
|
||||
break;
|
||||
case FILE_OPS_TYPE.UNARCHIVE:
|
||||
await changeFilesVisibility(files, VISIBILITY_STATE.VISIBLE);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFileHelper = async (
|
||||
selectedFiles: EnteFile[],
|
||||
permanent: boolean,
|
||||
setDeletedFileIds: (
|
||||
deletedFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void
|
||||
) => {
|
||||
try {
|
||||
setDeletedFileIds((deletedFileIds) => {
|
||||
selectedFiles.forEach((file) => deletedFileIds.add(file.id));
|
||||
return new Set(deletedFileIds);
|
||||
});
|
||||
if (permanent) {
|
||||
await deleteFromTrash(selectedFiles.map((file) => file.id));
|
||||
} else {
|
||||
await trashFiles(selectedFiles);
|
||||
}
|
||||
} catch (e) {
|
||||
setDeletedFileIds(new Set());
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFileAsBlob = async (
|
||||
file: EnteFile,
|
||||
castToken: string
|
||||
|
@ -855,33 +615,6 @@ export const downloadFileAsBlob = async (
|
|||
}
|
||||
};
|
||||
|
||||
const hideFilesHelper = async (
|
||||
selectedFiles: EnteFile[],
|
||||
setHiddenFileIds: (
|
||||
hiddenFileIds: Set<number> | ((prev: Set<number>) => Set<number>)
|
||||
) => void
|
||||
) => {
|
||||
try {
|
||||
setHiddenFileIds((hiddenFileIds) => {
|
||||
selectedFiles.forEach((file) => hiddenFileIds.add(file.id));
|
||||
return new Set(hiddenFileIds);
|
||||
});
|
||||
await moveToHiddenCollection(selectedFiles);
|
||||
} catch (e) {
|
||||
setHiddenFileIds(new Set());
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const fixTimeHelper = async (
|
||||
selectedFiles: EnteFile[],
|
||||
setFixCreationTimeAttributes: (fixCreationTimeAttributes: {
|
||||
files: EnteFile[];
|
||||
}) => void
|
||||
) => {
|
||||
setFixCreationTimeAttributes({ files: selectedFiles });
|
||||
};
|
||||
|
||||
const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => {
|
||||
const originalURL = URL.createObjectURL(originalBlob);
|
||||
const convertedURL = convertedBlob
|
||||
|
|
|
@ -14,23 +14,6 @@ interface DateComponent<T = number> {
|
|||
second: T;
|
||||
}
|
||||
|
||||
export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number {
|
||||
let currentDate = new Date();
|
||||
if (delta?.hours) {
|
||||
currentDate = _addHours(currentDate, delta.hours);
|
||||
}
|
||||
if (delta?.days) {
|
||||
currentDate = _addDays(currentDate, delta.days);
|
||||
}
|
||||
if (delta?.months) {
|
||||
currentDate = _addMonth(currentDate, delta.months);
|
||||
}
|
||||
if (delta?.years) {
|
||||
currentDate = _addYears(currentDate, delta.years);
|
||||
}
|
||||
return currentDate.getTime() * 1000;
|
||||
}
|
||||
|
||||
export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) {
|
||||
if (!dateTime || isNaN(dateTime.getTime())) {
|
||||
return null;
|
||||
|
@ -46,30 +29,6 @@ export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) {
|
|||
}
|
||||
}
|
||||
|
||||
function _addDays(date: Date, days: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setDate(date.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
function _addHours(date: Date, hours: number): Date {
|
||||
const result = new Date(date);
|
||||
result.setHours(date.getHours() + hours);
|
||||
return result;
|
||||
}
|
||||
|
||||
function _addMonth(date: Date, months: number) {
|
||||
const result = new Date(date);
|
||||
result.setMonth(date.getMonth() + months);
|
||||
return result;
|
||||
}
|
||||
|
||||
function _addYears(date: Date, years: number) {
|
||||
const result = new Date(date);
|
||||
result.setFullYear(date.getFullYear() + years);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
generates data component for date in format YYYYMMDD-HHMMSS
|
||||
*/
|
||||
|
|
Loading…
Add table
Reference in a new issue