Merge remote-tracking branch 'origin/main' into streaming-patched
This commit is contained in:
@@ -47,12 +47,6 @@ Future<void> share(
|
||||
pathFutures.add(
|
||||
getFile(file, isOrigin: true).then((fetchedFile) => fetchedFile?.path),
|
||||
);
|
||||
if (file.fileType == FileType.livePhoto) {
|
||||
pathFutures.add(
|
||||
getFile(file, liveVideo: true)
|
||||
.then((fetchedFile) => fetchedFile?.path),
|
||||
);
|
||||
}
|
||||
}
|
||||
final paths = await Future.wait(pathFutures);
|
||||
await dialog.hide();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logUnhandledErrorsAndRejectionsInWorker } from "@/base/log-web";
|
||||
import { expose } from "comlink";
|
||||
import type { StateAddress } from "libsodium-wrappers-sumo";
|
||||
import * as ei from "./ente-impl";
|
||||
@@ -98,3 +99,5 @@ export class CryptoWorker {
|
||||
}
|
||||
|
||||
expose(CryptoWorker);
|
||||
|
||||
logUnhandledErrorsAndRejectionsInWorker();
|
||||
|
||||
@@ -50,6 +50,31 @@ export const logUnhandledErrorsAndRejections = (attach: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach handlers to log any unhandled exceptions and promise rejections in web
|
||||
* workers.
|
||||
*
|
||||
* This is a variant of {@link logUnhandledErrorsAndRejections} that works in
|
||||
* web workers. It should be called at the top level of the main worker script.
|
||||
*
|
||||
* Note: When I tested this, attaching the onerror handler to the worker outside
|
||||
* the worker (e.g. when creating it in comlink-worker.ts) worked, but attaching
|
||||
* the "unhandledrejection" event there did not work. Attaching them to `self`
|
||||
* (the {@link WorkerGlobal}) worked.
|
||||
*/
|
||||
export const logUnhandledErrorsAndRejectionsInWorker = () => {
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
log.error("Unhandled error", event.error);
|
||||
};
|
||||
|
||||
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
||||
log.error("Unhandled promise rejection", event.reason);
|
||||
};
|
||||
|
||||
self.addEventListener("error", handleError);
|
||||
self.addEventListener("unhandledrejection", handleUnhandledRejection);
|
||||
};
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: number;
|
||||
logLine: string;
|
||||
|
||||
@@ -271,7 +271,9 @@ class DownloadManager {
|
||||
}
|
||||
|
||||
private downloadThumbnail = async (file: EnteFile) => {
|
||||
const encryptedData = await this._downloadThumbnail(file);
|
||||
const encryptedData = await wrapErrors(() =>
|
||||
this._downloadThumbnail(file),
|
||||
);
|
||||
const decryptionHeader = file.thumbnail.decryptionHeader;
|
||||
return decryptThumbnail({ encryptedData, decryptionHeader }, file.key);
|
||||
};
|
||||
@@ -385,13 +387,15 @@ class DownloadManager {
|
||||
): Promise<ReadableStream<Uint8Array> | null> {
|
||||
log.info(`download attempted for file id ${file.id}`);
|
||||
|
||||
const res = await this._downloadFile(file);
|
||||
const res = await wrapErrors(() => this._downloadFile(file));
|
||||
|
||||
if (
|
||||
file.metadata.fileType === FileType.image ||
|
||||
file.metadata.fileType === FileType.livePhoto
|
||||
) {
|
||||
const encryptedData = new Uint8Array(await res.arrayBuffer());
|
||||
const encryptedData = new Uint8Array(
|
||||
await wrapErrors(() => res.arrayBuffer()),
|
||||
);
|
||||
|
||||
const decrypted = await decryptStreamBytes(
|
||||
{
|
||||
@@ -431,7 +435,9 @@ class DownloadManager {
|
||||
do {
|
||||
// done is a boolean and value is an Uint8Array. When done
|
||||
// is true value will be empty.
|
||||
const { done, value } = await reader.read();
|
||||
const { done, value } = await wrapErrors(() =>
|
||||
reader.read(),
|
||||
);
|
||||
|
||||
let data: Uint8Array;
|
||||
if (done) {
|
||||
@@ -522,6 +528,52 @@ class DownloadManager {
|
||||
*/
|
||||
export const downloadManager = new DownloadManager();
|
||||
|
||||
/**
|
||||
* A custom Error that is thrown if a download fails during network I/O.
|
||||
*
|
||||
* [Note: Identifying network related errors during download]
|
||||
*
|
||||
* We dealing with code that touches the network, we often don't specifically
|
||||
* care about the specific error - there is a lot that can go wrong when a
|
||||
* network is involved - but need to identify if an error was in the network
|
||||
* related phase of an action, since these are usually transient and can be
|
||||
* dealt with more softly than other errors.
|
||||
*
|
||||
* To that end, network related phases of download operations are wrapped in
|
||||
* catches that intercept the error and wrap it in our custom
|
||||
* {@link NetworkDownloadError} whose presence can be checked using the
|
||||
* {@link isNetworkDownloadError} predicate.
|
||||
*/
|
||||
export class NetworkDownloadError extends Error {
|
||||
error: unknown;
|
||||
|
||||
constructor(e: unknown) {
|
||||
super(
|
||||
`NetworkDownloadError: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
|
||||
// Cargo culted from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (Error.captureStackTrace)
|
||||
Error.captureStackTrace(this, NetworkDownloadError);
|
||||
|
||||
this.error = e;
|
||||
}
|
||||
}
|
||||
|
||||
export const isNetworkDownloadError = (e: unknown) =>
|
||||
e instanceof NetworkDownloadError;
|
||||
|
||||
/**
|
||||
* A helper function to convert all rejections of the given promise {@link op}
|
||||
* into {@link NetworkDownloadError}s.
|
||||
*/
|
||||
const wrapErrors = <T>(op: () => Promise<T>) =>
|
||||
op().catch((e: unknown) => {
|
||||
throw new NetworkDownloadError(e);
|
||||
});
|
||||
|
||||
/**
|
||||
* Create and return a {@link RenderableSourceURLs} for the given {@link file},
|
||||
* where {@link originalFileURLPromise} is a promise that resolves with an
|
||||
|
||||
@@ -2,6 +2,7 @@ import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import { dateFromEpochMicroseconds } from "@/base/date";
|
||||
import log from "@/base/log";
|
||||
import { type Metadata, ItemVisibility } from "./file-metadata";
|
||||
import { FileType } from "./file-type";
|
||||
|
||||
// TODO: Audit this file.
|
||||
|
||||
@@ -405,12 +406,19 @@ export const mergeMetadata1 = (file: EnteFile): EnteFile => {
|
||||
}
|
||||
}
|
||||
|
||||
// In a very rare cases (have found only one so far, a very old file in
|
||||
// In very rare cases (have found only one so far, a very old file in
|
||||
// Vishnu's account, uploaded by an initial dev version of Ente) the photo
|
||||
// has no modification time. Gracefully handle such cases.
|
||||
if (!file.metadata.modificationTime)
|
||||
file.metadata.modificationTime = file.metadata.creationTime;
|
||||
|
||||
// In very rare cases (again, some files shared with Vishnu's account,
|
||||
// uploaded by dev builds) the photo might not have a file type. Gracefully
|
||||
// handle these too. The file ID threshold is an arbitrary cutoff so that
|
||||
// this graceful handling does not mask new issues.
|
||||
if (!file.metadata.fileType && file.id < 100000000)
|
||||
file.metadata.fileType = FileType.image;
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { logUnhandledErrorsAndRejectionsInWorker } from "@/base/log-web";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { expose } from "comlink";
|
||||
import HeicConvert from "heic-convert";
|
||||
@@ -19,6 +20,8 @@ export class HEICConvertWorker {
|
||||
|
||||
expose(HEICConvertWorker);
|
||||
|
||||
logUnhandledErrorsAndRejectionsInWorker();
|
||||
|
||||
const heicToJPEG = async (heicBlob: Blob): Promise<Blob> => {
|
||||
const buffer = new Uint8Array(await heicBlob.arrayBuffer());
|
||||
const result = await HeicConvert({ buffer, format: "JPEG" });
|
||||
|
||||
@@ -304,7 +304,7 @@ const ManageML: React.FC<ManageMLProps> = ({ mlStatus, onDisableML }) => {
|
||||
</Typography>
|
||||
<Typography>{status}</Typography>
|
||||
</Stack>
|
||||
<Divider sx={{ marginInline: 2 }} />
|
||||
<Divider sx={{ mx: 1.5 }} />
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
|
||||
@@ -336,7 +336,7 @@ export const mlSync = async () => {
|
||||
//
|
||||
|
||||
// Fetch indexes, or index locally if needed.
|
||||
await (await worker()).index();
|
||||
await worker().then((w) => w.index());
|
||||
|
||||
await updateClustersAndPeople();
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import { clientPackageName } from "@/base/app";
|
||||
import { assertionFailed } from "@/base/assert";
|
||||
import { isHTTP4xxError } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import { logUnhandledErrorsAndRejectionsInWorker } from "@/base/log-web";
|
||||
import type { ElectronMLWorker } from "@/base/types/ipc";
|
||||
import { isNetworkDownloadError } from "@/gallery/services/download";
|
||||
import type { UploadItem } from "@/gallery/services/upload";
|
||||
import { fileLogID, type EnteFile } from "@/media/file";
|
||||
import { wait } from "@/utils/promise";
|
||||
@@ -347,6 +349,8 @@ export class MLWorker {
|
||||
|
||||
expose(MLWorker);
|
||||
|
||||
logUnhandledErrorsAndRejectionsInWorker();
|
||||
|
||||
/**
|
||||
* Index the given batch of items.
|
||||
*
|
||||
@@ -370,7 +374,7 @@ const indexNextBatch = async (
|
||||
}
|
||||
|
||||
// Keep track if any of the items failed.
|
||||
let allSuccess = true;
|
||||
let failureCount = 0;
|
||||
|
||||
// Index up to 4 items simultaneously.
|
||||
const tasks = new Array<Promise<void> | undefined>(4).fill(undefined);
|
||||
@@ -386,8 +390,10 @@ const indexNextBatch = async (
|
||||
.then(() => {
|
||||
tasks[j] = undefined;
|
||||
})
|
||||
.catch(() => {
|
||||
allSuccess = false;
|
||||
.catch((e: unknown) => {
|
||||
const f = fileLogID(item.file);
|
||||
log.error(`Failed to index ${f}`, e);
|
||||
failureCount++;
|
||||
tasks[j] = undefined;
|
||||
}))(items[i++]!, j);
|
||||
}
|
||||
@@ -411,8 +417,14 @@ const indexNextBatch = async (
|
||||
// Clear any cached CLIP indexes, since now we might have new ones.
|
||||
clearCachedCLIPIndexes();
|
||||
|
||||
log.info(
|
||||
failureCount > 0
|
||||
? `Indexed ${items.length - failureCount} files (${failureCount} failed)`
|
||||
: `Indexed ${items.length} files`,
|
||||
);
|
||||
|
||||
// Return true if nothing failed.
|
||||
return allSuccess;
|
||||
return failureCount == 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -521,25 +533,25 @@ const index = async (
|
||||
// and return.
|
||||
|
||||
if (existingFaceIndex && existingCLIPIndex) {
|
||||
try {
|
||||
await saveIndexes(
|
||||
{ fileID, ...existingFaceIndex },
|
||||
{ fileID, ...existingCLIPIndex },
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(`Failed to save indexes for ${f}`, e);
|
||||
throw e;
|
||||
}
|
||||
await saveIndexes(
|
||||
{ fileID, ...existingFaceIndex },
|
||||
{ fileID, ...existingCLIPIndex },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// There is at least one ML data type that still needs to be indexed.
|
||||
|
||||
const renderableBlob = await fetchRenderableBlob(
|
||||
file,
|
||||
uploadItem,
|
||||
electron,
|
||||
);
|
||||
let renderableBlob: Blob;
|
||||
try {
|
||||
renderableBlob = await fetchRenderableBlob(file, uploadItem, electron);
|
||||
} catch (e) {
|
||||
// Network errors are transient and shouldn't be marked.
|
||||
//
|
||||
// See: [Note: Transient and permanent indexing failures]
|
||||
if (!isNetworkDownloadError(e)) await markIndexingFailed(fileID);
|
||||
throw e;
|
||||
}
|
||||
|
||||
let image: ImageBitmapAndData;
|
||||
try {
|
||||
@@ -551,8 +563,7 @@ const index = async (
|
||||
// reindexing attempt for failed files).
|
||||
//
|
||||
// See: [Note: Transient and permanent indexing failures]
|
||||
log.error(`Failed to get image data for indexing ${f}`, e);
|
||||
await markIndexingFailed(file.id);
|
||||
await markIndexingFailed(fileID);
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -569,8 +580,7 @@ const index = async (
|
||||
]);
|
||||
} catch (e) {
|
||||
// See: [Note: Transient and permanent indexing failures]
|
||||
log.error(`Failed to index ${f}`, e);
|
||||
await markIndexingFailed(file.id);
|
||||
await markIndexingFailed(fileID);
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -611,33 +621,21 @@ const index = async (
|
||||
await putMLData(file, rawMLData);
|
||||
} catch (e) {
|
||||
// See: [Note: Transient and permanent indexing failures]
|
||||
log.error(`Failed to put ML data for ${f}`, e);
|
||||
if (isHTTP4xxError(e)) await markIndexingFailed(file.id);
|
||||
if (isHTTP4xxError(e)) await markIndexingFailed(fileID);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveIndexes(
|
||||
{ fileID, ...faceIndex },
|
||||
{ fileID, ...clipIndex },
|
||||
);
|
||||
} catch (e) {
|
||||
// Not sure if DB failures should be considered permanent or
|
||||
// transient. There isn't a known case where writing to the local
|
||||
// indexedDB should systematically fail. It could fail if there was
|
||||
// no space on device, but that's eminently retriable.
|
||||
log.error(`Failed to save indexes for ${f}`, e);
|
||||
throw e;
|
||||
}
|
||||
await saveIndexes({ fileID, ...faceIndex }, { fileID, ...clipIndex });
|
||||
|
||||
// This step, saving face crops, is conceptually not part of the
|
||||
// indexing pipeline; we just do it here since we have already have the
|
||||
// ImageBitmap at hand. Ignore errors that happen during this since it
|
||||
// does not impact the generated face index.
|
||||
// ImageBitmap at hand.
|
||||
if (!existingFaceIndex) {
|
||||
try {
|
||||
await saveFaceCrops(image.bitmap, faceIndex);
|
||||
} catch (e) {
|
||||
// Ignore errors that happen during this since it does not
|
||||
// impact the generated face index.
|
||||
log.error(`Failed to save face crops for ${f}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HTTPError } from "@/base/http";
|
||||
import { logUnhandledErrorsAndRejectionsInWorker } from "@/base/log-web";
|
||||
import type { Location } from "@/base/types";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import type { EnteFile } from "@/media/file";
|
||||
@@ -113,6 +114,8 @@ export class SearchWorker {
|
||||
|
||||
expose(SearchWorker);
|
||||
|
||||
logUnhandledErrorsAndRejectionsInWorker();
|
||||
|
||||
/**
|
||||
* @param s The normalized form of {@link searchString}.
|
||||
* @param searchString The original search string.
|
||||
|
||||
Reference in New Issue
Block a user