diff --git a/src/equicordplugins/fileUpload/native.ts b/src/equicordplugins/fileUpload/native.ts index c566cab5e..d9d1eb106 100644 --- a/src/equicordplugins/fileUpload/native.ts +++ b/src/equicordplugins/fileUpload/native.ts @@ -495,6 +495,47 @@ export async function uploadToWebdav( } } +export async function createWebdavShare( + _: IpcMainInvokeEvent, + ocsUrl: string, + headers: Record, + body: string +): Promise { + if (!isValidHttpsUrl(ocsUrl)) { + return { success: false, error: "Invalid or non-HTTPS share endpoint URL" }; + } + + try { + const response = await fetch(ocsUrl, { + method: "POST", + headers, + body + }); + + const text = await response.text(); + + if (!response.ok) { + return { success: false, error: `Share creation failed: ${response.status} ${text.slice(0, 200)}` }; + } + + let data: { ocs?: { data?: { token?: string; }; }; }; + try { + data = JSON.parse(text); + } catch { + return { success: false, error: `Invalid share response: ${text.slice(0, 200)}` }; + } + + const token = data?.ocs?.data?.token; + if (!token) { + return { success: false, error: "No share token in server response" }; + } + + return { success: true, url: token }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; + } +} + export async function fetchFile( _: IpcMainInvokeEvent, diff --git a/src/equicordplugins/fileUpload/settings.tsx b/src/equicordplugins/fileUpload/settings.tsx index d26a35fda..0b211adde 100644 --- a/src/equicordplugins/fileUpload/settings.tsx +++ b/src/equicordplugins/fileUpload/settings.tsx @@ -338,6 +338,28 @@ export const settings = definePluginSettings({ default: "", hidden: true }, + webdavServerType: { + type: OptionType.SELECT, + description: "WebDAV server type", + options: [ + { label: "Nextcloud", value: "nextcloud", default: true }, + { label: "ownCloud", value: "owncloud" }, + { label: "Generic WebDAV", value: "generic" } + ], + default: "nextcloud", + hidden: true + }, + webdavShareType: { + type: OptionType.SELECT, + description: "WebDAV share link format", + options: [ + { label: "Share Page", value: "share-page", default: true }, + { label: "Direct Download", value: "direct-download" }, + { label: "Markdown Link", value: "markdown" } + ], + default: "share-page", + hidden: true + }, uploadAllowedFileTypes: { type: OptionType.STRING, description: "Comma-separated list of allowed file extensions (e.g. png,jpg,gif,mp4). Leave empty to allow all files.", @@ -847,6 +869,40 @@ export function SettingsComponent() { onChange={v => store.webdavDirectory = v} placeholder="Leave empty for root directory" /> + + v === store.webdavShareType} + select={v => { + store.webdavShareType = v; + update(); + }} + serialize={v => v} + placeholder="Select share link format" + /> + + )} )} diff --git a/src/equicordplugins/fileUpload/utils/upload.ts b/src/equicordplugins/fileUpload/utils/upload.ts index 023f7d5da..94da98de4 100644 --- a/src/equicordplugins/fileUpload/utils/upload.ts +++ b/src/equicordplugins/fileUpload/utils/upload.ts @@ -974,11 +974,106 @@ async function uploadToPixelDrain(fileBlob: Blob, filename: string): Promise { - const { webdavUrl, webdavUsername, webdavPassword, webdavDirectory } = settings.store as { - webdavUrl?: string; +function buildWebdavAuthHeader(): string | null { + const { webdavUsername, webdavPassword } = settings.store as { webdavUsername?: string; webdavPassword?: string; + }; + if (webdavUsername?.trim() && webdavPassword?.trim()) { + return `Basic ${btoa(`${webdavUsername.trim()}:${webdavPassword.trim()}`)}`; + } + return null; +} + +async function createWebdavShare(relativePath: string, serverOrigin: string, filename: string): Promise { + const { webdavServerType, webdavShareType } = settings.store as { + webdavServerType?: string; + webdavShareType?: string; + }; + + const ocsVersion = webdavServerType === "owncloud" ? "v1" : "v2"; + const ocsUrl = `${serverOrigin}/ocs/${ocsVersion}.php/apps/files_sharing/api/v1/shares?format=json`; + + const authHeader = buildWebdavAuthHeader(); + const ocsHeaders: Record = { + "OCS-APIRequest": "true", + "Content-Type": "application/x-www-form-urlencoded" + }; + if (authHeader) { + ocsHeaders.Authorization = authHeader; + } + + const body = new URLSearchParams({ + path: `/${relativePath}`, + shareType: "3", + permissions: "1" + }).toString(); + + let shareToken: string; + let shareUrl: string | undefined; + + if (Native) { + const result = await Native.createWebdavShare(ocsUrl, ocsHeaders, body); + if (!result.success) { + throw new Error(result.error || "Failed to create public share"); + } + shareToken = result.url || ""; + if (!shareToken) { + throw new Error("No share token returned from server"); + } + } else { + const response = await uploadRequestWithTimeout(ocsUrl, { + method: "POST", + headers: ocsHeaders, + body + }); + + const responseText = await response.text(); + + if (!response.ok) { + throw new Error(`Failed to create public share: ${response.status} ${responseText}`); + } + + let shareData: { ocs?: { data?: { url?: string; token?: string; }; meta?: Record; }; }; + try { + shareData = JSON.parse(responseText); + } catch { + throw new Error(`Failed to parse share response: ${responseText.slice(0, 200)}`); + } + + shareToken = shareData?.ocs?.data?.token ?? ""; + + if (!shareToken) { + const meta = shareData?.ocs?.meta; + const description = meta ? `${meta.status || "unknown"} (${meta.statuscode || "?"})` : "no token in response"; + throw new Error(`Failed to create public share: ${description}`); + } + + shareUrl = shareData.ocs?.data?.url; + } + + const shareType = webdavShareType || "share-page"; + + const sharePageUrl = shareUrl || `${serverOrigin}/s/${shareToken}`; + const directDownloadUrl = webdavServerType === "owncloud" + ? `${serverOrigin}/remote.php/dav/public-files/${shareToken}` + : `${serverOrigin}/public.php/dav/files/${shareToken}`; + + if (shareType === "direct-download") { + return directDownloadUrl; + } + + if (shareType === "markdown") { + return `[${filename}](${directDownloadUrl})`; + } + + return sharePageUrl; +} + +async function uploadToWebdav(fileBlob: Blob, filename: string): Promise { + const { webdavUrl, webdavServerType, webdavDirectory } = settings.store as { + webdavUrl?: string; + webdavServerType?: string; webdavDirectory?: string; }; @@ -986,40 +1081,51 @@ async function uploadToWebdav(fileBlob: Blob, filename: string): Promise throw new Error("WebDAV server URL is required"); } + const authHeader = buildWebdavAuthHeader(); const baseUrl = webdavUrl.replace(/\/+$/, ""); const dir = (webdavDirectory || "").replace(/^\/+|\/+$/g, ""); - const path = dir ? `/${dir}/${encodeURIComponent(filename)}` : `/${encodeURIComponent(filename)}`; - const uploadUrl = `${baseUrl}${path}`; + const relativePath = dir ? `${dir}/${filename}` : filename; + const encodedDir = dir ? dir.split("/").map(encodeURIComponent).join("/") + "/" : ""; + const uploadUrl = `${baseUrl}/${encodedDir}${encodeURIComponent(filename)}`; + const serverType = webdavServerType || "nextcloud"; - const headers: Record = { + const requestHeaders: Record = { "Content-Type": fileBlob.type || "application/octet-stream" }; - - if (webdavUsername?.trim() && webdavPassword?.trim()) { - headers.Authorization = `Basic ${btoa(`${webdavUsername.trim()}:${webdavPassword.trim()}`)}`; + if (authHeader) { + requestHeaders.Authorization = authHeader; } if (Native) { const arrayBuffer = await fileBlob.arrayBuffer(); - const result = await Native.uploadToWebdav(arrayBuffer, uploadUrl, headers); + const result = await Native.uploadToWebdav(arrayBuffer, uploadUrl, requestHeaders); if (!result.success) { throw new Error(result.error || "Upload failed"); } + } else { + const response = await uploadRequestWithTimeout(uploadUrl, { + method: "PUT", + headers: requestHeaders, + body: fileBlob + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Upload failed: ${response.status} ${errorText}`); + } + } + + if (serverType === "generic") { return uploadUrl; } - const response = await uploadRequestWithTimeout(uploadUrl, { - method: "PUT", - headers, - body: fileBlob - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Upload failed: ${response.status} ${errorText}`); + let serverOrigin: string; + try { + serverOrigin = new URL(webdavUrl).origin; + } catch { + throw new Error("Invalid WebDAV server URL"); } - - return uploadUrl; + return await createWebdavShare(relativePath, serverOrigin, filename); } async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filename: string): Promise {