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:
parent
476a6b0ac3
commit
90b4eb14ba
3 changed files with 224 additions and 21 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue