FileUpload: support public shares for WebDAV (#1170)

* FileUpload: support public shares for WebDAV

* FileUpload: fix construction of webdav share url

* sono me dare no me

* tweak formatting of direct link for nc, add global option for md link
This commit is contained in:
Key 2026-06-12 22:00:26 +01:00 • committed by GitHub
parent 476a6b0ac3
commit 90b4eb14ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 224 additions and 21 deletions

View file

@ -495,6 +495,47 @@ export async function uploadToWebdav(
}
}
export async function createWebdavShare(
_: IpcMainInvokeEvent,
ocsUrl: string,
headers: Record<string, string>,
body: string
): Promise<NativeUploadResult> {
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,

View file

@ -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"
/>
<SettingsSection name="Server Type" description="Select your WebDAV server type. Nextcloud and ownCloud will create a public share link. Generic returns the raw file URL.">
<Select
options={[
{ label: "Nextcloud", value: "nextcloud", default: true },
{ label: "ownCloud", value: "owncloud" },
{ label: "Generic WebDAV", value: "generic" }
]}
isSelected={v => v === store.webdavServerType}
select={v => {
store.webdavServerType = v;
update();
}}
serialize={v => v}
placeholder="Select server type"
/>
</SettingsSection>
{store.webdavServerType !== "generic" && (
<SettingsSection name="Share Link Format" description="How to return the public share link. Share Page links to a web page; Direct Download links straight to the file; Markdown Link wraps the share page in a clickable filename.">
<Select
options={[
{ label: "Share Page", value: "share-page", default: true },
{ label: "Direct Download", value: "direct-download" },
{ label: "Markdown Link", value: "markdown" }
]}
isSelected={v => v === store.webdavShareType}
select={v => {
store.webdavShareType = v;
update();
}}
serialize={v => v}
placeholder="Select share link format"
/>
</SettingsSection>
)}
</SettingGroup>
)}

View file

@ -974,11 +974,106 @@ async function uploadToPixelDrain(fileBlob: Blob, filename: string): Promise<str
return `https://pixeldrain.com/u/${data.id}`;
}
async function uploadToWebdav(fileBlob: Blob, filename: string): Promise<string> {
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<string> {
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<string, string> = {
"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<string, string>; }; };
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<string> {
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<string>
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<string, string> = {
const requestHeaders: Record<string, string> = {
"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<string> {