Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2cd64cb40 |
@@ -142,16 +142,15 @@ matters.
|
||||
| `3` | There is no usable session: none is saved, the saved one is corrupt, or the server no longer accepts it. Run `quak login` as the user the cron job runs as. |
|
||||
|
||||
`<message>` is the error's message, followed by the message of each error that
|
||||
caused it. Under Node, as `yarn quak` runs quak, `fetch` reports every failure
|
||||
below HTTP as just `fetch failed` and gives the reason as its cause, so a run
|
||||
that could not reach the server prints, for example,
|
||||
caused it. Node's `fetch` reports every failure below HTTP as just
|
||||
`fetch failed` and gives the reason as its cause, so a run that could not reach
|
||||
the server prints, for example,
|
||||
`quak: fetch failed: connect ECONNREFUSED 1.2.3.4:443`. When the server has
|
||||
several addresses and none of them could be reached, the line gives the reason
|
||||
for each, separated by `, `. A file whose download failed that way is in
|
||||
`failures.json` with that error, as a transient failure. So is each file with no
|
||||
ML data cached when the ML data fetch failed that way, its error starting
|
||||
`ML data: `. The `~/bin/quak` that `make install` builds runs under bun, and
|
||||
prints bun's own wording for such a failure.
|
||||
`ML data: `.
|
||||
|
||||
After a `1`, the next run fetches each missing original again, fetches the ML
|
||||
data that is not cached, and rebuilds the album links. An album quak could not
|
||||
|
||||
+10
-12
@@ -245,6 +245,8 @@ const classify = (err: unknown): FailureClass => {
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
const errorMessage = (err: unknown): string => describeError(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.
|
||||
@@ -471,7 +473,7 @@ const imageMetadataFor = async (
|
||||
const bytes = await readFile(originalPath);
|
||||
return { imageMetadata: extractImageMetadata(bytes) ?? {} };
|
||||
} catch (err) {
|
||||
return { imageMetadataError: describeError(err) };
|
||||
return { imageMetadataError: errorMessage(err) };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -611,7 +613,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 = describeError(err);
|
||||
const error = errorMessage(err);
|
||||
errors.push({ fileID, title, collection, error });
|
||||
const prior = ledger.get(fileID);
|
||||
ledger.set(fileID, {
|
||||
@@ -682,7 +684,7 @@ const runLockedBackup = async (
|
||||
}
|
||||
} catch (err) {
|
||||
log(
|
||||
`FAILED verifying original ${file.metadata.title}: ${describeError(err)}`,
|
||||
`FAILED verifying original ${file.metadata.title}: ${errorMessage(err)}`,
|
||||
);
|
||||
recordFailure(
|
||||
file.id,
|
||||
@@ -718,9 +720,7 @@ const runLockedBackup = async (
|
||||
}
|
||||
downloaded++;
|
||||
} catch (err) {
|
||||
log(
|
||||
`FAILED original ${file.metadata.title}: ${describeError(err)}`,
|
||||
);
|
||||
log(`FAILED original ${file.metadata.title}: ${errorMessage(err)}`);
|
||||
recordFailure(
|
||||
file.id,
|
||||
file.metadata.title,
|
||||
@@ -795,7 +795,7 @@ const runLockedBackup = async (
|
||||
await lib.fetchMLData();
|
||||
} catch (err) {
|
||||
mlDataFailure = err;
|
||||
mlDataError = describeError(err);
|
||||
mlDataError = errorMessage(err);
|
||||
log(`FAILED ML data: ${mlDataError}`);
|
||||
}
|
||||
for (const file of distinct.values()) {
|
||||
@@ -851,7 +851,7 @@ const runLockedBackup = async (
|
||||
downloadDirectory,
|
||||
);
|
||||
} catch (err) {
|
||||
log(`FAILED removing old album directories: ${describeError(err)}`);
|
||||
log(`FAILED removing old album directories: ${errorMessage(err)}`);
|
||||
}
|
||||
|
||||
for (const c of collections) {
|
||||
@@ -873,9 +873,7 @@ const runLockedBackup = async (
|
||||
try {
|
||||
removeStaleLinks(colDir, new Set(linkNames), downloadDirectory);
|
||||
} catch (err) {
|
||||
log(
|
||||
`FAILED removing old links in ${c.name}: ${describeError(err)}`,
|
||||
);
|
||||
log(`FAILED removing old links in ${c.name}: ${errorMessage(err)}`);
|
||||
}
|
||||
|
||||
const metaFiles = files.map((f) => ({
|
||||
@@ -892,7 +890,7 @@ const runLockedBackup = async (
|
||||
);
|
||||
} catch (err) {
|
||||
log(
|
||||
`FAILED symlink ${c.name}/${linkName}: ${describeError(err)}`,
|
||||
`FAILED symlink ${c.name}/${linkName}: ${errorMessage(err)}`,
|
||||
);
|
||||
recordFailure(
|
||||
link.file.id,
|
||||
|
||||
@@ -136,8 +136,8 @@ const streamDecrypt = async (
|
||||
// is reported as the truncation it almost always is, with
|
||||
// the authentication failure kept as the error's cause.
|
||||
// The message leaves that failure to the cause, which
|
||||
// `describeError` appends to what `quak backup` prints and
|
||||
// records. Only the pull is guarded: a sink failure on a
|
||||
// `describeError` appends when the error is printed or
|
||||
// recorded. Only the pull is guarded: a sink failure on a
|
||||
// chunk that did authenticate is a disk error, not a
|
||||
// truncation.
|
||||
let pulled;
|
||||
|
||||
+9
-22
@@ -79,35 +79,22 @@ export const causeChain = (
|
||||
// has several addresses and every one fails, the cause is an `AggregateError`
|
||||
// with an empty message and one error per address in `errors`, so an
|
||||
// `AggregateError` is given as the messages of its errors, joined with ", ".
|
||||
// A cause that is not an object, such as a thrown string, is given as
|
||||
// `String(cause)`. Each message is trimmed of the white space around it, as
|
||||
// OpenSSL ends its own with a newline. A message the text already holds is
|
||||
// left out.
|
||||
// A cause whose message is already in the text before it is left out.
|
||||
export const describeError = (err: unknown): string => {
|
||||
if (!(err instanceof Error)) return String(err);
|
||||
const { chain } = causeChain(err);
|
||||
const messages = chain
|
||||
.slice(1)
|
||||
.map((cause) =>
|
||||
let text = err.message;
|
||||
for (const cause of causeChain(err).chain.slice(1)) {
|
||||
const message =
|
||||
cause instanceof AggregateError
|
||||
? cause.errors
|
||||
.map((e: unknown) =>
|
||||
(e instanceof Error ? e.message : String(e)).trim(),
|
||||
e instanceof Error ? e.message : String(e),
|
||||
)
|
||||
.join(", ")
|
||||
: (cause as { message?: unknown }).message,
|
||||
);
|
||||
// `causeChain` stops at a cause that is not an object, so that one is read
|
||||
// from the last error on the chain.
|
||||
const last = chain[chain.length - 1] as { cause?: unknown };
|
||||
if (last.cause !== undefined && typeof last.cause !== "object") {
|
||||
messages.push(String(last.cause));
|
||||
}
|
||||
let text = err.message.trim();
|
||||
for (const message of messages) {
|
||||
if (typeof message !== "string") continue;
|
||||
const trimmed = message.trim();
|
||||
if (!text.includes(trimmed)) text += `: ${trimmed}`;
|
||||
: (cause as { message?: unknown }).message;
|
||||
if (typeof message === "string" && !text.includes(message)) {
|
||||
text += `: ${message}`;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
@@ -102,24 +102,6 @@ describe("run", () => {
|
||||
expect(result.stderr).toBe("quak: download: file 7: read ECONNRESET\n");
|
||||
});
|
||||
|
||||
// OpenSSL ends its message for a failed TLS connection, such as one to a
|
||||
// server that answers in plain HTTP, with a newline. The printed line is
|
||||
// still one line.
|
||||
it("prints a cause's message without the white space around it", async () => {
|
||||
const wrongVersion = new TypeError("fetch failed", {
|
||||
cause: Object.assign(
|
||||
new Error(
|
||||
"C0B73A55A47F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:\n",
|
||||
),
|
||||
{ code: "ERR_SSL_WRONG_VERSION_NUMBER" },
|
||||
),
|
||||
});
|
||||
const result = await runToExit(Promise.reject(wrongVersion));
|
||||
expect(result.stderr).toBe(
|
||||
"quak: fetch failed: C0B73A55A47F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("prints a thrown value that is not an Error", async () => {
|
||||
const result = await runToExit(Promise.reject("offline"));
|
||||
expect(result).toEqual({
|
||||
@@ -129,14 +111,6 @@ describe("run", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// A cause that is not an Error, such as a thrown string, is printed as a
|
||||
// thrown value that is not an Error is.
|
||||
it("prints a cause that is not an Error", async () => {
|
||||
const wrapped = new Error("ML data", { cause: "offline" });
|
||||
const result = await runToExit(Promise.reject(wrapped));
|
||||
expect(result.stderr).toBe("quak: ML data: offline\n");
|
||||
});
|
||||
|
||||
it("on a 401 from the server says to run quak login, on one line, and exits 3", async () => {
|
||||
const result = await runToExit(
|
||||
Promise.reject(new ApiError("unauthorized", 401)),
|
||||
|
||||
Reference in New Issue
Block a user