import { ref } from 'vue' import { createFileAccess, rewriteFileHtml, rewriteFileNodes } from './file-access-core' export const fileAccessRevision = ref(0) const client = createFileAccess({ appId: import.meta.env.VITE_FILE_ACCESS_APP_ID, kid: import.meta.env.VITE_FILE_ACCESS_KEY_ID, key: import.meta.env.VITE_FILE_ACCESS_KEY, baseUrl: () => import.meta.env.VITE_IMAGE_BASE_PATH || import.meta.env.VITE_API_URL || '/api', allowedOrigins: [import.meta.env.VITE_IMAGE_BASE_PATH, typeof location === 'undefined' ? '' : location.origin, ...(import.meta.env.VITE_FILE_ACCESS_ALLOWED_ORIGINS || '').split(',')], }) let timer: ReturnType | undefined let pending: Promise | undefined let lastSync = 0 const retried = new Map() const retrying = new Map>() export function fileUrl(source?: string | null): string { void fileAccessRevision.value try { return client.url(source) } catch { return '' } } export const accessFileUrl = (source?: string | null) => isProtectedFile(source) ? fileUrl(source) : (source || '') export const fileUrls = (sources: any) => Array.isArray(sources) ? sources.map(accessFileUrl) : [] export const originalFileUrl = (source?: string | null) => client.strip(source) export const resolveFileUrl = (source?: string | null) => client.resolve(source) export const fileUploadHeaders = (source: string) => client.uploadHeaders(source) export const fileHtml = (html: string) => rewriteFileHtml(html, accessFileUrl) export const stripFileHtml = (html: string) => rewriteFileHtml(html, originalFileUrl) export const fileNodes = (nodes: any) => rewriteFileNodes(nodes, accessFileUrl) export const isProtectedFile = (source?: string | null) => { try { return client.protected(source) } catch { return false } } export async function refreshFileAccess(force = false): Promise { if (pending) return pending if (!force && Date.now() - lastSync < 60000) return pending = (async () => { const started = Date.now() try { const result: any = await readAccessInfo() const info = result?.data || result if (info?.serverTime) client.synchronize(Number(info.serverTime), Number(info.ttlSeconds), Date.now() - started) lastSync = Date.now() } catch { /* Retain the last clock offset and signed access; never fall back to a bare URL. */ } finally { fileAccessRevision.value++; pending = undefined } })() return pending } export function startFileAccess() { if (!timer) timer = setInterval(() => { fileAccessRevision.value++ }, 60000) void refreshFileAccess() } export function stopFileAccess() { if (timer) clearInterval(timer); timer = undefined } export async function retryFileAccess(source?: string | null): Promise { if (!isProtectedFile(source)) return false const raw = originalFileUrl(source), previous = retried.get(raw) || 0 const active = retrying.get(raw) if (active) return active if (Date.now() - previous < 30 * 60 * 1000) return false if (retried.size >= 2048) retried.clear() retried.set(raw, Date.now()) const task = (async () => { const previousUrl = fileUrl(source) await refreshFileAccess(true) client.invalidate(source) if (fileUrl(source) === previousUrl) { await new Promise(resolve => setTimeout(resolve, 1100)) client.invalidate(source) } fileAccessRevision.value++ return true })() retrying.set(raw, task) try { return await task } finally { retrying.delete(raw) } } type FileDownloader = (url: string, query: Record, name: string) => unknown /** Keep owned file links inside the application's fetch/blob download flow. */ export function handleFileLinkClick(event: MouseEvent, download: FileDownloader): boolean { const anchor = (event.target as Element | null)?.closest?.('a[href]') const href = anchor?.getAttribute('href') if (event.defaultPrevented || !href || !isProtectedFile(href)) return false event.preventDefault() event.stopImmediatePropagation() const name = /[?&]fileName=([^&#]*)/.exec(resolveFileUrl(href)) const filename = anchor?.getAttribute('download') || (name ? decodeURIComponent(name[1].replace(/\+/g, ' ')) : '文件') void download(originalFileUrl(href), {}, filename) return true } export function installFileAccess(app: any, download?: FileDownloader) { Object.assign(app.config.globalProperties, { $fileUrl: accessFileUrl, $fileUrls: fileUrls, $fileHtml: fileHtml, $fileNodes: fileNodes, $fileRetry: retryFileAccess, }) startFileAccess() if (typeof window !== 'undefined') { if (download) document.addEventListener('click', event => { handleFileLinkClick(event, download) }, true) window.addEventListener('online', () => { retried.clear(); void refreshFileAccess(true) }) document.addEventListener('visibilitychange', () => { if (document.hidden) stopFileAccess() else { retried.clear(); startFileAccess() } }) } } async function readAccessInfo() { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 5000) try { const response = await fetch(client.resolve('/admin/sys-file/oss/access-info'), { credentials: 'omit', cache: 'no-store', signal: controller.signal }) if (!response.ok) throw new Error('File clock unavailable') return await response.json() } finally { clearTimeout(timeout) } }