[web] Remove dead code from cast (#1256)

This commit is contained in:
Manav Rathi 2024-03-29 22:45:05 +05:30 committed by GitHub
commit d52f873c92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 113 additions and 1613 deletions

View file

@ -0,0 +1,95 @@
import { SlideshowContext } from "pages/slideshow";
import { useContext, useEffect, useState } from "react";
export default function PhotoAuditorium({
url,
nextSlideUrl,
}: {
url: string;
nextSlideUrl: string;
}) {
const { showNextSlide } = useContext(SlideshowContext);
const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false);
const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false);
const [prerenderTime, setPrerenderTime] = useState<number | null>(null);
useEffect(() => {
let timeout: NodeJS.Timeout;
let timeout2: NodeJS.Timeout;
if (nextSlidePrerendered) {
const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0;
const delayTime = Math.max(10000 - elapsedTime, 0);
if (elapsedTime >= 10000) {
setShowPreloadedNextSlide(true);
} else {
timeout = setTimeout(() => {
setShowPreloadedNextSlide(true);
}, delayTime);
}
if (showNextSlide) {
timeout2 = setTimeout(() => {
showNextSlide();
setNextSlidePrerendered(false);
setPrerenderTime(null);
setShowPreloadedNextSlide(false);
}, delayTime);
}
}
return () => {
if (timeout) clearTimeout(timeout);
if (timeout2) clearTimeout(timeout2);
};
}, [nextSlidePrerendered, showNextSlide, prerenderTime]);
return (
<div
style={{
width: "100vw",
height: "100vh",
backgroundImage: `url(${url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundBlendMode: "multiply",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
>
<div
style={{
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backdropFilter: "blur(10px)",
}}
>
<img
src={url}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "none" : "block",
}}
/>
<img
src={nextSlideUrl}
style={{
maxWidth: "100%",
maxHeight: "100%",
display: showPreloadedNextSlide ? "block" : "none",
}}
onLoad={() => {
setNextSlidePrerendered(true);
setPrerenderTime(Date.now());
}}
/>
</div>
</div>
);
}

View file

@ -1,30 +0,0 @@
import { useEffect, useState } from "react";
export default function TimerBar({ percentage }: { percentage: number }) {
const okColor = "#75C157";
const warningColor = "#FFC000";
const lateColor = "#FF0000";
const [backgroundColor, setBackgroundColor] = useState(okColor);
useEffect(() => {
if (percentage >= 40) {
setBackgroundColor(okColor);
} else if (percentage >= 20) {
setBackgroundColor(warningColor);
} else {
setBackgroundColor(lateColor);
}
}, [percentage]);
return (
<div
style={{
width: `${percentage}%`, // Set the width based on the time left
height: "10px", // Same as the border thickness
backgroundColor, // The color of the moving border
transition: "width 1s linear", // Smooth transition for the width change
}}
/>
);
}

View file

@ -1,56 +0,0 @@
import { getAlbumsURL } from "@ente/shared/network/api";
import { runningInBrowser } from "@ente/shared/platform";
import { PAGES } from "constants/pages";
export enum APPS {
PHOTOS = "PHOTOS",
AUTH = "AUTH",
ALBUMS = "ALBUMS",
}
export const ALLOWED_APP_PAGES = new Map([
[APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
[
APPS.AUTH,
[
PAGES.ROOT,
PAGES.LOGIN,
PAGES.SIGNUP,
PAGES.VERIFY,
PAGES.CREDENTIALS,
PAGES.RECOVER,
PAGES.CHANGE_PASSWORD,
PAGES.GENERATE,
PAGES.AUTH,
PAGES.TWO_FACTOR_VERIFY,
PAGES.TWO_FACTOR_RECOVER,
],
],
]);
export const CLIENT_PACKAGE_NAMES = new Map([
[APPS.ALBUMS, "io.ente.albums.web"],
[APPS.PHOTOS, "io.ente.photos.web"],
[APPS.AUTH, "io.ente.auth.web"],
]);
export const getAppNameAndTitle = () => {
if (!runningInBrowser()) {
return {};
}
const currentURL = new URL(window.location.href);
const albumsURL = new URL(getAlbumsURL());
if (currentURL.origin === albumsURL.origin) {
return { name: APPS.ALBUMS, title: "ente Photos" };
} else {
return { name: APPS.PHOTOS, title: "ente Photos" };
}
};
export const getAppTitle = () => {
return getAppNameAndTitle().title;
};
export const getAppName = () => {
return getAppNameAndTitle().name;
};

View file

@ -1,5 +0,0 @@
export enum CACHES {
THUMBS = "thumbs",
FACE_CROPS = "face-crops",
FILES = "files",
}

View file

@ -1,14 +1,3 @@
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date();
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const MAX_CAPTION_SIZE = 5000;
export const TYPE_HEIC = "heic";
export const TYPE_HEIF = "heif";
export const TYPE_JPEG = "jpeg";
export const TYPE_JPG = "jpg";
export enum FILE_TYPE {
IMAGE,
VIDEO,
@ -29,15 +18,3 @@ export const RAW_FORMATS = [
"dng",
"tif",
];
export const SUPPORTED_RAW_FORMATS = [
"heic",
"rw2",
"tiff",
"arw",
"cr3",
"cr2",
"nef",
"psd",
"dng",
"tif",
];

View file

@ -1,15 +0,0 @@
export const GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48;
export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72;
export const IMAGE_CONTAINER_MAX_HEIGHT = 180;
export const IMAGE_CONTAINER_MAX_WIDTH = 180;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;
export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244;
export enum PLAN_PERIOD {
MONTH = "month",
YEAR = "year",
}
export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes

View file

@ -1,20 +0,0 @@
export enum PAGES {
CHANGE_EMAIL = "/change-email",
CHANGE_PASSWORD = "/change-password",
CREDENTIALS = "/credentials",
GALLERY = "/gallery",
GENERATE = "/generate",
LOGIN = "/login",
RECOVER = "/recover",
SIGNUP = "/signup",
TWO_FACTOR_SETUP = "/two-factor/setup",
TWO_FACTOR_VERIFY = "/two-factor/verify",
TWO_FACTOR_RECOVER = "/two-factor/recover",
VERIFY = "/verify",
ROOT = "/",
SHARED_ALBUMS = "/shared-albums",
// ML_DEBUG = '/ml-debug',
DEDUPLICATE = "/deduplicate",
// AUTH page is used to show (auth)enticator codes
AUTH = "/auth",
}

View file

@ -1,11 +1,5 @@
import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants";
import { FILE_TYPE } from "constants/file";
import {
FileTypeInfo,
ImportSuggestion,
Location,
ParsedExtractedMetadata,
} from "types/upload";
import { FileTypeInfo } from "types/upload";
// list of format that were missed by type-detection for some files.
export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
@ -45,98 +39,3 @@ export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [
];
export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"];
export const EXIFLESS_FORMATS = ["gif", "bmp"];
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
export const MULTIPART_PART_SIZE = 20 * 1024 * 1024;
export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE;
export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor(
MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE,
);
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
export const NULL_LOCATION: Location = { latitude: null, longitude: null };
export enum UPLOAD_STAGES {
START,
READING_GOOGLE_METADATA_FILES,
EXTRACTING_METADATA,
UPLOADING,
CANCELLING,
FINISH,
}
export enum UPLOAD_STRATEGY {
SINGLE_COLLECTION,
COLLECTION_PER_FOLDER,
}
export enum UPLOAD_RESULT {
FAILED,
ALREADY_UPLOADED,
UNSUPPORTED,
BLOCKED,
TOO_LARGE,
LARGER_THAN_AVAILABLE_STORAGE,
UPLOADED,
UPLOADED_WITH_STATIC_THUMBNAIL,
ADDED_SYMLINK,
}
export enum PICKED_UPLOAD_TYPE {
FILES = "files",
FOLDERS = "folders",
ZIPS = "zips",
}
export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
location: NULL_LOCATION,
creationTime: null,
width: null,
height: null,
};
export const A_SEC_IN_MICROSECONDS = 1e6;
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
rootFolderName: "",
hasNestedFolders: false,
hasRootLevelFileWithFolder: false,
};
export const BLACK_THUMBNAIL_BASE64 =
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" +
"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" +
"EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" +
"ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" +
"BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" +
"6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" +
"W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" +
"AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" +
"nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" +
"kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" +
"AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" +
"gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" +
"AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" +
"CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" +
"ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" +
"KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" +
"AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k=";

View file

@ -1,19 +0,0 @@
export const ENTE_WEBSITE_LINK = "https://ente.io";
export const ML_BLOG_LINK = "https://ente.io/blog/desktop-ml-beta";
export const FACE_SEARCH_PRIVACY_POLICY_LINK =
"https://ente.io/privacy#8-biometric-information-privacy-policy";
export const SUPPORT_EMAIL = "support@ente.io";
export const APP_DOWNLOAD_URL = "https://ente.io/download/desktop";
export const FEEDBACK_EMAIL = "feedback@ente.io";
export const DELETE_ACCOUNT_EMAIL = "account-deletion@ente.io";
export const WEB_ROADMAP_URL = "https://github.com/ente-io/ente/discussions";
export const DESKTOP_ROADMAP_URL =
"https://github.com/ente-io/ente/discussions";

View file

@ -1,31 +0,0 @@
export enum MS_KEYS {
SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress",
REDIRECT_URL = "redirectUrl",
}
type StoreType = Map<Partial<MS_KEYS>, any>;
class InMemoryStore {
private store: StoreType = new Map();
get(key: MS_KEYS) {
return this.store.get(key);
}
set(key: MS_KEYS, value: any) {
this.store.set(key, value);
}
delete(key: MS_KEYS) {
this.store.delete(key);
}
has(key: MS_KEYS) {
return this.store.has(key);
}
clear() {
this.store.clear();
}
}
export default new InMemoryStore();

View file

@ -1,25 +0,0 @@
import { LimitedCacheStorage } from "types/cache/index";
class cacheStorageFactory {
getCacheStorage(): LimitedCacheStorage {
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
}
}
export const CacheStorageFactory = new cacheStorageFactory();
function transformBrowserCacheStorageToLimitedCacheStorage(
caches: CacheStorage,
): LimitedCacheStorage {
return {
async open(cacheName) {
const cache = await caches.open(cacheName);
return {
match: cache.match.bind(cache),
put: cache.put.bind(cache),
delete: cache.delete.bind(cache),
};
},
delete: caches.delete.bind(caches),
};
}

View file

@ -1,33 +0,0 @@
import { logError } from "@ente/shared/sentry";
import { CacheStorageFactory } from "./cacheStorageFactory";
const SecurityError = "SecurityError";
const INSECURE_OPERATION = "The operation is insecure.";
async function openCache(cacheName: string) {
try {
return await CacheStorageFactory.getCacheStorage().open(cacheName);
} catch (e) {
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
// no-op
} else {
// log and ignore, we don't want to break the caller flow, when cache is not available
logError(e, "openCache failed");
}
}
}
async function deleteCache(cacheName: string) {
try {
return await CacheStorageFactory.getCacheStorage().delete(cacheName);
} catch (e) {
// ignoring insecure operation error, as it is thrown in incognito mode in firefox
if (e.name === SecurityError && e.message === INSECURE_OPERATION) {
// no-op
} else {
// log and ignore, we don't want to break the caller flow, when cache is not available
logError(e, "deleteCache failed");
}
}
}
export const CacheStorageService = { open: openCache, delete: deleteCache };

View file

@ -1,162 +1,14 @@
import { EnteFile } from "types/file";
import {
createTypedObjectURL,
generateStreamFromArrayBuffer,
getRenderableFileURL,
} from "utils/file";
import { CustomError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { getCastFileURL, getCastThumbnailURL } from "@ente/shared/network/api";
import { logError } from "@ente/shared/sentry";
import { CACHES } from "constants/cache";
import { getCastFileURL } from "@ente/shared/network/api";
import { FILE_TYPE } from "constants/file";
import { LimitedCache } from "types/cache";
import { EnteFile } from "types/file";
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
import { CacheStorageService } from "./cache/cacheStorageService";
import { generateStreamFromArrayBuffer } from "utils/file";
class CastDownloadManager {
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;
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
this.progressUpdater = progressUpdater;
}
private async getThumbnailCache() {
try {
const thumbnailCache = await CacheStorageService.open(
CACHES.THUMBS,
);
return thumbnailCache;
} catch (e) {
return null;
// ignore
}
}
public async getCachedThumbnail(
file: EnteFile,
thumbnailCache?: LimitedCache,
) {
try {
if (!thumbnailCache) {
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, castToken: string) {
try {
if (!this.thumbnailObjectURLPromise.has(file.id)) {
const downloadPromise = async () => {
const thumbnailCache = await this.getThumbnailCache();
const cachedThumb = await this.getCachedThumbnail(
file,
thumbnailCache,
);
if (cachedThumb) {
return cachedThumb;
}
const thumb = await this.downloadThumb(castToken, file);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache?.put(
file.id.toString(),
new Response(thumbBlob),
);
} catch (e) {
// 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 castDownloadManager preview Failed");
throw e;
}
}
private downloadThumb = async (castToken: string, file: EnteFile) => {
const resp = await HTTPService.get(
getCastThumbnailURL(file.id),
null,
{
"X-Cast-Access-Token": castToken,
},
{ responseType: "arraybuffer" },
);
if (typeof resp.data === "undefined") {
throw Error(CustomError.REQUEST_FAILED);
}
const cryptoWorker = 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, castToken: string, forPreview = false) => {
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
try {
const getFilePromise = async () => {
const fileStream = await this.downloadFile(castToken, 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, "castDownloadManager failed to get file");
throw e;
}
};
public async getCachedOriginalFile(file: EnteFile) {
return await this.fileObjectURLPromise.get(file.id.toString());
}
async downloadFile(castToken: string, file: EnteFile) {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const onDownloadProgress = this.trackDownloadProgress(file.id);
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
@ -187,9 +39,6 @@ class CastDownloadManager {
});
const reader = resp.body.getReader();
const contentLength = +resp.headers.get("Content-Length");
let downloadedBytes = 0;
const stream = new ReadableStream({
async start(controller) {
const decryptionHeader = await cryptoWorker.fromB64(
@ -208,11 +57,6 @@ class CastDownloadManager {
reader.read().then(async ({ done, value }) => {
// Is there more data to read?
if (!done) {
downloadedBytes += value.byteLength;
onDownloadProgress({
loaded: downloadedBytes,
total: contentLength,
});
const buffer = new Uint8Array(
data.byteLength + value.byteLength,
);
@ -254,20 +98,6 @@ class CastDownloadManager {
});
return stream;
}
trackDownloadProgress = (fileID: number) => {
return (event: { loaded: number; total: number }) => {
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));
};
};
}
export default new CastDownloadManager();

View file

@ -1,12 +0,0 @@
import { EventEmitter } from "eventemitter3";
// When registering event handlers,
// handle errors to avoid unhandled rejection or propagation to emit call
export enum Events {
LOGOUT = "logout",
FILE_UPLOADED = "fileUploaded",
LOCAL_FILES_UPDATED = "localFilesUpdated",
}
export const eventBus = new EventEmitter<Events>();

View file

@ -1,26 +1,19 @@
// import isElectron from 'is-electron';
// import { ElectronFFmpeg } from 'services/electron/ffmpeg';
import { ElectronFile } from "types/upload";
import ComlinkFFmpegWorker from "utils/comlink/ComlinkFFmpegWorker";
export interface IFFmpeg {
run: (
cmd: string[],
inputFile: File | ElectronFile,
inputFile: File,
outputFilename: string,
dontTimeout?: boolean,
) => Promise<File | ElectronFile>;
) => Promise<File>;
}
class FFmpegFactory {
private client: IFFmpeg;
async getFFmpegClient() {
if (!this.client) {
// if (isElectron()) {
// this.client = new ElectronFFmpeg();
// } else {
this.client = await ComlinkFFmpegWorker.getInstance();
// }
}
return this.client;
}

View file

@ -4,10 +4,9 @@ import {
INPUT_PATH_PLACEHOLDER,
OUTPUT_PATH_PLACEHOLDER,
} from "constants/ffmpeg";
import { ElectronFile } from "types/upload";
import ffmpegFactory from "./ffmpegFactory";
export async function convertToMP4(file: File | ElectronFile) {
export async function convertToMP4(file: File) {
try {
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
return await ffmpegClient.run(

View file

@ -1,14 +0,0 @@
import { logError } from "@ente/shared/sentry";
import WasmHEICConverterService from "./wasmHeicConverter/wasmHEICConverterService";
class HeicConversionService {
async convert(heicFileData: Blob): Promise<Blob> {
try {
return await WasmHEICConverterService.convert(heicFileData);
} catch (e) {
logError(e, "failed to convert heic file");
throw e;
}
}
}
export default new HeicConversionService();

View file

@ -30,16 +30,3 @@ export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => {
}
return livePhoto;
};
export const encodeLivePhoto = async (livePhoto: LivePhoto) => {
const zip = new JSZip();
zip.file(
"image" + getFileExtensionWithDot(livePhoto.imageNameTitle),
livePhoto.image,
);
zip.file(
"video" + getFileExtensionWithDot(livePhoto.videoNameTitle),
livePhoto.video,
);
return await zip.generateAsync({ type: "uint8array" });
};

View file

@ -1,10 +1,7 @@
import { logError } from "@ente/shared/sentry";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import { ElectronFile } from "types/upload";
export async function getUint8ArrayView(
file: Blob | ElectronFile,
): Promise<Uint8Array> {
export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> {
try {
return new Uint8Array(await file.arrayBuffer());
} catch (e) {
@ -14,45 +11,3 @@ export async function getUint8ArrayView(
throw e;
}
}
export function getFileStream(file: File, chunkSize: number) {
const fileChunkReader = fileChunkReaderMaker(file, chunkSize);
const stream = new ReadableStream<Uint8Array>({
async pull(controller: ReadableStreamDefaultController) {
const chunk = await fileChunkReader.next();
if (chunk.done) {
controller.close();
} else {
controller.enqueue(chunk.value);
}
},
});
const chunkCount = Math.ceil(file.size / chunkSize);
return {
stream,
chunkCount,
};
}
export async function getElectronFileStream(
file: ElectronFile,
chunkSize: number,
) {
const chunkCount = Math.ceil(file.size / chunkSize);
return {
stream: await file.stream(),
chunkCount,
};
}
async function* fileChunkReaderMaker(file: File, chunkSize: number) {
let offset = 0;
while (offset < file.size) {
const blob = file.slice(offset, chunkSize + offset);
const fileChunk = await getUint8ArrayView(blob);
yield fileChunk;
offset += chunkSize;
}
return null;
}

View file

@ -6,37 +6,25 @@ import {
KNOWN_NON_MEDIA_FORMATS,
WHITELISTED_FILE_FORMATS,
} from "constants/upload";
import FileType, { FileTypeResult } from "file-type";
import { ElectronFile, FileTypeInfo } from "types/upload";
import FileType from "file-type";
import { FileTypeInfo } from "types/upload";
import { getFileExtension } from "utils/file";
import { getUint8ArrayView } from "./readerService";
function getFileSize(file: File | ElectronFile) {
return file.size;
}
const TYPE_VIDEO = "video";
const TYPE_IMAGE = "image";
const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100;
export async function getFileType(
receivedFile: File | ElectronFile,
): Promise<FileTypeInfo> {
export async function getFileType(receivedFile: File): Promise<FileTypeInfo> {
try {
let fileType: FILE_TYPE;
let typeResult: FileTypeResult;
if (receivedFile instanceof File) {
typeResult = await extractFileType(receivedFile);
} else {
typeResult = await extractElectronFileType(receivedFile);
}
const typeResult = await extractFileType(receivedFile);
const mimTypeParts: string[] = typeResult.mime?.split("/");
if (mimTypeParts?.length !== 2) {
throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
}
switch (mimTypeParts[0]) {
case TYPE_IMAGE:
fileType = FILE_TYPE.IMAGE;
@ -54,7 +42,7 @@ export async function getFileType(
};
} catch (e) {
const fileFormat = getFileExtension(receivedFile.name);
const fileSize = convertBytesToHumanReadable(getFileSize(receivedFile));
const fileSize = convertBytesToHumanReadable(receivedFile.size);
const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
(a) => a.exactType === fileFormat,
);
@ -85,14 +73,6 @@ async function extractFileType(file: File) {
return getFileTypeFromBuffer(fileDataChunk);
}
async function extractElectronFileType(file: ElectronFile) {
const stream = await file.stream();
const reader = stream.getReader();
const { value: fileDataChunk } = await reader.read();
await reader.cancel();
return getFileTypeFromBuffer(fileDataChunk);
}
async function getFileTypeFromBuffer(buffer: Uint8Array) {
const result = await FileType.fromBuffer(buffer);
if (!result?.mime) {

View file

@ -1,13 +0,0 @@
import * as HeicConvert from "heic-convert";
import { getUint8ArrayView } from "services/readerService";
export async function convertHEIC(
fileBlob: Blob,
format: string,
): Promise<Blob> {
const filedata = await getUint8ArrayView(fileBlob);
const result = await HeicConvert({ buffer: filedata, format });
const convertedFileData = new Uint8Array(result);
const convertedFileBlob = new Blob([convertedFileData]);
return convertedFileBlob;
}

View file

@ -1,114 +0,0 @@
import { CustomError } from "@ente/shared/error";
import { addLogLine } from "@ente/shared/logging";
import { retryAsyncFunction } from "@ente/shared/promise";
import { logError } from "@ente/shared/sentry";
import QueueProcessor from "@ente/shared/utils/queueProcessor";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker";
import { ComlinkWorker } from "utils/comlink/comlinkWorker";
import { DedicatedConvertWorker } from "worker/convert.worker";
const WORKER_POOL_SIZE = 2;
const MAX_CONVERSION_IN_PARALLEL = 1;
const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100];
const WAIT_TIME_IN_MICROSECONDS = 30 * 1000;
const BREATH_TIME_IN_MICROSECONDS = 1000;
const CONVERT_FORMAT = "JPEG";
class HEICConverter {
private convertProcessor = new QueueProcessor<Blob>(
MAX_CONVERSION_IN_PARALLEL,
);
private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
private ready: Promise<void>;
constructor() {
this.ready = this.init();
}
private async init() {
this.workerPool = [];
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
this.workerPool.push(getDedicatedConvertWorker());
}
}
async convert(fileBlob: Blob): Promise<Blob> {
await this.ready;
const response = this.convertProcessor.queueUpRequest(() =>
retryAsyncFunction<Blob>(async () => {
const convertWorker = this.workerPool.shift();
const worker = await convertWorker.remote;
try {
const convertedHEIC = await new Promise<Blob>(
(resolve, reject) => {
const main = async () => {
try {
const timeout = setTimeout(() => {
reject(Error("wait time exceeded"));
}, WAIT_TIME_IN_MICROSECONDS);
const startTime = Date.now();
const convertedHEIC =
await worker.convertHEIC(
fileBlob,
CONVERT_FORMAT,
);
addLogLine(
`originalFileSize:${convertBytesToHumanReadable(
fileBlob?.size,
)},convertedFileSize:${convertBytesToHumanReadable(
convertedHEIC?.size,
)}, heic conversion time: ${
Date.now() - startTime
}ms `,
);
clearTimeout(timeout);
resolve(convertedHEIC);
} catch (e) {
reject(e);
}
};
main();
},
);
if (!convertedHEIC || convertedHEIC?.size === 0) {
logError(
Error(`converted heic fileSize is Zero`),
"converted heic fileSize is Zero",
{
originalFileSize: convertBytesToHumanReadable(
fileBlob?.size ?? 0,
),
convertedFileSize: convertBytesToHumanReadable(
convertedHEIC?.size ?? 0,
),
},
);
}
await new Promise((resolve) => {
setTimeout(
() => resolve(null),
BREATH_TIME_IN_MICROSECONDS,
);
});
this.workerPool.push(convertWorker);
return convertedHEIC;
} catch (e) {
logError(e, "heic conversion failed");
convertWorker.terminate();
this.workerPool.push(getDedicatedConvertWorker());
throw e;
}
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS),
);
try {
return await response.promise;
} catch (e) {
if (e.message === CustomError.REQUEST_CANCELLED) {
// ignore
return null;
}
throw e;
}
}
}
export default new HEICConverter();

View file

@ -1,10 +0,0 @@
export interface LimitedCacheStorage {
open: (cacheName: string) => Promise<LimitedCache>;
delete: (cacheName: string) => Promise<boolean>;
}
export interface LimitedCache {
match: (key: string) => Promise<Response>;
put: (key: string, data: Response) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}

View file

@ -1,5 +0,0 @@
export interface CastPayload {
collectionID: number;
collectionKey: string;
castToken: string;
}

View file

@ -3,7 +3,6 @@ import {
LocalFileAttributes,
} from "@ente/shared/crypto/types";
import { FILE_TYPE } from "constants/file";
import { Collection } from "types/collection";
import {
FilePublicMagicMetadata,
FilePublicMagicMetadataProps,
@ -39,24 +38,6 @@ export interface Metadata {
deviceFolder?: string;
}
export interface Location {
latitude: number;
longitude: number;
}
export interface ParsedMetadataJSON {
creationTime: number;
modificationTime: number;
latitude: number;
longitude: number;
}
export interface MultipartUploadURLs {
objectKey: string;
partURLs: string[];
completeURL: string;
}
export interface FileTypeInfo {
fileType: FILE_TYPE;
exactType: string;
@ -65,43 +46,6 @@ export interface FileTypeInfo {
videoType?: string;
}
/*
* ElectronFile is a custom interface that is used to represent
* any file on disk as a File-like object in the Electron desktop app.
*
* This was added to support the auto-resuming of failed uploads
* which needed absolute paths to the files which the
* normal File interface does not provide.
*/
export interface ElectronFile {
name: string;
path: string;
size: number;
lastModified: number;
stream: () => Promise<ReadableStream<Uint8Array>>;
blob: () => Promise<Blob>;
arrayBuffer: () => Promise<Uint8Array>;
}
export interface UploadAsset {
isLivePhoto?: boolean;
file?: File | ElectronFile;
livePhotoAssets?: LivePhotoAssets;
isElectron?: boolean;
}
export interface LivePhotoAssets {
image: globalThis.File | ElectronFile;
video: globalThis.File | ElectronFile;
}
export interface FileWithCollection extends UploadAsset {
localID: number;
collection?: Collection;
collectionID?: number;
}
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
export interface UploadURL {
url: string;
objectKey: string;

View file

@ -1,43 +0,0 @@
import { UPLOAD_RESULT, UPLOAD_STAGES } from "constants/upload";
export type FileID = number;
export type FileName = string;
export type PercentageUploaded = number;
export type UploadFileNames = Map<FileID, FileName>;
export interface UploadCounter {
finished: number;
total: number;
}
export interface InProgressUpload {
localFileID: FileID;
progress: PercentageUploaded;
}
export interface FinishedUpload {
localFileID: FileID;
result: UPLOAD_RESULT;
}
export type InProgressUploads = Map<FileID, PercentageUploaded>;
export type FinishedUploads = Map<FileID, UPLOAD_RESULT>;
export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>;
export interface ProgressUpdater {
setPercentComplete: React.Dispatch<React.SetStateAction<number>>;
setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>;
setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>;
setInProgressUploads: React.Dispatch<
React.SetStateAction<InProgressUpload[]>
>;
setFinishedUploads: React.Dispatch<
React.SetStateAction<SegregatedFinishedUploads>
>;
setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>;
setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>;
setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>;
}

View file

@ -1,147 +0,0 @@
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { User } from "@ente/shared/user/types";
import {
CollectionSummaryType,
CollectionType,
HIDE_FROM_COLLECTION_BAR_TYPES,
OPTIONS_NOT_HAVING_COLLECTION_TYPES,
} from "constants/collection";
import { COLLECTION_ROLE, Collection } from "types/collection";
import { SUB_TYPE, VISIBILITY_STATE } from "types/magicMetadata";
export enum COLLECTION_OPS_TYPE {
ADD,
MOVE,
REMOVE,
RESTORE,
UNHIDE,
}
export function getSelectedCollection(
collectionID: number,
collections: Collection[],
) {
return collections.find((collection) => collection.id === collectionID);
}
export const shouldShowOptions = (type: CollectionSummaryType) => {
return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type);
};
export const showEmptyTrashQuickOption = (type: CollectionSummaryType) => {
return type === CollectionSummaryType.trash;
};
export const showDownloadQuickOption = (type: CollectionSummaryType) => {
return (
type === CollectionSummaryType.folder ||
type === CollectionSummaryType.favorites ||
type === CollectionSummaryType.album ||
type === CollectionSummaryType.uncategorized ||
type === CollectionSummaryType.hiddenItems ||
type === CollectionSummaryType.incomingShareViewer ||
type === CollectionSummaryType.incomingShareCollaborator ||
type === CollectionSummaryType.outgoingShare ||
type === CollectionSummaryType.sharedOnlyViaLink ||
type === CollectionSummaryType.archived ||
type === CollectionSummaryType.pinned
);
};
export const showShareQuickOption = (type: CollectionSummaryType) => {
return (
type === CollectionSummaryType.folder ||
type === CollectionSummaryType.album ||
type === CollectionSummaryType.outgoingShare ||
type === CollectionSummaryType.sharedOnlyViaLink ||
type === CollectionSummaryType.archived ||
type === CollectionSummaryType.incomingShareViewer ||
type === CollectionSummaryType.incomingShareCollaborator ||
type === CollectionSummaryType.pinned
);
};
export const shouldBeShownOnCollectionBar = (type: CollectionSummaryType) => {
return !HIDE_FROM_COLLECTION_BAR_TYPES.has(type);
};
export const getUserOwnedCollections = (collections: Collection[]) => {
const user: User = getData(LS_KEYS.USER);
if (!user?.id) {
throw Error("user missing");
}
return collections.filter((collection) => collection.owner.id === user.id);
};
export const isDefaultHiddenCollection = (collection: Collection) =>
collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN;
export const isHiddenCollection = (collection: Collection) =>
collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN;
export const isQuickLinkCollection = (collection: Collection) =>
collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION;
export function isOutgoingShare(collection: Collection, user: User): boolean {
return collection.owner.id === user.id && collection.sharees?.length > 0;
}
export function isIncomingShare(collection: Collection, user: User) {
return collection.owner.id !== user.id;
}
export function isIncomingViewerShare(collection: Collection, user: User) {
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
return sharee?.role === COLLECTION_ROLE.VIEWER;
}
export function isIncomingCollabShare(collection: Collection, user: User) {
const sharee = collection.sharees?.find((sharee) => sharee.id === user.id);
return sharee?.role === COLLECTION_ROLE.COLLABORATOR;
}
export function isSharedOnlyViaLink(collection: Collection) {
return collection.publicURLs?.length && !collection.sharees?.length;
}
export function isValidMoveTarget(
sourceCollectionID: number,
targetCollection: Collection,
user: User,
) {
return (
sourceCollectionID !== targetCollection.id &&
!isHiddenCollection(targetCollection) &&
!isQuickLinkCollection(targetCollection) &&
!isIncomingShare(targetCollection, user)
);
}
export function isValidReplacementAlbum(
collection: Collection,
user: User,
wantedCollectionName: string,
) {
return (
collection.name === wantedCollectionName &&
(collection.type === CollectionType.album ||
collection.type === CollectionType.folder) &&
!isHiddenCollection(collection) &&
!isQuickLinkCollection(collection) &&
!isIncomingShare(collection, user)
);
}
export function getCollectionNameMap(
collections: Collection[],
): Map<number, string> {
return new Map<number, string>(
collections.map((collection) => [collection.id, collection.name]),
);
}
export function getNonHiddenCollections(
collections: Collection[],
): Collection[] {
return collections.filter((collection) => !isHiddenCollection(collection));
}
export function getHiddenCollections(collections: Collection[]): Collection[] {
return collections.filter((collection) => isHiddenCollection(collection));
}

View file

@ -1,30 +0,0 @@
import { runningInBrowser } from "@ente/shared/platform";
import { Remote } from "comlink";
import { DedicatedConvertWorker } from "worker/convert.worker";
import { ComlinkWorker } from "./comlinkWorker";
class ComlinkConvertWorker {
private comlinkWorkerInstance: Remote<DedicatedConvertWorker>;
async getInstance() {
if (!this.comlinkWorkerInstance) {
this.comlinkWorkerInstance =
await getDedicatedConvertWorker().remote;
}
return this.comlinkWorkerInstance;
}
}
export const getDedicatedConvertWorker = () => {
if (runningInBrowser()) {
const cryptoComlinkWorker = new ComlinkWorker<
typeof DedicatedConvertWorker
>(
"ente-convert-worker",
new Worker(new URL("worker/convert.worker.ts", import.meta.url)),
);
return cryptoComlinkWorker;
}
};
export default new ComlinkConvertWorker();

View file

@ -1,6 +1,5 @@
import { addLocalLog } from "@ente/shared/logging";
import { Remote, wrap } from "comlink";
// import { WorkerElectronCacheStorageClient } from 'services/workerElectronCache/client';
export class ComlinkWorker<T extends new () => InstanceType<T>> {
public remote: Promise<Remote<InstanceType<T>>>;
@ -17,7 +16,6 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
addLocalLog(() => `Initiated ${this.name}`);
const comlink = wrap<T>(this.worker);
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
// expose(WorkerElectronCacheStorageClient, this.worker);
}
public terminate() {

View file

@ -1,15 +0,0 @@
export const readAsDataURL = (blob) =>
new Promise<string>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader.result as string);
fileReader.onerror = () => reject(fileReader.error);
fileReader.readAsDataURL(blob);
});
export const readAsText = (blob) =>
new Promise<string>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => resolve(fileReader.result as string);
fileReader.onerror = () => reject(fileReader.error);
fileReader.readAsText(blob);
});

View file

@ -1,14 +1,6 @@
import { logError } from "@ente/shared/sentry";
import {
FILE_TYPE,
RAW_FORMATS,
SUPPORTED_RAW_FORMATS,
TYPE_HEIC,
TYPE_HEIF,
} from "constants/file";
import { FILE_TYPE, RAW_FORMATS } from "constants/file";
import CastDownloadManager from "services/castDownloadManager";
import * as ffmpegService from "services/ffmpeg/ffmpegService";
import heicConversionService from "services/heicConversionService";
import { decodeLivePhoto } from "services/livePhotoService";
import { getFileType } from "services/typeDetectionService";
import {
@ -17,20 +9,8 @@ import {
FileMagicMetadata,
FilePublicMagicMetadata,
} from "types/file";
import { isArchivedFile } from "utils/magicMetadata";
import { CustomError } from "@ente/shared/error";
import { addLocalLog, addLogLine } from "@ente/shared/logging";
import { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { User } from "@ente/shared/user/types";
import { convertBytesToHumanReadable } from "@ente/shared/utils/size";
import isElectron from "is-electron";
import { FileTypeInfo } from "types/upload";
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000;
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
@ -46,20 +26,6 @@ export function sortFiles(files: EnteFile[], sortAsc = false) {
});
}
export function sortTrashFiles(files: EnteFile[]) {
return files.sort((a, b) => {
if (a.deleteBy === b.deleteBy) {
if (a.metadata.creationTime === b.metadata.creationTime) {
return (
b.metadata.modificationTime - a.metadata.modificationTime
);
}
return b.metadata.creationTime - a.metadata.creationTime;
}
return a.deleteBy - b.deleteBy;
});
}
export async function decryptFile(
file: EncryptedEnteFile,
collectionKey: string,
@ -154,176 +120,6 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) {
});
}
export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) {
switch (file.metadata.fileType) {
case FILE_TYPE.IMAGE: {
const convertedBlob = await getRenderableImage(
file.metadata.title,
fileBlob,
);
const { originalURL, convertedURL } = getFileObjectURLs(
fileBlob,
convertedBlob,
);
return {
converted: [convertedURL],
original: [originalURL],
};
}
case FILE_TYPE.LIVE_PHOTO: {
return await getRenderableLivePhotoURL(file, fileBlob);
}
case FILE_TYPE.VIDEO: {
const convertedBlob = await getPlayableVideo(
file.metadata.title,
fileBlob,
);
const { originalURL, convertedURL } = getFileObjectURLs(
fileBlob,
convertedBlob,
);
return {
converted: [convertedURL],
original: [originalURL],
};
}
default: {
const previewURL = await createTypedObjectURL(
fileBlob,
file.metadata.title,
);
return {
converted: [previewURL],
original: [previewURL],
};
}
}
}
async function getRenderableLivePhotoURL(
file: EnteFile,
fileBlob: Blob,
): Promise<{ original: string[]; converted: string[] }> {
const livePhoto = await decodeLivePhoto(file, fileBlob);
const imageBlob = new Blob([livePhoto.image]);
const videoBlob = new Blob([livePhoto.video]);
const convertedImageBlob = await getRenderableImage(
livePhoto.imageNameTitle,
imageBlob,
);
const convertedVideoBlob = await getPlayableVideo(
livePhoto.videoNameTitle,
videoBlob,
true,
);
const { originalURL: originalImageURL, convertedURL: convertedImageURL } =
getFileObjectURLs(imageBlob, convertedImageBlob);
const { originalURL: originalVideoURL, convertedURL: convertedVideoURL } =
getFileObjectURLs(videoBlob, convertedVideoBlob);
return {
converted: [convertedImageURL, convertedVideoURL],
original: [originalImageURL, originalVideoURL],
};
}
export async function getPlayableVideo(
videoNameTitle: string,
videoBlob: Blob,
forceConvert = false,
) {
try {
const isPlayable = await isPlaybackPossible(
URL.createObjectURL(videoBlob),
);
if (isPlayable && !forceConvert) {
return videoBlob;
} else {
if (!forceConvert && !isElectron()) {
return null;
}
addLogLine(
"video format not supported, converting it name:",
videoNameTitle,
);
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
new File([videoBlob], videoNameTitle),
);
addLogLine("video successfully converted", videoNameTitle);
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
}
} catch (e) {
addLogLine("video conversion failed", videoNameTitle);
logError(e, "video conversion failed");
return null;
}
}
export async function getRenderableImage(fileName: string, imageBlob: Blob) {
let fileTypeInfo: FileTypeInfo;
try {
const tempFile = new File([imageBlob], fileName);
fileTypeInfo = await getFileType(tempFile);
addLocalLog(() => `file type info: ${JSON.stringify(fileTypeInfo)}`);
const { exactType } = fileTypeInfo;
let convertedImageBlob: Blob;
if (isRawFile(exactType)) {
try {
if (!isSupportedRawFormat(exactType)) {
throw Error(CustomError.UNSUPPORTED_RAW_FORMAT);
}
if (!isElectron()) {
throw Error(CustomError.NOT_AVAILABLE_ON_WEB);
}
addLogLine(
`RawConverter called for ${fileName}-${convertBytesToHumanReadable(
imageBlob.size,
)}`,
);
// convertedImageBlob = await imageProcessor.convertToJPEG(
// imageBlob,
// fileName
// );
addLogLine(`${fileName} successfully converted`);
} catch (e) {
try {
if (!isFileHEIC(exactType)) {
throw e;
}
addLogLine(
`HEICConverter called for ${fileName}-${convertBytesToHumanReadable(
imageBlob.size,
)}`,
);
convertedImageBlob =
await heicConversionService.convert(imageBlob);
addLogLine(`${fileName} successfully converted`);
} catch (e) {
throw Error(CustomError.NON_PREVIEWABLE_FILE);
}
}
return convertedImageBlob;
} else {
return imageBlob;
}
} catch (e) {
logError(e, "get Renderable Image failed", { fileTypeInfo });
return null;
}
}
export function isFileHEIC(exactType: string) {
return (
exactType.toLowerCase().endsWith(TYPE_HEIC) ||
exactType.toLowerCase().endsWith(TYPE_HEIF)
);
}
export function isRawFile(exactType: string) {
return RAW_FORMATS.includes(exactType.toLowerCase());
}
export function isRawFileFromFileName(fileName: string) {
for (const rawFormat of RAW_FORMATS) {
if (fileName.toLowerCase().endsWith(rawFormat)) {
@ -333,10 +129,6 @@ export function isRawFileFromFileName(fileName: string) {
return false;
}
export function isSupportedRawFormat(exactType: string) {
return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase());
}
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
return files.map((file) => {
if (file.pubMagicMetadata?.data.editedTime) {
@ -350,187 +142,24 @@ export function mergeMetadata(files: EnteFile[]): EnteFile[] {
});
}
export async function getFileFromURL(fileURL: string) {
const fileBlob = await (await fetch(fileURL)).blob();
const fileFile = new File([fileBlob], "temp");
return fileFile;
}
export function getUniqueFiles(files: EnteFile[]) {
const idSet = new Set<number>();
const uniqueFiles = files.filter((file) => {
if (!idSet.has(file.id)) {
idSet.add(file.id);
return true;
} else {
return false;
}
});
return uniqueFiles;
}
export const isImageOrVideo = (fileType: FILE_TYPE) =>
[FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType);
export const getArchivedFiles = (files: EnteFile[]) => {
return files.filter(isArchivedFile).map((file) => file.id);
};
export const createTypedObjectURL = async (blob: Blob, fileName: string) => {
const type = await getFileType(new File([blob], fileName));
return URL.createObjectURL(new Blob([blob], { type: type.mimeType }));
};
export const getUserOwnedFiles = (files: EnteFile[]) => {
const user: User = getData(LS_KEYS.USER);
if (!user?.id) {
throw Error("user missing");
}
return files.filter((file) => file.ownerID === user.id);
};
// doesn't work on firefox
export const copyFileToClipboard = async (fileUrl: string) => {
const canvas = document.createElement("canvas");
const canvasCTX = canvas.getContext("2d");
const image = new Image();
const blobPromise = new Promise<Blob>((resolve, reject) => {
let timeout: NodeJS.Timeout = null;
try {
image.setAttribute("src", fileUrl);
image.onload = () => {
canvas.width = image.width;
canvas.height = image.height;
canvasCTX.drawImage(image, 0, 0, image.width, image.height);
canvas.toBlob(
(blob) => {
resolve(blob);
},
"image/png",
1,
);
clearTimeout(timeout);
};
} catch (e) {
void logError(e, "failed to copy to clipboard");
reject(e);
} finally {
clearTimeout(timeout);
}
timeout = setTimeout(
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
WAIT_TIME_IMAGE_CONVERSION,
);
});
const { ClipboardItem } = window;
await navigator.clipboard
.write([new ClipboardItem({ "image/png": blobPromise })])
.catch((e) => logError(e, "failed to copy to clipboard"));
};
export function getLatestVersionFiles(files: EnteFile[]) {
const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => {
const uid = `${file.collectionID}-${file.id}`;
if (
!latestVersionFiles.has(uid) ||
latestVersionFiles.get(uid).updationTime < file.updationTime
) {
latestVersionFiles.set(uid, file);
}
});
return Array.from(latestVersionFiles.values()).filter(
(file) => !file.isDeleted,
);
}
export function getPersonalFiles(files: EnteFile[], user: User) {
if (!user?.id) {
throw Error("user missing");
}
return files.filter((file) => file.ownerID === user.id);
}
export function getIDBasedSortedFiles(files: EnteFile[]) {
return files.sort((a, b) => a.id - b.id);
}
export function constructFileToCollectionMap(files: EnteFile[]) {
const fileToCollectionsMap = new Map<number, number[]>();
(files ?? []).forEach((file) => {
if (!fileToCollectionsMap.get(file.id)) {
fileToCollectionsMap.set(file.id, []);
}
fileToCollectionsMap.get(file.id).push(file.collectionID);
});
return fileToCollectionsMap;
}
export const shouldShowAvatar = (file: EnteFile, user: User) => {
if (!file || !user) {
return false;
}
// is Shared file
else if (file.ownerID !== user.id) {
return true;
}
// is public collected file
else if (
file.ownerID === user.id &&
file.pubMagicMetadata?.data?.uploaderName
) {
return true;
} else {
return false;
}
};
export const getPreviewableImage = async (
file: EnteFile,
castToken: string,
): Promise<Blob> => {
try {
let fileBlob: Blob;
const fileURL =
await CastDownloadManager.getCachedOriginalFile(file)[0];
if (!fileURL) {
fileBlob = await new Response(
await CastDownloadManager.downloadFile(castToken, file),
).blob();
} else {
fileBlob = await (await fetch(fileURL)).blob();
}
let fileBlob = await new Response(
await CastDownloadManager.downloadFile(castToken, file),
).blob();
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const livePhoto = await decodeLivePhoto(file, fileBlob);
fileBlob = new Blob([livePhoto.image]);
}
const convertedBlob = await getRenderableImage(
file.metadata.title,
fileBlob,
);
fileBlob = convertedBlob;
const fileType = await getFileType(
new File([fileBlob], file.metadata.title),
);
fileBlob = new Blob([fileBlob], { type: fileType.mimeType });
return fileBlob;
} catch (e) {
logError(e, "failed to download file");
}
};
const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => {
const originalURL = URL.createObjectURL(originalBlob);
const convertedURL = convertedBlob
? convertedBlob === originalBlob
? originalURL
: URL.createObjectURL(convertedBlob)
: null;
return { originalURL, convertedURL };
};

View file

@ -1,42 +0,0 @@
import { FILE_TYPE } from "constants/file";
import { getFileExtension } from "utils/file";
const IMAGE_EXTENSIONS = [
"heic",
"heif",
"jpeg",
"jpg",
"png",
"gif",
"bmp",
"tiff",
"webp",
];
const VIDEO_EXTENSIONS = [
"mov",
"mp4",
"m4v",
"avi",
"wmv",
"flv",
"mkv",
"webm",
"3gp",
"3g2",
"avi",
"ogv",
"mpg",
"mp",
];
export function getFileTypeFromExtensionForLivePhotoClustering(
filename: string,
) {
const extension = getFileExtension(filename)?.toLowerCase();
if (IMAGE_EXTENSIONS.includes(extension)) {
return FILE_TYPE.IMAGE;
} else if (VIDEO_EXTENSIONS.includes(extension)) {
return FILE_TYPE.VIDEO;
}
}

View file

@ -1,97 +0,0 @@
import { Collection } from "types/collection";
import { EnteFile } from "types/file";
import { MagicMetadataCore, VISIBILITY_STATE } from "types/magicMetadata";
import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker";
export function isArchivedFile(item: EnteFile): boolean {
if (!item || !item.magicMetadata || !item.magicMetadata.data) {
return false;
}
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
}
export function isArchivedCollection(item: Collection): boolean {
if (!item) {
return false;
}
if (item.magicMetadata && item.magicMetadata.data) {
return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
}
if (item.sharedMagicMetadata && item.sharedMagicMetadata.data) {
return (
item.sharedMagicMetadata.data.visibility ===
VISIBILITY_STATE.ARCHIVED
);
}
return false;
}
export function isPinnedCollection(item: Collection) {
if (
!item ||
!item.magicMetadata ||
!item.magicMetadata.data ||
typeof item.magicMetadata.data === "string" ||
typeof item.magicMetadata.data.order === "undefined"
) {
return false;
}
return item.magicMetadata.data.order !== 0;
}
export async function updateMagicMetadata<T>(
magicMetadataUpdates: T,
originalMagicMetadata?: MagicMetadataCore<T>,
decryptionKey?: string,
): Promise<MagicMetadataCore<T>> {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
if (!originalMagicMetadata) {
originalMagicMetadata = getNewMagicMetadata<T>();
}
if (typeof originalMagicMetadata?.data === "string") {
originalMagicMetadata.data = await cryptoWorker.decryptMetadata(
originalMagicMetadata.data,
originalMagicMetadata.header,
decryptionKey,
);
}
// copies the existing magic metadata properties of the files and updates the visibility value
// The expected behavior while updating magic metadata is to let the existing property as it is and update/add the property you want
const magicMetadataProps: T = {
...originalMagicMetadata.data,
...magicMetadataUpdates,
};
const nonEmptyMagicMetadataProps =
getNonEmptyMagicMetadataProps(magicMetadataProps);
const magicMetadata = {
...originalMagicMetadata,
data: nonEmptyMagicMetadataProps,
count: Object.keys(nonEmptyMagicMetadataProps).length,
};
return magicMetadata;
}
export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => {
return {
version: 1,
data: null,
header: null,
count: 0,
};
};
export const getNonEmptyMagicMetadataProps = <T>(magicMetadataProps: T): T => {
return Object.fromEntries(
Object.entries(magicMetadataProps).filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, v]) => v !== null && v !== undefined,
),
) as T;
};

View file

@ -1,10 +0,0 @@
import * as Comlink from "comlink";
import { convertHEIC } from "services/wasmHeicConverter/wasmHEICConverterClient";
export class DedicatedConvertWorker {
async convertHEIC(fileBlob: Blob, format: string) {
return convertHEIC(fileBlob, format);
}
}
Comlink.expose(DedicatedConvertWorker, self);