Преглед изворни кода

add support of `filenameConflictAction` with:
- WebDAV
- GitHub
- Google Drive
(`prompt` is not supported yet)

Gildas пре 2 година
родитељ
комит
8e80746bd8
3 измењених фајлова са 164 додато и 42 уклоњено
  1. 63 26
      src/core/bg/downloads.js
  2. 48 4
      src/lib/gdrive/gdrive.js
  3. 53 12
      src/lib/github/github.js

+ 63 - 26
src/core/bg/downloads.js

@@ -42,6 +42,7 @@ 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 REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
 
 const gDrive = new GDrive(GDRIVE_CLIENT_ID, GDRIVE_CLIENT_KEY, SCOPES);
@@ -135,15 +136,16 @@ async function downloadContent(contents, tab, incognito, message) {
 	try {
 		let response;
 		if (message.saveWithWebDAV) {
-			response = await saveWithWebDAV(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.webDAVURL, message.webDAVUser, message.webDAVPassword);
+			response = await saveWithWebDAV(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.webDAVURL, message.webDAVUser, message.webDAVPassword, message.filenameConflictAction);
 		} else if (message.saveToGDrive) {
 			await saveToGDrive(message.taskId, encodeSharpCharacter(message.filename), new Blob(contents, { type: MIMETYPE_HTML }), {
 				forceWebAuthFlow: message.forceWebAuthFlow
 			}, {
-				onProgress: (offset, size) => ui.onUploadProgress(tab.id, offset, size)
+				onProgress: (offset, size) => ui.onUploadProgress(tab.id, offset, size),
+				filenameConflictAction: message.filenameConflictAction
 			});
 		} else if (message.saveToGitHub) {
-			response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch);
+			response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, message.filenameConflictAction);
 			await response.pushPromise;
 		} else if (message.saveWithCompanion) {
 			await companion.save({
@@ -212,10 +214,10 @@ async function getAuthInfo(authOptions, force) {
 	return authInfo;
 }
 
-async function saveToGitHub(taskId, filename, content, githubToken, githubUser, githubRepository, githubBranch) {
+async function saveToGitHub(taskId, filename, content, githubToken, githubUser, githubRepository, githubBranch, filenameConflictAction) {
 	const taskInfo = business.getTaskInfo(taskId);
 	if (!taskInfo || !taskInfo.cancelled) {
-		const pushInfo = pushGitHub(githubToken, githubUser, githubRepository, githubBranch, filename, content);
+		const pushInfo = pushGitHub(githubToken, githubUser, githubRepository, githubBranch, filename, content, { filenameConflictAction });
 		business.setCancelCallback(taskId, pushInfo.cancelPush);
 		try {
 			await (await pushInfo).pushPromise;
@@ -226,7 +228,7 @@ async function saveToGitHub(taskId, filename, content, githubToken, githubUser,
 	}
 }
 
-async function saveWithWebDAV(taskId, filename, content, url, username, password, retry = true) {
+async function saveWithWebDAV(taskId, filename, content, url, username, password, filenameConflictAction) {
 	const taskInfo = business.getTaskInfo(taskId);
 	const controller = new AbortController();
 	const { signal } = controller;
@@ -237,33 +239,68 @@ async function saveWithWebDAV(taskId, filename, content, url, username, password
 	if (!taskInfo || !taskInfo.cancelled) {
 		business.setCancelCallback(taskId, () => controller.abort());
 		try {
-			const response = await sendRequest(url + filename, "PUT", content);
-			if (response.status == 404 && 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");
+			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);
+					}
+				} 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) {
-							const response = await sendRequest(url + path, "MKCOL");
-							if (response.status >= 400) {
-								throw new Error("Error " + response.status + " (WebDAV)");
+							return saveWithWebDAV(taskId, filename, content, url, username, password, filenameConflictAction);
+						} else {
+							indexFilename++;
+						}
+					}
+				} else if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
+					return response;
+				}
+			} else if (response.status == 404) {
+				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 += "/";
 						}
-						path += "/";
 					}
 				}
-				if (retry) {
-					return saveWithWebDAV(taskId, filename, content, url, username, password, false);
+				response = await sendRequest(url + filename, "PUT", content);
+				if (response.status >= 400) {
+					throw new Error("Error " + response.status);
 				} else {
-					throw new Error("Error 404 (WebDAV)");
+					return response;
 				}
 			} else if (response.status >= 400) {
-				throw new Error("Error " + response.status + " (WebDAV)");
-			} else {
-				return response;
+				throw new Error("Error " + response.status);
 			}
 		} catch (error) {
 			if (error.name != "AbortError") {
@@ -346,7 +383,7 @@ async function downloadPage(pageData, options) {
 				if (url.startsWith("/")) {
 					url = downloadData.filename.substring(1);
 				}
-				url = "file:///" + encodeSharpCharacter(downloadData.filename);
+				url = "file:///" + encodeSharpCharacter(url);
 			}
 			return { url };
 		}

+ 48 - 4
src/lib/gdrive/gdrive.js

@@ -28,6 +28,9 @@ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
 const REVOKE_ACCESS_URL = "https://accounts.google.com/o/oauth2/revoke";
 const GDRIVE_URL = "https://www.googleapis.com/drive/v3/files";
 const GDRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3/files";
+const CONFLICT_ACTION_UNIQUIFY = "uniquify";
+const CONFLICT_ACTION_OVERWRITE = "overwrite";
+const CONFLICT_ACTION_SKIP = "skip";
 
 class GDrive {
 	constructor(clientId, clientKey, scopes) {
@@ -136,7 +139,8 @@ class GDrive {
 			file: blob,
 			parents: [parentFolderId],
 			filename,
-			onProgress: options.onProgress
+			onProgress: options.onProgress,
+			filenameConflictAction: options.filenameConflictAction
 		});
 		try {
 			if (setCancelCallback) {
@@ -168,10 +172,50 @@ class MediaUploader {
 		this.token = options.token;
 		this.offset = 0;
 		this.chunkSize = options.chunkSize || 512 * 1024;
+		this.filenameConflictAction = options.filenameConflictAction;
 	}
-	async upload() {
-		const httpResponse = getResponse(await fetch(GDRIVE_UPLOAD_URL + "?uploadType=resumable", {
-			method: "POST",
+	async upload(indexFilename = 1) {
+		let method = "POST";
+		let fileId;
+		const httpListResponse = getResponse(await fetch(GDRIVE_URL + `?q=name = '${this.metadata.name}' and trashed != true and '${this.metadata.parents[0]}' in parents`, {
+			headers: {
+				"Authorization": "Bearer " + this.token,
+				"Content-Type": "application/json"
+			}
+		}));
+		const response = await httpListResponse.json();
+		if (response.files.length) {
+			if (this.filenameConflictAction == CONFLICT_ACTION_OVERWRITE) {
+				method = "PATCH";
+				fileId = response.files[0].id;
+				this.metadata.parents = null;
+			} else if (this.filenameConflictAction == CONFLICT_ACTION_UNIQUIFY) {
+				let nameWithoutExtension = this.metadata.name;
+				let extension = "";
+				const dotIndex = this.metadata.name.lastIndexOf(".");
+				if (dotIndex > -1) {
+					nameWithoutExtension = this.metadata.name.substring(0, dotIndex);
+					extension = this.metadata.name.substring(dotIndex + 1);
+				}
+				const name = nameWithoutExtension + " (" + indexFilename + ")." + extension;
+				const httpResponse = getResponse(await fetch(GDRIVE_URL + `?q=name = '${name}' and trashed != true and '${this.metadata.parents[0]}' in parents`, {
+					headers: {
+						"Authorization": "Bearer " + this.token,
+						"Content-Type": "application/json"
+					}
+				}));
+				const response = await httpResponse.json();
+				if (response.files.length) {
+					return this.upload(indexFilename + 1);
+				} else {
+					this.metadata.name = name;
+				}
+			} else if (this.filenameConflictAction == CONFLICT_ACTION_SKIP) {
+				return {};
+			}
+		}
+		const httpResponse = getResponse(await fetch(GDRIVE_UPLOAD_URL + (fileId ? "/" + fileId : "") + "?uploadType=resumable", {
+			method,
 			headers: {
 				"Authorization": "Bearer " + this.token,
 				"Content-Type": "application/json",

+ 53 - 12
src/lib/github/github.js

@@ -23,11 +23,15 @@
 
 /* global fetch, btoa, AbortController */
 
+const CONFLICT_ACTION_SKIP = "skip";
+const CONFLICT_ACTION_UNIQUIFY = "uniquify";
+const CONFLICT_ACTION_OVERWRITE = "overwrite";
+
 export { pushGitHub };
 
 let pendingPush;
 
-async function pushGitHub(token, userName, repositoryName, branchName, path, content) {
+async function pushGitHub(token, userName, repositoryName, branchName, path, content, { filenameConflictAction } = {}) {
 	while (pendingPush) {
 		await pendingPush;
 	}
@@ -45,18 +49,46 @@ async function pushGitHub(token, userName, repositoryName, branchName, path, con
 		pushPromise: pendingPush
 	};
 
-	async function createContent({ path, content, message = "" }, signal) {
+	async function createContent({ path, content, message = "", sha }, signal) {
+		const headers = new Map([
+			["Authorization", `Bearer ${token}`],
+			["Accept", "application/vnd.github+json"],
+			["X-GitHub-Api-Version", "2022-11-28"]
+		]);
 		try {
-			const response = await fetch(`https://api.github.com/repos/${userName}/${repositoryName}/contents/${path}`, {
-				method: "PUT",
-				headers: new Map([
-					["Authorization", `token ${token}`],
-					["Accept", "application/vnd.github.v3+json"]
-				]),
-				body: JSON.stringify({ content: btoa(unescape(encodeURIComponent(content))), message, branch: branchName }),
-				signal
-			});
+			const response = await fetchContentData("PUT", JSON.stringify({ content: btoa(unescape(encodeURIComponent(content))), message, branch: branchName, sha }));
 			const responseData = await response.json();
+			if (response.status == 422) {
+				if (filenameConflictAction == CONFLICT_ACTION_OVERWRITE) {
+					const response = await fetchContentData();
+					const responseData = await response.json();
+					const sha = responseData.sha;
+					return createContent({ path, content, message, sha }, signal);
+				} else if (filenameConflictAction == CONFLICT_ACTION_UNIQUIFY) {
+					let pathWithoutExtension = path;
+					let extension = "";
+					const dotIndex = path.lastIndexOf(".");
+					if (dotIndex > -1) {
+						pathWithoutExtension = path.substring(0, dotIndex);
+						extension = path.substring(dotIndex + 1);
+					}
+					let saved = false;
+					let indexFilename = 1;
+					while (!saved) {
+						path = pathWithoutExtension + " (" + indexFilename + ")." + extension;
+						const response = await fetchContentData();
+						if (response.status == 404) {
+							return createContent({ path, content, message }, signal);
+						} else {
+							indexFilename++;
+						}
+					}
+				} else if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
+					return responseData;
+				} else {
+					throw new Error("File already exists");
+				}
+			}
 			if (response.status < 400) {
 				return responseData;
 			} else {
@@ -67,5 +99,14 @@ async function pushGitHub(token, userName, repositoryName, branchName, path, con
 				throw error;
 			}
 		}
+
+		function fetchContentData(method = "GET", body) {
+			return fetch(`https://api.github.com/repos/${userName}/${repositoryName}/contents/${path}`, {
+				method,
+				headers,
+				body,
+				signal
+			});
+		}
 	}
-}
+}