import { ref } from 'vue' import { createFileAccess, rewriteFileHtml, rewriteFileNodes } from './file-access-core' import { isDevEnv } from './app' 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: () => isDevEnv() ? (import.meta.env.VITE_IMAGE_DEV_PATH || import.meta.env.VITE_APP_DEV_SERVICE) : import.meta.env.VITE_APP_PROD_IMAGE, allowedOrigins: [import.meta.env.VITE_IMAGE_DEV_PATH, import.meta.env.VITE_APP_DEV_SERVICE, import.meta.env.VITE_APP_PROD_IMAGE, import.meta.env.VITE_APP_PROD_SERVICE, ...(import.meta.env.VITE_FILE_ACCESS_ALLOWED_ORIGINS || '').split(',')], }) let timer: ReturnType | undefined let pending: Promise | undefined let lastSync = 0 const retried = 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) { if (!isProtectedFile(source)) return false const raw = originalFileUrl(source), previous = retried.get(raw) || 0 if (Date.now() - previous < 30 * 60 * 1000) return false if (retried.size >= 2048) retried.clear() retried.set(raw, Date.now()) const previousUrl = fileUrl(source) await refreshFileAccess(true) client.clear() if (fileUrl(source) === previousUrl) { await new Promise(resolve => setTimeout(resolve, 1100)) client.clear() } fileAccessRevision.value++ return true } export function installFileAccess(app: any) { if (typeof uni.addInterceptor === 'function') { uni.addInterceptor('uploadFile', { invoke(options: any) { options.header = { ...(options.header || {}), ...fileUploadHeaders(options.url) } }, }) } Object.assign(app.config.globalProperties, { $fileUrl: accessFileUrl, $fileUrls: fileUrls, $fileHtml: fileHtml, $fileNodes: fileNodes, $fileRetry: retryFileAccess, }) if (typeof document !== 'undefined') { document.addEventListener('click', event => { const anchor = (event.target as Element | null)?.closest?.('a[href]') const href = anchor?.getAttribute('href') if (event.defaultPrevented || !href || !isProtectedFile(href)) return event.preventDefault() event.stopImmediatePropagation() openFileLink(href) }, true) } app.mixin({ onShow() { startFileAccess() }, }) uni.onNetworkStatusChange(({ isConnected }: any) => { if (isConnected) { retried.clear(); void refreshFileAccess(true) } }) } function readAccessInfo(): Promise { return new Promise((resolve, reject) => uni.request({ url: client.resolve('/admin/sys-file/oss/access-info'), method: 'GET', timeout: 5000, success: (response: any) => response.statusCode === 200 ? resolve(response.data) : reject(new Error('File clock unavailable')), fail: reject, })) } /** All preview entrypoints sign at the moment of opening, including already-open pages. */ export function previewFiles(options: any) { const values = { ...options, urls: fileUrls(options.urls) } if (typeof options.current === 'string') values.current = fileUrl(options.current) return uni.previewImage(values) } /** One clock/signature retry. Only successful downloads are delivered to the caller. */ export function downloadFile(options: any) { const run = (retry: boolean) => uni.downloadFile({ ...options, url: fileUrl(options.url), success: async (response: any) => { if (response.statusCode === 200) { options.success?.(response); options.complete?.(response); return } if (retry && response.statusCode === 403 && isProtectedFile(options.url)) { await refreshFileAccess(true); client.clear(); run(false); return } const error = { errMsg: '文件下载失败', statusCode: response.statusCode } options.fail?.(error); options.complete?.(error) if (!options.fail) uni.showToast({ title: error.errMsg, icon: 'none' }) }, complete: undefined, fail: (error: any) => { options.fail?.(error); options.complete?.(error) }, }) return run(true) } /** Open owned rich-text file links without navigating the browser to the file endpoint. */ export function openFileLink(source: string) { const match = /[?&]fileName=([^&#]*)/.exec(resolveFileUrl(source)) const filename = match ? decodeURIComponent(match[1].replace(/\+/g, ' ')) : '文件' if (/\.(?:jpe?g|png|gif|webp|bmp|svg|ico)$/i.test(filename)) { return previewFiles({ urls: [source] }) } return downloadFile({ url: source, success: (response: any) => { if (typeof document !== 'undefined') { const anchor = document.createElement('a') anchor.href = response.tempFilePath anchor.download = filename document.body.appendChild(anchor) anchor.click() anchor.remove() } else { uni.openDocument({ filePath: response.tempFilePath, showMenu: true, fail: () => uni.showToast({ title: '无法打开此文件', icon: 'none' }), }) } }, }) }