From cc990eb071bd229c83eee0ca79e3f5f844002102 Mon Sep 17 00:00:00 2001 From: yabo083 Date: Sun, 18 Jan 2026 01:00:33 +0800 Subject: [PATCH 1/2] feat(settings): support custom domain for share and direct links --- src/hooks/useLink.ts | 18 ++++++++++++++++-- src/lang/en/settings.json | 6 +++++- src/utils/share.ts | 17 ++++++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/hooks/useLink.ts b/src/hooks/useLink.ts index 4bc32b762..c5df6d6ab 100644 --- a/src/hooks/useLink.ts +++ b/src/hooks/useLink.ts @@ -1,4 +1,4 @@ -import { objStore, selectedObjs, State, me } from "~/store" +import { objStore, selectedObjs, State, me, getSetting } from "~/store" import { Obj, ArchiveObj } from "~/types" import { base_path, @@ -13,6 +13,20 @@ import { cookieStorage } from "@solid-primitives/storage" type URLType = "preview" | "direct" | "proxy" +// Get the host URL for direct/share links, using custom domain if configured +const getCustomHost = (isShare: boolean): string => { + const settingKey = isShare ? "share_url_domain" : "direct_link_url_domain" + const customDomain = getSetting(settingKey) + if (customDomain) { + let url = customDomain.trim() + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url + } + return url.replace(/\/$/, "") + base_path + } + return api +} + // get download url by dir and obj export const getLinkByDirAndObj = ( dir: string, @@ -29,7 +43,7 @@ export const getLinkByDirAndObj = ( dir = standardizePath(dir, true) let path = `${dir}/${obj.name}` path = encodePath(path, encodeAll) - let host = api + let host = type === "preview" ? api : getCustomHost(isShare) let prefix = isShare ? "/sd" : type === "direct" ? "/d" : "/p" if (type === "preview") { prefix = "" diff --git a/src/lang/en/settings.json b/src/lang/en/settings.json index c624a41d7..42d28491a 100755 --- a/src/lang/en/settings.json +++ b/src/lang/en/settings.json @@ -142,5 +142,9 @@ "version": "Version", "video_autoplay": "Video autoplay", "video_types": "Video types", - "webauthn_login_enabled": "Webauthn login enabled" + "webauthn_login_enabled": "Webauthn login enabled", + "share_url_domain": "Share URL domain", + "share_url_domain-tips": "Custom domain for share links (e.g., https://share.example.com). Leave empty to use site_url", + "direct_link_url_domain": "Direct link URL domain", + "direct_link_url_domain-tips": "Custom domain for direct links (e.g., https://download.example.com). Leave empty to use site_url" } diff --git a/src/utils/share.ts b/src/utils/share.ts index cb6abe42f..9b4be4538 100644 --- a/src/utils/share.ts +++ b/src/utils/share.ts @@ -1,5 +1,6 @@ import { ShareInfo } from "~/types" import { base_path } from "." +import { getSetting } from "~/store" const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" @@ -11,12 +12,26 @@ export const randomPwd = () => { return arr.join("") } +// Get the base URL for share links, using custom domain if configured +export const getShareBaseUrl = () => { + const customDomain = getSetting("share_url_domain") + if (customDomain) { + // Ensure proper URL format + let url = customDomain.trim() + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url + } + return url.replace(/\/$/, "") + base_path + } + return location.origin + base_path +} + export const makeTemplateData = ( share: ShareInfo, other?: { [k: string]: any }, ) => { return { - base_url: location.origin + base_path, + base_url: getShareBaseUrl(), ...share, ...other, } From 4057f0d7495b3f39f0d69337444d360a454fc3bd Mon Sep 17 00:00:00 2001 From: Auto-fix Date: Sun, 18 Jan 2026 08:54:30 +0800 Subject: [PATCH 2/2] feat: improve share page link generation with smart routing - Add smart internal/external network routing for share page links - Fix single file share download links (avoid path duplication) - Extract sharing ID correctly from share paths - Use location.origin for automatic network routing (internal vs external) - Copy page URL instead of download link on share pages - Ensure settings are loaded before page rendering This allows users to access shared resources via internal network when accessing from LAN, and via external domain when accessing from internet, without any manual configuration needed. --- src/app/App.tsx | 37 +++++++------ src/hooks/useLink.ts | 121 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 121 insertions(+), 37 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index fcc5b25eb..a594d4de8 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -43,23 +43,26 @@ const App: Component = () => { }) const [err, setErr] = createSignal([]) - const [loading, data] = useLoading(() => - Promise.all([ - (async () => { - handleRespWithoutAuthAndNotify( - (await r.get("/public/settings")) as Resp>, - setSettings, - (e) => setErr(err().concat(e)), - ) - })(), - (async () => { - handleRespWithoutAuthAndNotify( - (await r.get("/public/archive_extensions")) as Resp, - setArchiveExtensions, - (e) => setErr(err().concat(e)), - ) - })(), - ]), + const [loading, data] = useLoading( + () => + Promise.all([ + (async () => { + handleRespWithoutAuthAndNotify( + (await r.get("/public/settings")) as Resp>, + setSettings, + (e) => setErr(err().concat(e)), + ) + })(), + (async () => { + handleRespWithoutAuthAndNotify( + (await r.get("/public/archive_extensions")) as Resp, + setArchiveExtensions, + (e) => setErr(err().concat(e)), + ) + })(), + ]), + false, // fetch parameter + true, // initial loading state - ensure settings are loaded before rendering ) data() return ( diff --git a/src/hooks/useLink.ts b/src/hooks/useLink.ts index c5df6d6ab..b7cf36295 100644 --- a/src/hooks/useLink.ts +++ b/src/hooks/useLink.ts @@ -13,18 +13,66 @@ import { cookieStorage } from "@solid-primitives/storage" type URLType = "preview" | "direct" | "proxy" -// Get the host URL for direct/share links, using custom domain if configured -const getCustomHost = (isShare: boolean): string => { - const settingKey = isShare ? "share_url_domain" : "direct_link_url_domain" - const customDomain = getSetting(settingKey) - if (customDomain) { - let url = customDomain.trim() - if (!url.startsWith("http://") && !url.startsWith("https://")) { - url = "https://" + url +// Check if current access is from external network (matching configured domain) +const isExternalAccess = (): boolean => { + const customDomain = getSetting("share_url_domain") + if (!customDomain) return false + + try { + let configuredUrl = customDomain.trim() + if ( + !configuredUrl.startsWith("http://") && + !configuredUrl.startsWith("https://") + ) { + configuredUrl = "https://" + configuredUrl } - return url.replace(/\/$/, "") + base_path + const configuredHost = new URL(configuredUrl).host + const currentHost = location.host + return ( + currentHost === configuredHost || + currentHost.endsWith("." + configuredHost) + ) + } catch { + return false + } +} + +// Get the host URL for direct/share links +// Smart routing: always use current origin (location.origin) +// This ensures internal users get internal links, external users get external links +const getCustomHost = (isShare: boolean): string => { + // Always use current origin for consistency + // This provides smart routing automatically: + // - External access via ol.miyakko.de → links use ol.miyakko.de + // - Internal access via 192.168.x.x → links use 192.168.x.x + return location.origin + base_path +} + +// Extract sharing ID from a share path (/@s/{sid}/...) +const extractSharingId = (sharePath: string): string => { + // sharePath format: /@s/{sid} or /@s/{sid}/path/to/file + // Remove /@s prefix first + const withoutPrefix = sharePath.startsWith("/@s/") + ? sharePath.substring(4) + : sharePath.substring(3) + const slashIndex = withoutPrefix.indexOf("/") + if (slashIndex === -1) { + return withoutPrefix // Single file share: /@s/{sid} } - return api + return withoutPrefix.substring(0, slashIndex) // Folder share: /@s/{sid}/... +} + +// Extract the path after sharing ID from a share path +const extractPathAfterSid = (sharePath: string): string => { + // sharePath format: /@s/{sid} or /@s/{sid}/path/to/file + const withoutPrefix = sharePath.startsWith("/@s/") + ? sharePath.substring(4) + : sharePath.substring(3) + const slashIndex = withoutPrefix.indexOf("/") + if (slashIndex === -1) { + return "" // Single file share, no path after sid + } + return withoutPrefix.substring(slashIndex) // Returns /path/to/file } // get download url by dir and obj @@ -35,16 +83,31 @@ export const getLinkByDirAndObj = ( isShare: boolean, encodeAll?: boolean, ) => { - if (type !== "preview") - dir = isShare - ? dir.substring(3) /* remove /@s */ - : pathJoin(me().base_path, dir) + let sharingId = "" + let isSingleFileShare = false + + if (type !== "preview") { + if (isShare) { + // For share pages, extract the sharing ID and path after it + sharingId = extractSharingId(dir) + const pathAfterSid = extractPathAfterSid(dir) + // If pathAfterSid is empty, this is a single file share + // In that case, we should NOT add obj.name to the path + // because the backend will use the sharing's file path directly + isSingleFileShare = pathAfterSid === "" + dir = pathAfterSid + } else { + dir = pathJoin(me().base_path, dir) + } + } dir = standardizePath(dir, true) - let path = `${dir}/${obj.name}` + // For single file share, path should be "/" (root) since backend knows the file + // For multi-file share or normal access, path includes the filename + let path = isSingleFileShare ? "/" : `${dir}/${obj.name}` path = encodePath(path, encodeAll) let host = type === "preview" ? api : getCustomHost(isShare) - let prefix = isShare ? "/sd" : type === "direct" ? "/d" : "/p" + let prefix = isShare ? `/sd/${sharingId}` : type === "direct" ? "/d" : "/p" if (type === "preview") { prefix = "" if (!api.startsWith(location.origin + base_path)) @@ -53,7 +116,7 @@ export const getLinkByDirAndObj = ( const { inner_path, archive } = obj as ArchiveObj if (archive) { prefix = "/ae" - path = `${dir}/${archive.name}` + path = isSingleFileShare ? "/" : `${dir}/${archive.name}` path = encodePath(path, encodeAll) } let ans = `${host}${prefix}${path}` @@ -77,7 +140,14 @@ export const getLinkByDirAndObj = ( export const useLink = () => { const { pathname, isShare } = useRouter() const getLinkByObj = (obj: Obj, type?: URLType, encodeAll?: boolean) => { - const dir = objStore.state !== State.File ? pathname() : pathDir(pathname()) + // For share pages, always pass full pathname to preserve sharing ID + // For non-share pages, use pathDir when viewing a file + let dir: string + if (isShare()) { + dir = pathname() // Keep full path to preserve sharing ID + } else { + dir = objStore.state !== State.File ? pathname() : pathDir(pathname()) + } return getLinkByDirAndObj(dir, obj, type, isShare(), encodeAll) } const rawLink = (obj: Obj, encodeAll?: boolean) => { @@ -122,15 +192,26 @@ export const useCopyLink = () => { const { copy } = useUtil() const { previewPagesText, rawLinksText } = useSelectedLink() const { currentObjLink } = useLink() + const { isShare } = useRouter() return { copySelectedPreviewPage: () => { copy(previewPagesText()) }, copySelectedRawLink: (encodeAll?: boolean) => { - copy(rawLinksText(encodeAll)) + // On share pages, copy the current page URL instead of download link + if (isShare()) { + copy(location.href) + } else { + copy(rawLinksText(encodeAll)) + } }, copyCurrentRawLink: (encodeAll?: boolean) => { - copy(currentObjLink(encodeAll)) + // On share pages, copy the current page URL instead of download link + if (isShare()) { + copy(location.href) + } else { + copy(currentObjLink(encodeAll)) + } }, } }