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 4bc32b762..b7cf36295 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,68 @@ import { cookieStorage } from "@solid-primitives/storage" type URLType = "preview" | "direct" | "proxy" +// 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 + } + 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 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 export const getLinkByDirAndObj = ( dir: string, @@ -21,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 = api - let prefix = isShare ? "/sd" : type === "direct" ? "/d" : "/p" + let host = type === "preview" ? api : getCustomHost(isShare) + let prefix = isShare ? `/sd/${sharingId}` : type === "direct" ? "/d" : "/p" if (type === "preview") { prefix = "" if (!api.startsWith(location.origin + base_path)) @@ -39,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}` @@ -63,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) => { @@ -108,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)) + } }, } } 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, }