Bläddra i källkod

add `webdav.js`

Gildas 2 år sedan
förälder
incheckning
860b3fc013
2 ändrade filer med 203 tillägg och 105 borttagningar
  1. 12 105
      src/core/bg/downloads.js
  2. 191 0
      src/lib/webdav/webdav.js

+ 12 - 105
src/core/bg/downloads.js

@@ -21,7 +21,7 @@
  *   Source.
  */
 
-/* global browser, Blob, URL, document, fetch, btoa, AbortController */
+/* global browser, Blob, URL, document, fetch */
 
 import * as config from "./config.js";
 import * as bookmarks from "./bookmarks.js";
@@ -32,6 +32,7 @@ import { launchWebAuthFlow, extractAuthCode } from "./tabs-util.js";
 import * as ui from "./../../ui/bg/index.js";
 import * as woleet from "./../../lib/woleet/woleet.js";
 import { GDrive } from "./../../lib/gdrive/gdrive.js";
+import { WebDAV } from "./../../lib/webdav/webdav.js";
 import { pushGitHub } from "./../../lib/github/github.js";
 import { download } from "./download-util.js";
 
@@ -42,8 +43,6 @@ const GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt";
 const SCOPES = ["https://www.googleapis.com/auth/drive.file"];
 const CONFLICT_ACTION_SKIP = "skip";
 const CONFLICT_ACTION_UNIQUIFY = "uniquify";
-const CONFLICT_ACTION_OVERWRITE = "overwrite";
-const CONFLICT_ACTION_PROMPT = "prompt";
 const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
 
 const gDrive = new GDrive(GDRIVE_CLIENT_ID, GDRIVE_CLIENT_KEY, SCOPES);
@@ -234,108 +233,16 @@ async function saveToGitHub(taskId, filename, content, githubToken, githubUser,
 	}
 }
 
-async function saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction, prompt, preventRetry }) {
-	const taskInfo = business.getTaskInfo(taskId);
-	const controller = new AbortController();
-	const { signal } = controller;
-	const authorization = "Basic " + btoa(username + ":" + password);
-	if (!url.endsWith("/")) {
-		url += "/";
-	}
-	if (!taskInfo || !taskInfo.cancelled) {
-		business.setCancelCallback(taskId, () => controller.abort());
-		try {
-			let response = await sendRequest(url + filename, "HEAD");
-			if (response.status == 200) {
-				if (filenameConflictAction == CONFLICT_ACTION_OVERWRITE) {
-					response = await sendRequest(url + filename, "PUT", content);
-					if (response.status == 201) {
-						return response;
-					} else if (response.status >= 400) {
-						response = await sendRequest(url + filename, "DELETE");
-						if (response.status >= 400) {
-							throw new Error("Error " + response.status);
-						}
-						return saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction, prompt });
-					}
-				} else if (filenameConflictAction == CONFLICT_ACTION_UNIQUIFY) {
-					let filenameWithoutExtension = filename;
-					let extension = "";
-					const dotIndex = filename.lastIndexOf(".");
-					if (dotIndex > -1) {
-						filenameWithoutExtension = filename.substring(0, dotIndex);
-						extension = filename.substring(dotIndex + 1);
-					}
-					let saved = false;
-					let indexFilename = 1;
-					while (!saved) {
-						filename = filenameWithoutExtension + " (" + indexFilename + ")." + extension;
-						const response = await sendRequest(url + filename, "HEAD");
-						if (response.status == 404) {
-							return saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction, prompt });
-						} else {
-							indexFilename++;
-						}
-					}
-				} else if (filenameConflictAction == CONFLICT_ACTION_PROMPT) {
-					if (prompt) {
-						filename = await prompt(filename);
-						if (filename) {
-							return saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction, prompt });
-						} else {
-							return response;
-						}
-					} else {
-						return saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction: CONFLICT_ACTION_UNIQUIFY });
-					}
-				} else if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
-					return response;
-				}
-			} else if (response.status == 404) {
-				response = await sendRequest(url + filename, "PUT", content);
-				if (response.status >= 400 && !preventRetry) {
-					if (filename.includes("/")) {
-						const filenameParts = filename.split(/\/+/);
-						filenameParts.pop();
-						let path = "";
-						for (const filenamePart of filenameParts) {
-							if (filenamePart) {
-								path += filenamePart;
-								const response = await sendRequest(url + path, "PROPFIND");
-								if (response.status == 404) {
-									const response = await sendRequest(url + path, "MKCOL");
-									if (response.status >= 400) {
-										throw new Error("Error " + response.status);
-									}
-								}
-								path += "/";
-							}
-						}
-						return saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction, prompt, preventRetry: true });
-					} else {
-						throw new Error("Error " + response.status);
-					}
-				} else {
-					return response;
-				}
-			} else if (response.status >= 400) {
-				throw new Error("Error " + response.status);
-			}
-		} catch (error) {
-			if (error.name != "AbortError") {
-				throw new Error(error.message + " (WebDAV)");
-			}
-		}
-	}
-
-	function sendRequest(url, method, body) {
-		const headers = {
-			"Authorization": authorization
-		};
-		if (body) {
-			headers["Content-Type"] = "text/html";
+async function saveWithWebDAV(taskId, filename, content, url, username, password, { filenameConflictAction, prompt }) {
+	try {
+		const taskInfo = business.getTaskInfo(taskId);
+		if (!taskInfo || !taskInfo.cancelled) {
+			const client = new WebDAV(url, username, password);
+			business.setCancelCallback(taskId, () => client.abort());
+			return await client.upload(filename, content, { filenameConflictAction, prompt });
 		}
-		return fetch(url, { method, headers, signal, body, credentials: "omit" });
+	} catch (error) {
+		throw new Error(error.message + " (WebDAV)");
 	}
 }
 
@@ -344,7 +251,7 @@ async function saveToGDrive(taskId, filename, blob, authOptions, uploadOptions)
 		await getAuthInfo(authOptions);
 		const taskInfo = business.getTaskInfo(taskId);
 		if (!taskInfo || !taskInfo.cancelled) {
-			return gDrive.upload(filename, blob, uploadOptions, callback => business.setCancelCallback(taskId, callback));
+			return await gDrive.upload(filename, blob, uploadOptions, callback => business.setCancelCallback(taskId, callback));
 		}
 	}
 	catch (error) {

+ 191 - 0
src/lib/webdav/webdav.js

@@ -0,0 +1,191 @@
+/*
+ * Copyright 2010-2020 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+/* global fetch, btoa, AbortController */
+
+const EMPTY_STRING = "";
+const CONFLICT_ACTION_SKIP = "skip";
+const CONFLICT_ACTION_UNIQUIFY = "uniquify";
+const CONFLICT_ACTION_OVERWRITE = "overwrite";
+const CONFLICT_ACTION_PROMPT = "prompt";
+const BASIC_PREFIX_AUTHORIZATION = "Basic ";
+const AUTHORIZATION_HEADER = "Authorization";
+const AUTHORIZATION_SEPARATOR = ":";
+const DIRECTORY_SEPARATOR = "/";
+const EXTENSION_SEPARATOR = ".";
+const ERROR_PREFIX_MESSAGE = "Error ";
+const INDEX_FILENAME_PREFIX = " (";
+const INDEX_FILENAME_SUFFIX = ")";
+const INDEX_FILENAME_REGEXP = /\s\((\d+)\)$/;
+const ABORT_ERROR_NAME = "AbortError";
+const HEAD_METHOD = "HEAD";
+const PUT_METHOD = "PUT";
+const DELETE_METHOD = "DELETE";
+const PROPFIND_METHOD = "PROPFIND";
+const MKCOL_METHOD = "MKCOL";
+const CONTENT_TYPE_HEADER = "Content-Type";
+const HTML_CONTENT_TYPE = "text/html";
+const CREDENTIALS_PARAMETER = "omit";
+const FOUND_STATUS = 200;
+const CREATED_STATUS = 201;
+const NOT_FOUND_STATUS = 404;
+const MIN_ERROR_STATUS = 400;
+
+export {
+	WebDAV
+};
+
+class WebDAV {
+	constructor(url, username, password) {
+		if (!url.endsWith(DIRECTORY_SEPARATOR)) {
+			url += DIRECTORY_SEPARATOR;
+		}
+		this.url = url;
+		this.authorization = BASIC_PREFIX_AUTHORIZATION + btoa(username + AUTHORIZATION_SEPARATOR + password);
+	}
+
+	upload(filename, content, options) {
+		this.controller = new AbortController();
+		options.signal = this.controller.signal;
+		options.authorization = this.authorization;
+		options.url = this.url;
+		return upload(filename, content, options);
+	}
+
+	abort() {
+		if (this.controller) {
+			this.controller.abort();
+		}
+	}
+}
+
+async function upload(filename, content, options) {
+	const { authorization, filenameConflictAction, prompt, signal, preventRetry } = options;
+	let { url } = options;
+	try {
+		let response = await sendRequest(filename, HEAD_METHOD);
+		if (response.status == FOUND_STATUS) {
+			if (filenameConflictAction == CONFLICT_ACTION_OVERWRITE) {
+				response = await sendRequest(filename, PUT_METHOD, content);
+				if (response.status == CREATED_STATUS) {
+					return response;
+				} else if (response.status >= MIN_ERROR_STATUS) {
+					response = await sendRequest(filename, DELETE_METHOD);
+					if (response.status >= MIN_ERROR_STATUS) {
+						throw new Error(ERROR_PREFIX_MESSAGE + response.status);
+					}
+					return await upload(filename, content, options);
+				}
+			} else if (filenameConflictAction == CONFLICT_ACTION_UNIQUIFY || (filenameConflictAction == CONFLICT_ACTION_PROMPT && !prompt)) {
+				const { filenameWithoutExtension, extension, indexFilename } = splitFilename(filename);
+				options.indexFilename = indexFilename + 1;
+				return await upload(getFilename(filenameWithoutExtension, extension), content, options);
+			} else if (filenameConflictAction == CONFLICT_ACTION_PROMPT) {
+				filename = await prompt(filename);
+				return filename ? upload(filename, content, options) : response;
+			} else if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
+				return response;
+			}
+		} else if (response.status == NOT_FOUND_STATUS) {
+			response = await sendRequest(filename, PUT_METHOD, content);
+			if (response.status >= MIN_ERROR_STATUS && !preventRetry) {
+				if (filename.includes(DIRECTORY_SEPARATOR)) {
+					await createDirectories();
+					options.preventRetry = true;
+					return await upload(filename, content, options);
+				} else {
+					throw new Error(ERROR_PREFIX_MESSAGE + response.status);
+				}
+			} else {
+				return response;
+			}
+		} else if (response.status >= MIN_ERROR_STATUS) {
+			throw new Error(ERROR_PREFIX_MESSAGE + response.status);
+		}
+	} catch (error) {
+		if (error.name != ABORT_ERROR_NAME) {
+			throw error;
+		}
+	}
+
+	function sendRequest(path, method, body) {
+		const headers = {
+			[AUTHORIZATION_HEADER]: authorization
+		};
+		if (body) {
+			headers[CONTENT_TYPE_HEADER] = HTML_CONTENT_TYPE;
+		}
+		return fetch(url + path, { method, headers, signal, body, credentials: CREDENTIALS_PARAMETER });
+	}
+
+	function splitFilename(filename) {
+		let filenameWithoutExtension = filename;
+		let extension = EMPTY_STRING;
+		const indexExtensionSeparator = filename.lastIndexOf(EXTENSION_SEPARATOR);
+		if (indexExtensionSeparator > -1) {
+			filenameWithoutExtension = filename.substring(0, indexExtensionSeparator);
+			extension = filename.substring(indexExtensionSeparator + 1);
+		}
+		let indexFilename;
+		({ filenameWithoutExtension, indexFilename } = extractIndexFilename(filenameWithoutExtension));
+		return { filenameWithoutExtension, extension, indexFilename };
+	}
+
+	function extractIndexFilename(filenameWithoutExtension) {
+		const indexFilenameMatch = filenameWithoutExtension.match(INDEX_FILENAME_REGEXP);
+		let indexFilename = 0;
+		if (indexFilenameMatch && indexFilenameMatch.length > 1) {
+			const parsedIndexFilename = Number(indexFilenameMatch[indexFilenameMatch.length - 1]);
+			if (!Number.isNaN(parsedIndexFilename)) {
+				indexFilename = parsedIndexFilename;
+				filenameWithoutExtension = filenameWithoutExtension.replace(INDEX_FILENAME_REGEXP, EMPTY_STRING);
+			}
+		}
+		return { filenameWithoutExtension, indexFilename };
+	}
+
+	function getFilename(filenameWithoutExtension, extension) {
+		return filenameWithoutExtension +
+			INDEX_FILENAME_PREFIX + options.indexFilename + INDEX_FILENAME_SUFFIX +
+			(extension ? EXTENSION_SEPARATOR + extension : EMPTY_STRING);
+	}
+
+	async function createDirectories() {
+		const filenameParts = filename.split(DIRECTORY_SEPARATOR);
+		filenameParts.pop();
+		let path = EMPTY_STRING;
+		for (const filenamePart of filenameParts) {
+			if (filenamePart) {
+				path += filenamePart;
+				const response = await sendRequest(path, PROPFIND_METHOD);
+				if (response.status == NOT_FOUND_STATUS) {
+					const response = await sendRequest(path, MKCOL_METHOD);
+					if (response.status >= MIN_ERROR_STATUS) {
+						throw new Error(ERROR_PREFIX_MESSAGE + response.status);
+					}
+				}
+				path += DIRECTORY_SEPARATOR;
+			}
+		}
+	}
+}