mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
web: improve handling of errors during upload
This commit is contained in:
committed by
Abdullah Atta
parent
5cc9c6e3e4
commit
0f9452f6ca
@@ -37,6 +37,7 @@ import { toBlob } from "@notesnook-importer/core/dist/src/utils/stream";
|
||||
import { Cipher, OutputFormat, SerializedKey } from "@notesnook/crypto";
|
||||
import { IDataType } from "hash-wasm/dist/lib/util";
|
||||
import { IndexedDBKVStore } from "./key-value";
|
||||
import FileHandle from "@notesnook/streamable-fs/dist/src/filehandle";
|
||||
|
||||
const ABYTES = 17;
|
||||
const CHUNK_SIZE = 512 * 1024;
|
||||
@@ -229,12 +230,17 @@ async function uploadFile(filename: string, requestOptions: RequestOptions) {
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
throw new S3Error("Could not initiate multi-part upload.", e);
|
||||
throw new WrappedError("Could not initiate multi-part upload.", e);
|
||||
});
|
||||
|
||||
uploadId = initiateMultiPartUpload.data.uploadId;
|
||||
const { parts } = initiateMultiPartUpload.data;
|
||||
|
||||
if (!parts)
|
||||
throw new Error(
|
||||
"Could not initiate multi-part upload: invalid response."
|
||||
);
|
||||
|
||||
await fileHandle.addAdditionalData("uploadId", uploadId);
|
||||
|
||||
const onUploadProgress = (ev: AxiosProgressEvent) => {
|
||||
@@ -267,7 +273,7 @@ async function uploadFile(filename: string, requestOptions: RequestOptions) {
|
||||
onUploadProgress
|
||||
})
|
||||
.catch((e) => {
|
||||
throw new S3Error(`Failed to upload part at offset ${i}`, e);
|
||||
throw new WrappedError(`Failed to upload part at offset ${i}`, e);
|
||||
});
|
||||
|
||||
if (!response.headers.etag || typeof response.headers.etag !== "string")
|
||||
@@ -297,8 +303,9 @@ async function uploadFile(filename: string, requestOptions: RequestOptions) {
|
||||
signal
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
throw new S3Error("Could not complete multi-part upload.", e);
|
||||
.catch(async (e) => {
|
||||
await resetUpload(fileHandle);
|
||||
throw new WrappedError("Could not complete multi-part upload.", e);
|
||||
});
|
||||
|
||||
await fileHandle.addAdditionalData("uploaded", true);
|
||||
@@ -310,13 +317,32 @@ async function uploadFile(filename: string, requestOptions: RequestOptions) {
|
||||
return true;
|
||||
} catch (e) {
|
||||
reportProgress(undefined, { type: "upload", hash: filename });
|
||||
if (e instanceof S3Error) e.handle();
|
||||
else handleS3Error(e);
|
||||
|
||||
const error = toS3Error(e);
|
||||
if (
|
||||
[
|
||||
"NoSuchKey",
|
||||
"NoSuchUpload",
|
||||
"IncompleteBody",
|
||||
"InternalError",
|
||||
"InvalidObjectState",
|
||||
"InvalidPart",
|
||||
"InvalidPartOrder",
|
||||
"SignatureDoesNotMatch"
|
||||
].includes(error.Code)
|
||||
)
|
||||
await resetUpload(fileHandle);
|
||||
showError(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetUpload(fileHandle: FileHandle) {
|
||||
await fileHandle.addAdditionalData("uploadId", undefined);
|
||||
await fileHandle.addAdditionalData("uploadedChunks", undefined);
|
||||
await fileHandle.addAdditionalData("uploadedBytes", undefined);
|
||||
await fileHandle.addAdditionalData("uploaded", false);
|
||||
}
|
||||
|
||||
async function checkUpload(filename: string) {
|
||||
if ((await getUploadedFileSize(filename)) <= 0) {
|
||||
const error = `Upload verification failed: file size is 0. Please upload this file again. (File hash: ${filename})`;
|
||||
@@ -418,7 +444,7 @@ async function downloadFile(filename: string, requestOptions: RequestOptions) {
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
handleS3Error(e, "Could not download file");
|
||||
showError(toS3Error(e), "Could not download file");
|
||||
reportProgress(undefined, { type: "download", hash: filename });
|
||||
return false;
|
||||
}
|
||||
@@ -477,7 +503,7 @@ async function deleteFile(filename: string, requestOptions: RequestOptions) {
|
||||
if (result) await streamablefs.deleteFile(filename);
|
||||
return result;
|
||||
} catch (e) {
|
||||
handleS3Error(e, "Could not delete file");
|
||||
showError(toS3Error(e), "Could not delete file");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -544,46 +570,62 @@ function cancellable(
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function parseS3Error(data: ArrayBuffer | unknown) {
|
||||
if (!(data instanceof ArrayBuffer)) {
|
||||
return {
|
||||
Code: "UNKNOWN",
|
||||
Message: typeof data === "object" ? JSON.stringify(data) : data
|
||||
};
|
||||
}
|
||||
const xml = new TextDecoder().decode(data);
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
const xml =
|
||||
data instanceof ArrayBuffer
|
||||
? new TextDecoder().decode(data)
|
||||
: typeof data === "string"
|
||||
? data
|
||||
: null;
|
||||
|
||||
const ErrorElement = doc.getElementsByTagName("Error")[0];
|
||||
if (!ErrorElement)
|
||||
return { Code: "Unknown", Message: "An unknown error occured." };
|
||||
const error = {
|
||||
Code: "UNKNOWN",
|
||||
Message: xml || JSON.stringify(data)
|
||||
};
|
||||
try {
|
||||
if (!xml) return error;
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
|
||||
const error: Record<string, string> = {};
|
||||
for (const child of ErrorElement.children) {
|
||||
if (child.textContent) error[child.tagName] = child.textContent;
|
||||
const ErrorElement = doc.getElementsByTagName("Error")[0];
|
||||
if (!ErrorElement) return error;
|
||||
|
||||
for (const child of ErrorElement.children) {
|
||||
if (
|
||||
child.textContent &&
|
||||
(child.tagName === "Code" || child.tagName === "Message")
|
||||
)
|
||||
error[child.tagName] = child.textContent;
|
||||
}
|
||||
return error;
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
function handleS3Error(e: unknown, message?: unknown) {
|
||||
if (axios.isAxiosError(e) && e.response?.data) {
|
||||
const error = parseS3Error(e.response.data);
|
||||
showToast("error", `${message}: [${error.Code}] ${error.Message}`);
|
||||
} else if (message && e instanceof Error) {
|
||||
showToast("error", `${message}: ${e.message}`);
|
||||
type S3Error = { Code: string; Message: string };
|
||||
function toS3Error(e: unknown): S3Error {
|
||||
if (e instanceof WrappedError) {
|
||||
const s3Error = toS3Error(e.error);
|
||||
return { ...s3Error, Message: `${e.message} ${s3Error.Message}` };
|
||||
} else if (axios.isAxiosError(e) && e.response?.data) {
|
||||
return parseS3Error(e.response.data);
|
||||
} else if (e instanceof Error) {
|
||||
showToast("error", e.message);
|
||||
return { Code: "Unknown", Message: e.message };
|
||||
} else {
|
||||
showToast("error", JSON.stringify(e));
|
||||
return { Code: "Unknown", Message: JSON.stringify(e) };
|
||||
}
|
||||
}
|
||||
|
||||
class S3Error extends Error {
|
||||
constructor(message: string, readonly error: Error) {
|
||||
function showError(error: S3Error, message?: string) {
|
||||
showToast(
|
||||
"error",
|
||||
`[${error.Code}] ${message ? message + " " : ""}${error.Message}`
|
||||
);
|
||||
}
|
||||
|
||||
class WrappedError extends Error {
|
||||
constructor(readonly message: string, readonly error: unknown) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
handle() {
|
||||
handleS3Error(this.error, this.message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user