ente/src/services/downloadManager.ts

181 lines
6.9 KiB
TypeScript
Raw Normal View History

2021-05-30 16:56:48 +00:00
import { getToken } from 'utils/common/key';
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
2021-04-03 04:36:15 +00:00
import CryptoWorker from 'utils/crypto';
2021-08-11 15:59:59 +00:00
import {
fileIsHEIC,
convertHEIC2JPEG,
fileNameWithoutExtension,
2021-08-12 05:26:17 +00:00
generateStreamFromArrayBuffer,
2021-08-11 15:59:59 +00:00
} from 'utils/file';
2021-05-29 06:27:52 +00:00
import HTTPService from './HTTPService';
2021-08-13 03:19:48 +00:00
import { File, FILE_TYPE } from './fileService';
2021-06-12 17:14:21 +00:00
import { logError } from 'utils/sentry';
2021-08-11 15:59:59 +00:00
import { decodeMotionPhoto } from './motionPhotoService';
class DownloadManager {
private fileDownloads = new Map<string, string>();
2021-05-29 06:27:52 +00:00
private thumbnailDownloads = new Map<number, string>();
public async getPreview(file: File) {
try {
const token = getToken();
if (!token) {
return null;
}
const cache = await caches.open('thumbs');
const cacheResp: Response = await cache.match(file.id.toString());
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
if (!this.thumbnailDownloads.get(file.id)) {
const download = await this.downloadThumb(token, cache, file);
this.thumbnailDownloads.set(file.id, download);
}
return await this.thumbnailDownloads.get(file.id);
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'get preview Failed');
}
}
downloadThumb = async (token: string, cache: Cache, file: File) => {
const resp = await HTTPService.get(
getThumbnailUrl(file.id),
null,
{ 'X-Auth-Token': token },
2021-08-13 02:38:38 +00:00
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
2021-08-13 02:38:38 +00:00
file.key
);
try {
await cache.put(
file.id.toString(),
2021-08-13 02:38:38 +00:00
new Response(new Blob([decrypted]))
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([decrypted]));
2021-08-11 15:38:14 +00:00
};
2021-08-11 15:38:14 +00:00
getFile = async (file: File, forPreview = false) => {
2021-03-14 13:20:00 +00:00
try {
if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) {
// unzip motion photo and return fileBlob of the image for preview
2021-08-11 15:59:59 +00:00
const fileStream = await this.downloadFile(file);
let fileBlob = await new Response(fileStream).blob();
if (forPreview) {
2021-08-11 15:59:59 +00:00
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const originalName = fileNameWithoutExtension(
2021-08-13 02:38:38 +00:00
file.metadata.title
2021-08-11 15:59:59 +00:00
);
const motionPhoto = await decodeMotionPhoto(
fileBlob,
2021-08-13 02:38:38 +00:00
originalName
2021-08-11 15:59:59 +00:00
);
2021-08-12 06:39:35 +00:00
fileBlob = new Blob([motionPhoto.image]);
}
if (fileIsHEIC(file.metadata.title)) {
fileBlob = await convertHEIC2JPEG(fileBlob);
2021-08-11 15:59:59 +00:00
}
}
2021-08-11 15:38:14 +00:00
this.fileDownloads.set(
`${file.id}_${forPreview}`,
2021-08-13 02:38:38 +00:00
URL.createObjectURL(fileBlob)
2021-08-11 15:38:14 +00:00
);
2021-03-14 13:20:00 +00:00
}
return this.fileDownloads.get(`${file.id}_${forPreview}`);
2021-03-14 13:20:00 +00:00
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'Failed to get File');
}
};
2021-02-23 10:11:46 +00:00
async downloadFile(file: File) {
const worker = await new CryptoWorker();
const token = getToken();
if (!token) {
return null;
}
2021-08-11 15:38:14 +00:00
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
) {
const resp = await HTTPService.get(
getFileUrl(file.id),
null,
2021-05-30 16:56:48 +00:00
{ 'X-Auth-Token': token },
2021-08-13 02:38:38 +00:00
{ responseType: 'arraybuffer' }
);
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
2021-08-13 02:38:38 +00:00
file.key
);
2021-08-12 05:26:17 +00:00
return generateStreamFromArrayBuffer(decrypted);
2021-05-29 06:27:52 +00:00
}
const resp = await fetch(getFileUrl(file.id), {
headers: {
'X-Auth-Token': token,
},
});
const reader = resp.body.getReader();
const stream = new ReadableStream({
async start(controller) {
const decryptionHeader = await worker.fromB64(
2021-08-13 02:38:38 +00:00
file.file.decryptionHeader
2021-05-29 06:27:52 +00:00
);
const fileKey = await worker.fromB64(file.key);
2021-08-11 15:38:14 +00:00
const { pullState, decryptionChunkSize } =
await worker.initDecryption(decryptionHeader, fileKey);
2021-05-29 06:27:52 +00:00
let data = new Uint8Array();
// The following function handles each data chunk
function push() {
// "done" is a Boolean and value a "Uint8Array"
2021-05-30 16:56:48 +00:00
reader.read().then(async ({ done, value }) => {
2021-05-29 06:27:52 +00:00
// Is there more data to read?
if (!done) {
const buffer = new Uint8Array(
2021-08-13 02:38:38 +00:00
data.byteLength + value.byteLength
2021-05-29 06:27:52 +00:00
);
buffer.set(new Uint8Array(data), 0);
2021-08-11 15:38:14 +00:00
buffer.set(new Uint8Array(value), data.byteLength);
2021-05-29 06:27:52 +00:00
if (buffer.length > decryptionChunkSize) {
const fileData = buffer.slice(
0,
2021-08-13 02:38:38 +00:00
decryptionChunkSize
);
2021-08-11 15:38:14 +00:00
const { decryptedData } =
await worker.decryptChunk(
fileData,
2021-08-13 02:38:38 +00:00
pullState
2021-08-11 15:38:14 +00:00
);
2021-05-29 06:27:52 +00:00
controller.enqueue(decryptedData);
data = buffer.slice(decryptionChunkSize);
} else {
2021-05-29 06:27:52 +00:00
data = buffer;
}
2021-05-29 06:27:52 +00:00
push();
} else {
if (data) {
2021-08-11 15:38:14 +00:00
const { decryptedData } =
await worker.decryptChunk(data, pullState);
2021-05-29 06:27:52 +00:00
controller.enqueue(decryptedData);
data = null;
}
controller.close();
}
});
}
2021-05-29 06:27:52 +00:00
push();
},
});
return stream;
}
}
export default new DownloadManager();