This commit is contained in:
Manav Rathi 2024-05-04 10:01:55 +05:30
parent 8bcbdeb6e2
commit 949dd22f81
No known key found for this signature in database
5 changed files with 228 additions and 243 deletions

View file

@ -3,8 +3,8 @@ import EnteSpinner from "@ente/shared/components/EnteSpinner";
import LargeType from "components/LargeType";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { storeCastData } from "services/cast";
import { advertiseCode, getCastData, register } from "services/pair";
import { storeCastData } from "services/cast/castService";
import { useCastReceiver } from "../utils/useCastReceiver";
export default function PairingMode() {

View file

@ -9,11 +9,11 @@ import { useEffect, useState } from "react";
import {
getCastCollection,
getLocalFiles,
getPreviewableImage,
syncPublicFiles,
} from "services/cast/castService";
} from "services/cast";
import { Collection } from "types/collection";
import { EnteFile } from "types/file";
import { getPreviewableImage } from "utils/file";
const renderableFileURLCache = new Map<number, string>();

View file

@ -1,17 +1,25 @@
import { FILE_TYPE } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import log from "@/next/log";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { getEndpoint } from "@ente/shared/network/api";
import { getCastFileURL, getEndpoint } from "@ente/shared/network/api";
import localForage from "@ente/shared/storage/localForage";
import { detectMediaMIMEType } from "services/detect-type";
import { Collection, CollectionPublicMagicMetadata } from "types/collection";
import { EncryptedEnteFile, EnteFile } from "types/file";
import { decryptFile, mergeMetadata, sortFiles } from "utils/file";
import {
EncryptedEnteFile,
EnteFile,
FileMagicMetadata,
FilePublicMagicMetadata,
} from "types/file";
export interface SavedCollectionFiles {
collectionLocalID: string;
files: EnteFile[];
}
const ENDPOINT = getEndpoint();
const COLLECTION_FILES_TABLE = "collection-files";
const COLLECTIONS_TABLE = "collections";
@ -302,3 +310,217 @@ export const storeCastData = (payloadObj: Object) => {
window.localStorage.setItem(key, payloadObj[key]);
}
};
export function sortFiles(files: EnteFile[], sortAsc = false) {
// sort based on the time of creation time of the file,
// for files with same creation time, sort based on the time of last modification
const factor = sortAsc ? -1 : 1;
return files.sort((a, b) => {
if (a.metadata.creationTime === b.metadata.creationTime) {
return (
factor *
(b.metadata.modificationTime - a.metadata.modificationTime)
);
}
return factor * (b.metadata.creationTime - a.metadata.creationTime);
});
}
export async function decryptFile(
file: EncryptedEnteFile,
collectionKey: string,
): Promise<EnteFile> {
try {
const worker = await ComlinkCryptoWorker.getInstance();
const {
encryptedKey,
keyDecryptionNonce,
metadata,
magicMetadata,
pubMagicMetadata,
...restFileProps
} = file;
const fileKey = await worker.decryptB64(
encryptedKey,
keyDecryptionNonce,
collectionKey,
);
const fileMetadata = await worker.decryptMetadata(
metadata.encryptedData,
metadata.decryptionHeader,
fileKey,
);
let fileMagicMetadata: FileMagicMetadata;
let filePubMagicMetadata: FilePublicMagicMetadata;
if (magicMetadata?.data) {
fileMagicMetadata = {
...file.magicMetadata,
data: await worker.decryptMetadata(
magicMetadata.data,
magicMetadata.header,
fileKey,
),
};
}
if (pubMagicMetadata?.data) {
filePubMagicMetadata = {
...pubMagicMetadata,
data: await worker.decryptMetadata(
pubMagicMetadata.data,
pubMagicMetadata.header,
fileKey,
),
};
}
return {
...restFileProps,
key: fileKey,
metadata: fileMetadata,
magicMetadata: fileMagicMetadata,
pubMagicMetadata: filePubMagicMetadata,
};
} catch (e) {
log.error("file decryption failed", e);
throw e;
}
}
export function generateStreamFromArrayBuffer(data: Uint8Array) {
return new ReadableStream({
async start(controller: ReadableStreamDefaultController) {
controller.enqueue(data);
controller.close();
},
});
}
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
return files.map((file) => {
if (file.pubMagicMetadata?.data.editedTime) {
file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
}
if (file.pubMagicMetadata?.data.editedName) {
file.metadata.title = file.pubMagicMetadata.data.editedName;
}
return file;
});
}
export const getPreviewableImage = async (
file: EnteFile,
castToken: string,
): Promise<Blob> => {
try {
let fileBlob = await new Response(
await downloadFile(castToken, file),
).blob();
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const { imageData } = await decodeLivePhoto(
file.metadata.title,
fileBlob,
);
fileBlob = new Blob([imageData]);
}
const mimeType = await detectMediaMIMEType(
new File([fileBlob], file.metadata.title),
);
if (!mimeType) return undefined;
fileBlob = new Blob([fileBlob], { type: mimeType });
return fileBlob;
} catch (e) {
log.error("failed to download file", e);
}
};
const downloadFile = async (castToken: string, file: EnteFile) => {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
) {
const resp = await HTTPService.get(
getCastFileURL(file.id),
null,
{
"X-Cast-Access-Token": castToken,
},
{ responseType: "arraybuffer" },
);
if (typeof resp.data === "undefined") {
throw Error(CustomError.REQUEST_FAILED);
}
const decrypted = await cryptoWorker.decryptFile(
new Uint8Array(resp.data),
await cryptoWorker.fromB64(file.file.decryptionHeader),
file.key,
);
return generateStreamFromArrayBuffer(decrypted);
}
const resp = await fetch(getCastFileURL(file.id), {
headers: {
"X-Cast-Access-Token": castToken,
},
});
const reader = resp.body.getReader();
const stream = new ReadableStream({
async start(controller) {
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
function push() {
// "done" is a Boolean and value a "Uint8Array"
reader.read().then(async ({ done, value }) => {
// Is there more data to read?
if (!done) {
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,
);
const { decryptedData } =
await cryptoWorker.decryptFileChunk(
fileData,
pullState,
);
controller.enqueue(decryptedData);
data = buffer.slice(decryptionChunkSize);
} else {
data = buffer;
}
push();
} else {
if (data) {
const { decryptedData } =
await cryptoWorker.decryptFileChunk(
data,
pullState,
);
controller.enqueue(decryptedData);
data = null;
}
controller.close();
}
});
}
push();
},
});
return stream;
};

View file

@ -1,103 +0,0 @@
import { FILE_TYPE } from "@/media/file-type";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { getCastFileURL } from "@ente/shared/network/api";
import { EnteFile } from "types/file";
import { generateStreamFromArrayBuffer } from "utils/file";
class CastDownloadManager {
async downloadFile(castToken: string, file: EnteFile) {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
) {
const resp = await HTTPService.get(
getCastFileURL(file.id),
null,
{
"X-Cast-Access-Token": castToken,
},
{ responseType: "arraybuffer" },
);
if (typeof resp.data === "undefined") {
throw Error(CustomError.REQUEST_FAILED);
}
const decrypted = await cryptoWorker.decryptFile(
new Uint8Array(resp.data),
await cryptoWorker.fromB64(file.file.decryptionHeader),
file.key,
);
return generateStreamFromArrayBuffer(decrypted);
}
const resp = await fetch(getCastFileURL(file.id), {
headers: {
"X-Cast-Access-Token": castToken,
},
});
const reader = resp.body.getReader();
const stream = new ReadableStream({
async start(controller) {
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
function push() {
// "done" is a Boolean and value a "Uint8Array"
reader.read().then(async ({ done, value }) => {
// Is there more data to read?
if (!done) {
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,
);
const { decryptedData } =
await cryptoWorker.decryptFileChunk(
fileData,
pullState,
);
controller.enqueue(decryptedData);
data = buffer.slice(decryptionChunkSize);
} else {
data = buffer;
}
push();
} else {
if (data) {
const { decryptedData } =
await cryptoWorker.decryptFileChunk(
data,
pullState,
);
controller.enqueue(decryptedData);
data = null;
}
controller.close();
}
});
}
push();
},
});
return stream;
}
}
export default new CastDownloadManager();

View file

@ -1,134 +0,0 @@
import { FILE_TYPE } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import log from "@/next/log";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import CastDownloadManager from "services/castDownloadManager";
import { detectMediaMIMEType } from "services/detect-type";
import {
EncryptedEnteFile,
EnteFile,
FileMagicMetadata,
FilePublicMagicMetadata,
} from "types/file";
export function sortFiles(files: EnteFile[], sortAsc = false) {
// sort based on the time of creation time of the file,
// for files with same creation time, sort based on the time of last modification
const factor = sortAsc ? -1 : 1;
return files.sort((a, b) => {
if (a.metadata.creationTime === b.metadata.creationTime) {
return (
factor *
(b.metadata.modificationTime - a.metadata.modificationTime)
);
}
return factor * (b.metadata.creationTime - a.metadata.creationTime);
});
}
export async function decryptFile(
file: EncryptedEnteFile,
collectionKey: string,
): Promise<EnteFile> {
try {
const worker = await ComlinkCryptoWorker.getInstance();
const {
encryptedKey,
keyDecryptionNonce,
metadata,
magicMetadata,
pubMagicMetadata,
...restFileProps
} = file;
const fileKey = await worker.decryptB64(
encryptedKey,
keyDecryptionNonce,
collectionKey,
);
const fileMetadata = await worker.decryptMetadata(
metadata.encryptedData,
metadata.decryptionHeader,
fileKey,
);
let fileMagicMetadata: FileMagicMetadata;
let filePubMagicMetadata: FilePublicMagicMetadata;
if (magicMetadata?.data) {
fileMagicMetadata = {
...file.magicMetadata,
data: await worker.decryptMetadata(
magicMetadata.data,
magicMetadata.header,
fileKey,
),
};
}
if (pubMagicMetadata?.data) {
filePubMagicMetadata = {
...pubMagicMetadata,
data: await worker.decryptMetadata(
pubMagicMetadata.data,
pubMagicMetadata.header,
fileKey,
),
};
}
return {
...restFileProps,
key: fileKey,
metadata: fileMetadata,
magicMetadata: fileMagicMetadata,
pubMagicMetadata: filePubMagicMetadata,
};
} catch (e) {
log.error("file decryption failed", e);
throw e;
}
}
export function generateStreamFromArrayBuffer(data: Uint8Array) {
return new ReadableStream({
async start(controller: ReadableStreamDefaultController) {
controller.enqueue(data);
controller.close();
},
});
}
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
return files.map((file) => {
if (file.pubMagicMetadata?.data.editedTime) {
file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
}
if (file.pubMagicMetadata?.data.editedName) {
file.metadata.title = file.pubMagicMetadata.data.editedName;
}
return file;
});
}
export const getPreviewableImage = async (
file: EnteFile,
castToken: string,
): Promise<Blob> => {
try {
let fileBlob = await new Response(
await CastDownloadManager.downloadFile(castToken, file),
).blob();
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const { imageData } = await decodeLivePhoto(
file.metadata.title,
fileBlob,
);
fileBlob = new Blob([imageData]);
}
const mimeType = await detectMediaMIMEType(
new File([fileBlob], file.metadata.title),
);
if (!mimeType) return undefined;
fileBlob = new Blob([fileBlob], { type: mimeType });
return fileBlob;
} catch (e) {
log.error("failed to download file", e);
}
};