A network failure is printed and recorded with its reason (closes #196)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Node's `fetch` rejects every failure below HTTP as `fetch failed` and puts the reason on `cause`. `describeError` in `src/errors.ts` gives the message followed by the messages on the cause chain, and for an `AggregateError`, which Node gives when every address of a host fails, the message of each address. `run` prints it, and the backup logs and records it. `classify` reads that text and calls a failure transient when `hasTransportCode`, now exported by `src/retry.ts`, finds a transport errno on the chain. A failed ML data fetch keeps its error as the cause. The bounded walk along the chain moved from `src/retry.ts` to `src/errors.ts`. A truncated download's message leaves the authentication failure to its cause. Model: opus-5-5
This commit was merged in pull request #203.
This commit is contained in:
+22
-15
@@ -82,6 +82,7 @@ import {
|
||||
init,
|
||||
} from "./crypto/index.js";
|
||||
import { removeLeftoverTempFiles } from "./download/index.js";
|
||||
import { describeError } from "./errors.js";
|
||||
import { sanitizeFileName, withExtension } from "./filename.js";
|
||||
import {
|
||||
copyAtomic,
|
||||
@@ -94,6 +95,7 @@ import type { Undecryptable } from "./library/store.js";
|
||||
import { extractImageMetadata } from "./metadata-backup.js";
|
||||
import type { MLData } from "./mldata-fetch.js";
|
||||
import type { Collection, EnteFile, FileMetadata } from "./model/types.js";
|
||||
import { hasTransportCode } from "./retry.js";
|
||||
|
||||
export type ProgressCallback = (message: string) => void;
|
||||
|
||||
@@ -219,11 +221,13 @@ const isPresent = (path: string): boolean => {
|
||||
|
||||
// Best-effort classification for the ledger. Retryable server/network problems
|
||||
// are transient; refusals and local filesystem/decrypt errors are permanent;
|
||||
// anything else is unknown. Both the error code and message are inspected.
|
||||
// anything else is unknown. The error code and the error as `describeError`
|
||||
// gives it are inspected, and so is each errno on the error's cause chain,
|
||||
// where Node's `fetch` puts it.
|
||||
const classify = (err: unknown): FailureClass => {
|
||||
if (hasTransportCode(err)) return "transient";
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
const text =
|
||||
`${e?.code ?? ""} ${err instanceof Error ? err.message : String(err)}`.toLowerCase();
|
||||
const text = `${e?.code ?? ""} ${describeError(err)}`.toLowerCase();
|
||||
if (
|
||||
/timeout|timed out|econnreset|econnrefused|econnaborted|network|socket|eai_again|throttl|temporarily|429|500|502|503|504/.test(
|
||||
text,
|
||||
@@ -241,9 +245,6 @@ const classify = (err: unknown): FailureClass => {
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const errorMessage = (err: unknown): string =>
|
||||
err instanceof Error ? err.message : String(err);
|
||||
|
||||
// Ensure `linkPath` is a symlink to `target`, rebuilding a missing, wrong, or
|
||||
// non-symlink entry. Throws on failure (a directory in the way, no permission)
|
||||
// so the caller records it and moves on rather than aborting the run.
|
||||
@@ -470,7 +471,7 @@ const imageMetadataFor = async (
|
||||
const bytes = await readFile(originalPath);
|
||||
return { imageMetadata: extractImageMetadata(bytes) ?? {} };
|
||||
} catch (err) {
|
||||
return { imageMetadataError: errorMessage(err) };
|
||||
return { imageMetadataError: describeError(err) };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -610,7 +611,7 @@ const runLockedBackup = async (
|
||||
// and thumbnail both fail this run must not double its attempt count
|
||||
// or appear twice in errors.
|
||||
if (failedThisRun.has(fileID)) return;
|
||||
const error = errorMessage(err);
|
||||
const error = describeError(err);
|
||||
errors.push({ fileID, title, collection, error });
|
||||
const prior = ledger.get(fileID);
|
||||
ledger.set(fileID, {
|
||||
@@ -681,7 +682,7 @@ const runLockedBackup = async (
|
||||
}
|
||||
} catch (err) {
|
||||
log(
|
||||
`FAILED verifying original ${file.metadata.title}: ${errorMessage(err)}`,
|
||||
`FAILED verifying original ${file.metadata.title}: ${describeError(err)}`,
|
||||
);
|
||||
recordFailure(
|
||||
file.id,
|
||||
@@ -717,7 +718,9 @@ const runLockedBackup = async (
|
||||
}
|
||||
downloaded++;
|
||||
} catch (err) {
|
||||
log(`FAILED original ${file.metadata.title}: ${errorMessage(err)}`);
|
||||
log(
|
||||
`FAILED original ${file.metadata.title}: ${describeError(err)}`,
|
||||
);
|
||||
recordFailure(
|
||||
file.id,
|
||||
file.metadata.title,
|
||||
@@ -785,12 +788,14 @@ const runLockedBackup = async (
|
||||
// instead and is recorded as failed. The next run fetches its ML data
|
||||
// again because none is cached.
|
||||
if (includeOriginals) {
|
||||
let mlDataFailure: unknown;
|
||||
let mlDataError: string | undefined;
|
||||
try {
|
||||
log("Fetching ML data...");
|
||||
await lib.fetchMLData();
|
||||
} catch (err) {
|
||||
mlDataError = errorMessage(err);
|
||||
mlDataFailure = err;
|
||||
mlDataError = describeError(err);
|
||||
log(`FAILED ML data: ${mlDataError}`);
|
||||
}
|
||||
for (const file of distinct.values()) {
|
||||
@@ -814,7 +819,7 @@ const runLockedBackup = async (
|
||||
file.id,
|
||||
file.metadata.title,
|
||||
collectionName.get(file.collectionID) ?? "",
|
||||
new Error(`ML data: ${mlDataError}`),
|
||||
new Error("ML data", { cause: mlDataFailure }),
|
||||
);
|
||||
} else {
|
||||
writeSidecar(path, file, { mlData }, image);
|
||||
@@ -846,7 +851,7 @@ const runLockedBackup = async (
|
||||
downloadDirectory,
|
||||
);
|
||||
} catch (err) {
|
||||
log(`FAILED removing old album directories: ${errorMessage(err)}`);
|
||||
log(`FAILED removing old album directories: ${describeError(err)}`);
|
||||
}
|
||||
|
||||
for (const c of collections) {
|
||||
@@ -868,7 +873,9 @@ const runLockedBackup = async (
|
||||
try {
|
||||
removeStaleLinks(colDir, new Set(linkNames), downloadDirectory);
|
||||
} catch (err) {
|
||||
log(`FAILED removing old links in ${c.name}: ${errorMessage(err)}`);
|
||||
log(
|
||||
`FAILED removing old links in ${c.name}: ${describeError(err)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const metaFiles = files.map((f) => ({
|
||||
@@ -885,7 +892,7 @@ const runLockedBackup = async (
|
||||
);
|
||||
} catch (err) {
|
||||
log(
|
||||
`FAILED symlink ${c.name}/${linkName}: ${errorMessage(err)}`,
|
||||
`FAILED symlink ${c.name}/${linkName}: ${describeError(err)}`,
|
||||
);
|
||||
recordFailure(
|
||||
link.file.id,
|
||||
|
||||
Reference in New Issue
Block a user