FileUpload: Add WebDAV support and QOL features (#1162)
* FileUpload: Add WebDAV support and QOL features - Now supports WebDAV providers such as OwnCloud and NextCloud - Added a context menu option to upload specific attachments rather than having to upload all of them at once. - File type filtering * greptile fixes
This commit is contained in:
parent
ec7886beaf
commit
3d1ea790a5
5 changed files with 280 additions and 30 deletions
|
|
@ -12,13 +12,14 @@ import { OpenExternalIcon } from "@components/Icons";
|
||||||
import { Devs, EquicordDevs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { classNameFactory } from "@utils/css";
|
import { classNameFactory } from "@utils/css";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
import { CloudUpload } from "@vencord/discord-types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { DraftType, FluxDispatcher, Menu, PermissionsBits, PermissionStore, React, useEffect, UserStore, useState } from "@webpack/common";
|
import { DraftType, FluxDispatcher, Menu, PermissionsBits, PermissionStore, React, showToast, Toasts, UploadAttachmentStore, useEffect, UserStore, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { serviceLabels, ServiceType } from "./types";
|
import { serviceLabels, ServiceType } from "./types";
|
||||||
import { getMediaUrl } from "./utils/getMediaUrl";
|
import { getMediaUrl } from "./utils/getMediaUrl";
|
||||||
import { cancelCurrentUpload, getUploadState, isConfigured, subscribeUploadState, uploadFile, uploadPickedFile, uploadProvidedFiles } from "./utils/upload";
|
import { cancelCurrentUpload, getUploadState, isConfigured, isFileTypeAllowed, logger, subscribeUploadState, uploadFile, uploadPickedFile, uploadProvidedFiles } from "./utils/upload";
|
||||||
const cl = classNameFactory("vc-file-upload-");
|
const cl = classNameFactory("vc-file-upload-");
|
||||||
const { getUserMaxFileSize } = findByPropsLazy("getUserMaxFileSize");
|
const { getUserMaxFileSize } = findByPropsLazy("getUserMaxFileSize");
|
||||||
let uploadAddFilesInterceptor: ((event: unknown) => void) | null = null;
|
let uploadAddFilesInterceptor: ((event: unknown) => void) | null = null;
|
||||||
|
|
@ -81,7 +82,7 @@ function interceptUploadAddFiles(event: unknown): void {
|
||||||
...extractFilesFromValue(payload.uploads),
|
...extractFilesFromValue(payload.uploads),
|
||||||
...extractFilesFromValue(payload.items)
|
...extractFilesFromValue(payload.items)
|
||||||
];
|
];
|
||||||
const uniqueFiles = Array.from(new Set(files));
|
const uniqueFiles = Array.from(new Set(files)).filter(f => isFileTypeAllowed(f));
|
||||||
|
|
||||||
if (!uniqueFiles.length) return;
|
if (!uniqueFiles.length) return;
|
||||||
if (!shouldInterceptUploadFiles(uniqueFiles, payload)) return;
|
if (!shouldInterceptUploadFiles(uniqueFiles, payload)) return;
|
||||||
|
|
@ -98,10 +99,13 @@ function handlePaste(event: ClipboardEvent) {
|
||||||
|
|
||||||
if (!settings.store.autoUploadPastedFiles || !isConfigured()) return;
|
if (!settings.store.autoUploadPastedFiles || !isConfigured()) return;
|
||||||
|
|
||||||
|
const allowed = files.filter(f => isFileTypeAllowed(f));
|
||||||
|
if (allowed.length === 0) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
void uploadProvidedFiles(files);
|
void uploadProvidedFiles(allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
|
|
@ -224,32 +228,88 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleUploadFileFromDraft(upload: CloudUpload) {
|
||||||
|
const file = upload.item?.file;
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!isFileTypeAllowed(file)) {
|
||||||
|
showToast("File type not allowed by current filter", Toasts.Type.FAILURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isConfigured()) {
|
||||||
|
showToast("Please configure FileUpload settings first", Toasts.Type.FAILURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadProvidedFiles([file], true);
|
||||||
|
upload.removeFromMsgDraft();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Draft upload encountered an unexpected error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ExternalIcon = () => <OpenExternalIcon height={24} width={24} />;
|
const ExternalIcon = () => <OpenExternalIcon height={24} width={24} />;
|
||||||
|
|
||||||
const channelAttachMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
const channelAttachMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||||
const channel = props?.channel;
|
const channel = props?.channel;
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
|
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
|
||||||
if (children.some(child => child?.props?.id === "file-upload-manual")) return;
|
if (children.some(child => child?.props?.id === "file-upload-manual" || child?.props?.id === "file-upload-uploads")) return;
|
||||||
|
|
||||||
children.splice(1, 0,
|
const uploads = UploadAttachmentStore.getUploads(channel.id, DraftType.ChannelMessage);
|
||||||
<Menu.MenuItem
|
const draftUploads = Array.isArray(uploads) ? uploads.filter((u: CloudUpload) => u.item?.file && isFileTypeAllowed(u.item.file)) : [];
|
||||||
id="file-upload-manual"
|
|
||||||
key="file-upload-manual"
|
if (draftUploads.length > 0) {
|
||||||
label="Upload to Host"
|
children.splice(1, 0,
|
||||||
iconLeft={ExternalIcon}
|
<Menu.MenuItem
|
||||||
leadingAccessory={{
|
id="file-upload-uploads"
|
||||||
type: "icon",
|
key="file-upload-uploads"
|
||||||
icon: ExternalIcon
|
label="Upload to Host"
|
||||||
}}
|
iconLeft={ExternalIcon}
|
||||||
action={() => uploadPickedFile()}
|
leadingAccessory={{
|
||||||
/>
|
type: "icon",
|
||||||
);
|
icon: ExternalIcon
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{draftUploads.map((upload: CloudUpload) => (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id={`file-upload-draft-${upload.id}`}
|
||||||
|
key={upload.id}
|
||||||
|
label={upload.filename}
|
||||||
|
action={() => handleUploadFileFromDraft(upload)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Menu.MenuSeparator />
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="file-upload-manual"
|
||||||
|
key="file-upload-manual"
|
||||||
|
label="Choose File..."
|
||||||
|
action={() => uploadPickedFile()}
|
||||||
|
/>
|
||||||
|
</Menu.MenuItem>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.splice(1, 0,
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="file-upload-manual"
|
||||||
|
key="file-upload-manual"
|
||||||
|
label="Upload to Host"
|
||||||
|
iconLeft={ExternalIcon}
|
||||||
|
leadingAccessory={{
|
||||||
|
type: "icon",
|
||||||
|
icon: ExternalIcon
|
||||||
|
}}
|
||||||
|
action={() => uploadPickedFile()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FileUpload",
|
name: "FileUpload",
|
||||||
description: "Upload images and videos to file hosting services like Zipline and Nest",
|
description: "Upload files to hosting services like Zipline, Nest, S3, and WebDAV",
|
||||||
tags: ["Media"],
|
tags: ["Media"],
|
||||||
authors: [EquicordDevs.creations, EquicordDevs.keircn, Devs.ScattrdBlade],
|
authors: [EquicordDevs.creations, EquicordDevs.keircn, Devs.ScattrdBlade],
|
||||||
settings,
|
settings,
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,43 @@ export async function uploadToPixelDrain(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidHttpsUrl(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.protocol === "https:";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadToWebdav(
|
||||||
|
_: IpcMainInvokeEvent,
|
||||||
|
fileBuffer: ArrayBuffer,
|
||||||
|
uploadUrl: string,
|
||||||
|
headers: Record<string, string>
|
||||||
|
): Promise<NativeUploadResult> {
|
||||||
|
if (!isValidHttpsUrl(uploadUrl)) {
|
||||||
|
return { success: false, error: "Invalid or non-HTTPS upload URL" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(uploadUrl, {
|
||||||
|
method: "PUT",
|
||||||
|
headers,
|
||||||
|
body: new Blob([fileBuffer])
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
return { success: false, error: `Upload failed: ${response.status} ${errorText}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, url: uploadUrl };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchFile(
|
export async function fetchFile(
|
||||||
|
|
||||||
_: IpcMainInvokeEvent,
|
_: IpcMainInvokeEvent,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ const serviceOptions = [
|
||||||
{ label: "filebin.net", value: ServiceType.FILEBIN },
|
{ label: "filebin.net", value: ServiceType.FILEBIN },
|
||||||
{ label: "PixelVault", value: ServiceType.PIXELVAULT },
|
{ label: "PixelVault", value: ServiceType.PIXELVAULT },
|
||||||
{ label: "PixelDrain", value: ServiceType.PIXELDRAIN },
|
{ label: "PixelDrain", value: ServiceType.PIXELDRAIN },
|
||||||
{ label: "ShareX Custom Uploader", value: ServiceType.SHAREX }
|
{ label: "ShareX Custom Uploader", value: ServiceType.SHAREX },
|
||||||
|
{ label: "WebDAV (Nextcloud/Owncloud)", value: ServiceType.WEBDAV }
|
||||||
];
|
];
|
||||||
|
|
||||||
const litterboxOptions = [
|
const litterboxOptions = [
|
||||||
|
|
@ -313,6 +314,36 @@ export const settings = definePluginSettings({
|
||||||
description: "Automatically upload files from clipboard to image host when pasting in chat input.",
|
description: "Automatically upload files from clipboard to image host when pasting in chat input.",
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
webdavUrl: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "WebDAV server URL",
|
||||||
|
default: "",
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
webdavUsername: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "WebDAV username",
|
||||||
|
default: "",
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
webdavPassword: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "WebDAV password or app token",
|
||||||
|
default: "",
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
webdavDirectory: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Optional subdirectory on the WebDAV server",
|
||||||
|
default: "",
|
||||||
|
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.",
|
||||||
|
default: "",
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
settingsComponent: {
|
settingsComponent: {
|
||||||
type: OptionType.COMPONENT,
|
type: OptionType.COMPONENT,
|
||||||
description: "Settings",
|
description: "Settings",
|
||||||
|
|
@ -475,6 +506,7 @@ export function SettingsComponent() {
|
||||||
const isPixelVault = store.serviceType === ServiceType.PIXELVAULT;
|
const isPixelVault = store.serviceType === ServiceType.PIXELVAULT;
|
||||||
const isPixelDrain = store.serviceType === ServiceType.PIXELDRAIN;
|
const isPixelDrain = store.serviceType === ServiceType.PIXELDRAIN;
|
||||||
const isShareX = store.serviceType === ServiceType.SHAREX;
|
const isShareX = store.serviceType === ServiceType.SHAREX;
|
||||||
|
const isWebdav = store.serviceType === ServiceType.WEBDAV;
|
||||||
|
|
||||||
const validateShareXConfig = () => {
|
const validateShareXConfig = () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -785,6 +817,39 @@ export function SettingsComponent() {
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isWebdav && (
|
||||||
|
<SettingGroup name="WebDAV" description="Connection details for WebDAV servers (Nextcloud, Owncloud, etc.).">
|
||||||
|
<SettingTextInput
|
||||||
|
name="Server URL"
|
||||||
|
description="Base WebDAV URL (e.g. https://nextcloud.example.com/remote.php/dav/files/username)"
|
||||||
|
value={store.webdavUrl}
|
||||||
|
onChange={v => store.webdavUrl = v}
|
||||||
|
placeholder="https://nextcloud.example.com/remote.php/dav/files/username"
|
||||||
|
/>
|
||||||
|
<SettingTextInput
|
||||||
|
name="Username"
|
||||||
|
description="WebDAV username"
|
||||||
|
value={store.webdavUsername}
|
||||||
|
onChange={v => store.webdavUsername = v}
|
||||||
|
placeholder="username"
|
||||||
|
/>
|
||||||
|
<SettingTextInput
|
||||||
|
name="Password or App Token"
|
||||||
|
description="WebDAV password or app token"
|
||||||
|
value={store.webdavPassword}
|
||||||
|
onChange={v => store.webdavPassword = v}
|
||||||
|
placeholder="password or app token"
|
||||||
|
/>
|
||||||
|
<SettingTextInput
|
||||||
|
name="Upload Directory"
|
||||||
|
description="Optional subdirectory on the server to upload into (e.g. uploads)"
|
||||||
|
value={store.webdavDirectory}
|
||||||
|
onChange={v => store.webdavDirectory = v}
|
||||||
|
placeholder="Leave empty for root directory"
|
||||||
|
/>
|
||||||
|
</SettingGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingGroup name="Upload Behavior" description="Control what FileUpload does after a host returns a URL.">
|
<SettingGroup name="Upload Behavior" description="Control what FileUpload does after a host returns a URL.">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
name="Strip Query Parameters"
|
name="Strip Query Parameters"
|
||||||
|
|
@ -882,6 +947,14 @@ export function SettingsComponent() {
|
||||||
checked={store.bypassDiscordUploadOnlyOverLimit}
|
checked={store.bypassDiscordUploadOnlyOverLimit}
|
||||||
onChange={v => store.bypassDiscordUploadOnlyOverLimit = v}
|
onChange={v => store.bypassDiscordUploadOnlyOverLimit = v}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingTextInput
|
||||||
|
name="Allowed File Types"
|
||||||
|
description="Comma-separated list of extensions (e.g. png,jpg,gif). Leave empty to allow all."
|
||||||
|
value={store.uploadAllowedFileTypes}
|
||||||
|
onChange={v => store.uploadAllowedFileTypes = v}
|
||||||
|
placeholder="png,jpg,gif,mp4,webp"
|
||||||
|
/>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|
||||||
<SettingGroup name="Network" description="Configure browser upload proxying and timeouts.">
|
<SettingGroup name="Network" description="Configure browser upload proxying and timeouts.">
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ export enum ServiceType {
|
||||||
TEMPSH = "tempsh",
|
TEMPSH = "tempsh",
|
||||||
FILEBIN = "filebin",
|
FILEBIN = "filebin",
|
||||||
PIXELVAULT = "pixelvault",
|
PIXELVAULT = "pixelvault",
|
||||||
PIXELDRAIN = "pixeldrain"
|
PIXELDRAIN = "pixeldrain",
|
||||||
|
WEBDAV = "webdav"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serviceLabels: Record<ServiceType, string> = {
|
export const serviceLabels: Record<ServiceType, string> = {
|
||||||
|
|
@ -39,7 +40,8 @@ export const serviceLabels: Record<ServiceType, string> = {
|
||||||
[ServiceType.TEMPSH]: "temp.sh",
|
[ServiceType.TEMPSH]: "temp.sh",
|
||||||
[ServiceType.FILEBIN]: "filebin.net",
|
[ServiceType.FILEBIN]: "filebin.net",
|
||||||
[ServiceType.PIXELVAULT]: "PixelVault",
|
[ServiceType.PIXELVAULT]: "PixelVault",
|
||||||
[ServiceType.PIXELDRAIN]: "PixelDrain"
|
[ServiceType.PIXELDRAIN]: "PixelDrain",
|
||||||
|
[ServiceType.WEBDAV]: "WebDAV"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fallbackServiceOrder: ServiceType[] = [
|
export const fallbackServiceOrder: ServiceType[] = [
|
||||||
|
|
@ -58,6 +60,7 @@ export const fallbackServiceOrder: ServiceType[] = [
|
||||||
ServiceType.FILEBIN,
|
ServiceType.FILEBIN,
|
||||||
ServiceType.PIXELVAULT,
|
ServiceType.PIXELVAULT,
|
||||||
ServiceType.PIXELDRAIN,
|
ServiceType.PIXELDRAIN,
|
||||||
|
ServiceType.WEBDAV,
|
||||||
ServiceType.SHAREX
|
ServiceType.SHAREX
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const Native = IS_DISCORD_DESKTOP
|
||||||
? VencordNative.pluginHelpers.FileUpload as PluginNative<typeof import("../native")>
|
? VencordNative.pluginHelpers.FileUpload as PluginNative<typeof import("../native")>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const logger = new Logger("FileUpload", "#7cb7ff");
|
export const logger = new Logger("FileUpload", "#7cb7ff");
|
||||||
|
|
||||||
function toProxyUrl(url: string): string {
|
function toProxyUrl(url: string): string {
|
||||||
const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl);
|
const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl);
|
||||||
|
|
@ -594,6 +594,8 @@ export function isConfigured(): boolean {
|
||||||
return true;
|
return true;
|
||||||
case ServiceType.PIXELVAULT:
|
case ServiceType.PIXELVAULT:
|
||||||
return Boolean((settings.store as { pixelVaultKey?: string; }).pixelVaultKey);
|
return Boolean((settings.store as { pixelVaultKey?: string; }).pixelVaultKey);
|
||||||
|
case ServiceType.WEBDAV:
|
||||||
|
return Boolean((settings.store as { webdavUrl?: string; }).webdavUrl);
|
||||||
case ServiceType.SHAREX:
|
case ServiceType.SHAREX:
|
||||||
try {
|
try {
|
||||||
parseShareXConfigFromSettings();
|
parseShareXConfigFromSettings();
|
||||||
|
|
@ -972,6 +974,54 @@ async function uploadToPixelDrain(fileBlob: Blob, filename: string): Promise<str
|
||||||
return `https://pixeldrain.com/u/${data.id}`;
|
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;
|
||||||
|
webdavUsername?: string;
|
||||||
|
webdavPassword?: string;
|
||||||
|
webdavDirectory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!webdavUrl) {
|
||||||
|
throw new Error("WebDAV server URL is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = webdavUrl.replace(/\/+$/, "");
|
||||||
|
const dir = (webdavDirectory || "").replace(/^\/+|\/+$/g, "");
|
||||||
|
const path = dir ? `/${dir}/${encodeURIComponent(filename)}` : `/${encodeURIComponent(filename)}`;
|
||||||
|
const uploadUrl = `${baseUrl}${path}`;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": fileBlob.type || "application/octet-stream"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (webdavUsername?.trim() && webdavPassword?.trim()) {
|
||||||
|
headers.Authorization = `Basic ${btoa(`${webdavUsername.trim()}:${webdavPassword.trim()}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Native) {
|
||||||
|
const arrayBuffer = await fileBlob.arrayBuffer();
|
||||||
|
const result = await Native.uploadToWebdav(arrayBuffer, uploadUrl, headers);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Upload failed");
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filename: string): Promise<string> {
|
async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filename: string): Promise<string> {
|
||||||
switch (serviceType) {
|
switch (serviceType) {
|
||||||
case ServiceType.ZIPLINE:
|
case ServiceType.ZIPLINE:
|
||||||
|
|
@ -1006,6 +1056,8 @@ async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filenam
|
||||||
return uploadToPixelVault(fileBlob, filename);
|
return uploadToPixelVault(fileBlob, filename);
|
||||||
case ServiceType.PIXELDRAIN:
|
case ServiceType.PIXELDRAIN:
|
||||||
return uploadToPixelDrain(fileBlob, filename);
|
return uploadToPixelDrain(fileBlob, filename);
|
||||||
|
case ServiceType.WEBDAV:
|
||||||
|
return uploadToWebdav(fileBlob, filename);
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown service type");
|
throw new Error("Unknown service type");
|
||||||
}
|
}
|
||||||
|
|
@ -1116,6 +1168,25 @@ function finalizeUploadedUrl(url: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllowedExtensions(): Set<string> | null {
|
||||||
|
const raw = (settings.store as { uploadAllowedFileTypes?: string; }).uploadAllowedFileTypes?.trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
const exts = raw.split(/[\s,;]+/).map(e => e.trim().toLowerCase()).filter(Boolean);
|
||||||
|
return exts.length > 0 ? new Set(exts) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFileTypeAllowed(file: File): boolean {
|
||||||
|
const allowed = getAllowedExtensions();
|
||||||
|
if (!allowed) return true;
|
||||||
|
|
||||||
|
const dotIndex = file.name.lastIndexOf(".");
|
||||||
|
if (dotIndex < 1 || dotIndex === file.name.length - 1) return false;
|
||||||
|
|
||||||
|
const ext = file.name.slice(dotIndex + 1).toLowerCase();
|
||||||
|
return allowed.has(ext);
|
||||||
|
}
|
||||||
|
|
||||||
function getFilenameExtension(filename: string): string | undefined {
|
function getFilenameExtension(filename: string): string | undefined {
|
||||||
const dotIndex = filename.lastIndexOf(".");
|
const dotIndex = filename.lastIndexOf(".");
|
||||||
if (dotIndex < 1 || dotIndex === filename.length - 1) return undefined;
|
if (dotIndex < 1 || dotIndex === filename.length - 1) return undefined;
|
||||||
|
|
@ -1124,7 +1195,7 @@ function getFilenameExtension(filename: string): string | undefined {
|
||||||
return ext.length <= 10 ? ext : undefined;
|
return ext.length <= 10 ? ext : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function notifyUploadSuccess(finalUrl: string): Promise<void> {
|
async function notifyUploadSuccess(finalUrl: string, forceSend?: boolean): Promise<void> {
|
||||||
if (settings.store.autoCopy) {
|
if (settings.store.autoCopy) {
|
||||||
if (!finalUrl || !finalUrl.trim()) {
|
if (!finalUrl || !finalUrl.trim()) {
|
||||||
showToast("Upload successful, but no URL was available to copy", Toasts.Type.MESSAGE);
|
showToast("Upload successful, but no URL was available to copy", Toasts.Type.MESSAGE);
|
||||||
|
|
@ -1142,7 +1213,7 @@ async function notifyUploadSuccess(finalUrl: string): Promise<void> {
|
||||||
showToast("Upload successful", Toasts.Type.SUCCESS);
|
showToast("Upload successful", Toasts.Type.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoSend = Boolean((settings.store as { autoSend?: boolean; }).autoSend);
|
const autoSend = forceSend || Boolean((settings.store as { autoSend?: boolean; }).autoSend);
|
||||||
const autoFormat = Boolean((settings.store as { autoFormat?: boolean; }).autoFormat);
|
const autoFormat = Boolean((settings.store as { autoFormat?: boolean; }).autoFormat);
|
||||||
if (autoSend) {
|
if (autoSend) {
|
||||||
insertTextIntoChatInputBox(autoFormat ? `<${finalUrl}>` : finalUrl);
|
insertTextIntoChatInputBox(autoFormat ? `<${finalUrl}>` : finalUrl);
|
||||||
|
|
@ -1274,13 +1345,14 @@ async function normalizeUploadBlob(blob: Blob, sourceUrl?: string): Promise<{ bl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadPreparedBlob(blob: Blob, sourceUrl?: string): Promise<void> {
|
async function uploadPreparedBlob(blob: Blob, sourceUrl?: string, forceSend?: boolean): Promise<string> {
|
||||||
const primary = settings.store.serviceType as ServiceType;
|
const primary = settings.store.serviceType as ServiceType;
|
||||||
const { blob: normalizedBlob, filename } = await normalizeUploadBlob(blob, sourceUrl);
|
const { blob: normalizedBlob, filename } = await normalizeUploadBlob(blob, sourceUrl);
|
||||||
setUploadState({ fileName: filename, status: "File ready, starting upload...", percent: 4 });
|
setUploadState({ fileName: filename, status: "File ready, starting upload...", percent: 4 });
|
||||||
const uploadedUrl = await uploadWithFallbacks(normalizedBlob, filename, primary);
|
const uploadedUrl = await uploadWithFallbacks(normalizedBlob, filename, primary);
|
||||||
const finalUrl = applyEmbedProxy(finalizeUploadedUrl(uploadedUrl));
|
const finalUrl = applyEmbedProxy(finalizeUploadedUrl(uploadedUrl));
|
||||||
await notifyUploadSuccess(finalUrl);
|
await notifyUploadSuccess(finalUrl, forceSend);
|
||||||
|
return finalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadFile(url: string): Promise<void> {
|
export async function uploadFile(url: string): Promise<void> {
|
||||||
|
|
@ -1368,10 +1440,15 @@ export async function uploadPickedFile(): Promise<void> {
|
||||||
const file = await chooseFile("*/*");
|
const file = await chooseFile("*/*");
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!isFileTypeAllowed(file)) {
|
||||||
|
showToast("File type not allowed by current filter", Toasts.Type.FAILURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await uploadProvidedFiles([file]);
|
await uploadProvidedFiles([file]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadProvidedFiles(files: readonly File[]): Promise<void> {
|
export async function uploadProvidedFiles(files: readonly File[], forceSend?: boolean): Promise<void> {
|
||||||
if (isUploading) {
|
if (isUploading) {
|
||||||
showToast("Upload already in progress", Toasts.Type.MESSAGE);
|
showToast("Upload already in progress", Toasts.Type.MESSAGE);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1384,7 +1461,7 @@ export async function uploadProvidedFiles(files: readonly File[]): Promise<void>
|
||||||
|
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
|
|
||||||
const uploadFiles = files.filter(file => Boolean(file));
|
const uploadFiles = files.filter(file => Boolean(file) && isFileTypeAllowed(file));
|
||||||
if (!uploadFiles.length) return;
|
if (!uploadFiles.length) return;
|
||||||
|
|
||||||
isUploading = true;
|
isUploading = true;
|
||||||
|
|
@ -1408,7 +1485,7 @@ export async function uploadProvidedFiles(files: readonly File[]): Promise<void>
|
||||||
canCancel: true
|
canCancel: true
|
||||||
});
|
});
|
||||||
|
|
||||||
await uploadPreparedBlob(file);
|
await uploadPreparedBlob(file, undefined, forceSend);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue