build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s
110 lines
5.3 KiB
TypeScript
110 lines
5.3 KiB
TypeScript
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<typeof setInterval> | undefined
|
|
let pending: Promise<void> | undefined
|
|
let lastSync = 0
|
|
const retried = new Map<string, number>()
|
|
const retrying = new Map<string, Promise<boolean>>()
|
|
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<void> {
|
|
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<boolean> {
|
|
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<string, never>, 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) }
|
|
}
|