fix: gallery preview retries and avoid caching failed image responses

Verify storage files before serving from the index, disable CDN caching
on image 404/403 and host-isolation blocks, and retry gallery thumbnails
after transient load failures.
This commit is contained in:
O96u committed 2026-08-28 14:25:04 +08:00
1 parent b0a5fbfca5
commit f25c89855f
4 files changed
+59 -6

No files matched your search

+43 -2
View File
@@ -26,8 +26,48 @@ const emit = defineEmits<{
const { formatFileSize } = useFileSize() const { formatFileSize } = useFileSize()
const { t, locale } = useI18n() const { t, locale } = useI18n()
const imageError = ref(false) const imageError = ref(false)
const retryCount = ref(0)
const copyFormat = ref<CopyFormat>('url') const copyFormat = ref<CopyFormat>('url')
const MAX_PREVIEW_RETRIES = 3
const PREVIEW_RETRY_DELAYS_MS = [1000, 2000, 4000]
const previewSrc = computed(() => {
if (retryCount.value === 0) return props.image.url
const separator = props.image.url.includes('?') ? '&' : '?'
return `${props.image.url}${separator}retry=${retryCount.value}`
})
watch(() => props.image.key, () => {
imageError.value = false
retryCount.value = 0
})
let retryTimer: ReturnType<typeof setTimeout> | undefined
function clearRetryTimer() {
if (retryTimer !== undefined) {
clearTimeout(retryTimer)
retryTimer = undefined
}
}
function onPreviewError() {
if (retryCount.value >= MAX_PREVIEW_RETRIES) {
imageError.value = true
return
}
const delay = PREVIEW_RETRY_DELAYS_MS[retryCount.value] ?? PREVIEW_RETRY_DELAYS_MS.at(-1)!
clearRetryTimer()
retryTimer = setTimeout(() => {
retryCount.value += 1
retryTimer = undefined
}, delay)
}
onUnmounted(clearRetryTimer)
const copyFormatItems = computed(() => [ const copyFormatItems = computed(() => [
{ label: t('copy.url'), value: 'url' as const }, { label: t('copy.url'), value: 'url' as const },
{ label: t('copy.markdown'), value: 'markdown' as const }, { label: t('copy.markdown'), value: 'markdown' as const },
@@ -78,11 +118,12 @@ function toggleSelected(value: boolean | 'indeterminate') {
<div class="relative aspect-square shrink-0 overflow-hidden bg-muted"> <div class="relative aspect-square shrink-0 overflow-hidden bg-muted">
<img <img
v-if="!imageError" v-if="!imageError"
:src="image.url" :key="`${image.key}-${retryCount}`"
:src="previewSrc"
:alt="image.originalName" :alt="image.originalName"
loading="lazy" loading="lazy"
class="absolute inset-0 size-full object-cover object-center" class="absolute inset-0 size-full object-cover object-center"
@error="imageError = true" @error="onPreviewError"
> >
<div <div
v-else v-else
+3
View File
@@ -1,7 +1,10 @@
import { shouldBlockImageHostRequest, shouldBlockSiteHostImageRequest } from '../utils/host-isolation' import { shouldBlockImageHostRequest, shouldBlockSiteHostImageRequest } from '../utils/host-isolation'
const HOST_ISOLATION_CACHE_CONTROL = 'private, no-cache, max-age=0'
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
if (shouldBlockImageHostRequest(event) || shouldBlockSiteHostImageRequest(event)) { if (shouldBlockImageHostRequest(event) || shouldBlockSiteHostImageRequest(event)) {
setHeader(event, 'Cache-Control', HOST_ISOLATION_CACHE_CONTROL)
throw createError({ statusCode: 404, statusMessage: 'Not Found' }) throw createError({ statusCode: 404, statusMessage: 'Not Found' })
} }
}) })
+1 -1
View File
@@ -20,7 +20,7 @@ import { getBackendRowForKey } from './storage/resolver'
* - Referer 可被非浏览器客户端伪造,本方案防普通网站盗链,不是身份认证 * - Referer 可被非浏览器客户端伪造,本方案防普通网站盗链,不是身份认证
*/ */
const ERROR_CACHE_CONTROL = 'public, max-age=60' const ERROR_CACHE_CONTROL = 'private, no-cache, max-age=0'
function getAllowedHosts(event: H3Event): Set<string> { function getAllowedHosts(event: H3Event): Set<string> {
const hosts = new Set<string>() const hosts = new Set<string>()
+12 -3
View File
@@ -53,13 +53,22 @@ export async function putImage(
} }
export async function headImage(key: string): Promise<StoredImage | null> { export async function headImage(key: string): Promise<StoredImage | null> {
const indexed = getImageIndexAsStored(key)
if (indexed) return indexed
const backend = await getBackendForKey(key) const backend = await getBackendForKey(key)
const stored = await backend.head(key) const stored = await backend.head(key)
if (!stored) return null if (!stored) return null
const indexed = getImageIndexAsStored(key)
if (indexed) {
return {
...stored,
backendId: backend.id,
originalName: indexed.originalName,
contentType: indexed.contentType,
uploadedAt: indexed.uploadedAt,
userId: indexed.userId
}
}
insertImageIndex({ insertImageIndex({
key: stored.key, key: stored.key,
backendId: backend.id, backendId: backend.id,