3 Commits
Author SHA1 Message Date
O96u 3dee0437e6 release: v1.1.5 — gallery preview reliability fixes
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Bump version to 1.1.5. Retry gallery thumbnails after transient failures,
verify storage before serving, and stop caching image 404/403 responses.
2026-08-28 14:25:40 +08:00
O96u f25c89855f 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.
2026-08-28 14:25:04 +08:00
O96u b0a5fbfca5 release: v1.1.4 — storage path layout and bidirectional domain isolation
Bump version to 1.1.4. Add flat vs date storage paths, basename-only
hide-folder URLs, and block image paths on the site host when domain
separation is active.
2026-08-28 09:59:13 +08:00
9 changed files with 80 additions and 13 deletions

No files matched your search

+12
View File
@@ -2,6 +2,18 @@
本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。 本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。
## [1.1.5] - 2026-08-28
### 修复
- 修复 API 上传后图库预览偶发失败、需等待刷新才显示的问题
- 图库卡片加载失败后自动重试,刷新列表时重置预览状态
### 改进
- 出图前校验存储文件实际存在,避免索引与磁盘短暂不一致
- 图片 404/403 与域名隔离 404 禁止 CDN 缓存,降低反代误判影响
## [1.1.4] - 2026-08-28 ## [1.1.4] - 2026-08-28
### 新增 ### 新增
+3 -2
View File
@@ -32,7 +32,7 @@
| Branch | Notes | | Branch | Notes |
| ------ | ----- | | ------ | ----- |
| [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.4** — local disk + multi-backend storage, bidirectional dual-domain isolation, [`/storage`](docs/README.md) UI, hybrid URLs | | [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.5** — local disk + multi-backend storage, bidirectional dual-domain isolation, [`/storage`](docs/README.md) UI, hybrid URLs |
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2–only line** — streamlined for deployments that use R2 exclusively | | [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2–only line** — streamlined for deployments that use R2 exclusively |
Use **main** for the full product and multiple cloud backends. Use **main** for the full product and multiple cloud backends.
@@ -228,7 +228,8 @@ npm test # Unit tests
| Version | Plan | | Version | Plan |
| ---------- | -------------------------------------------------------------- | | ---------- | -------------------------------------------------------------- |
| **v1.1.4** | Flat vs date storage paths, basename public URLs, bidirectional domain isolation (current `main`) | | **v1.1.5** | Gallery preview reliability: auto-retry, storage verify, no-cache errors (current `main`) |
| **v1.1.4** | Flat vs date storage paths, basename public URLs, bidirectional domain isolation |
| **v1.1.3** | Dual-domain separation, optional `images/` URL prefix, update check | | **v1.1.3** | Dual-domain separation, optional `images/` URL prefix, update check |
| **v1.1.2** | Activity logs, gallery storage filter, upload rate limits | | **v1.1.2** | Activity logs, gallery storage filter, upload rate limits |
| **v1.1.1** | Session check fixes, storage placeholder cleanup, Docker logging | | **v1.1.1** | Session check fixes, storage placeholder cleanup, Docker logging |
+3 -2
View File
@@ -32,7 +32,7 @@
| 分支 | 说明 | | 分支 | 说明 |
| ---- | ---- | | ---- | ---- |
| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.4** 主线:本地磁盘 + 多对象存储、双域名双向隔离、[`/storage`](docs/README.md) 存储管理、混合直链 | | [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.5** 主线:本地磁盘 + 多对象存储、双域名双向隔离、[`/storage`](docs/README.md) 存储管理、混合直链 |
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2 专用线**:面向「只用 R2」的部署,预设与配置更聚焦 R2 | | [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2 专用线**:面向「只用 R2」的部署,预设与配置更聚焦 R2 |
日常使用、需要管理多种存储后端,请直接用 **main**。 日常使用、需要管理多种存储后端,请直接用 **main**。
@@ -233,7 +233,8 @@ npm test # 单元测试
| 版本 | 说明 | | 版本 | 说明 |
| ---------- | ------------------------------------------------- | | ---------- | ------------------------------------------------- |
| **v1.1.4** | 存储路径扁平/分组、basename 外链、双域名双向隔离(当前 main) | | **v1.1.5** | 图库预览可靠性:自动重试、出图前校验、错误响应禁缓存(当前 main) |
| **v1.1.4** | 存储路径扁平/分组、basename 外链、双域名双向隔离 |
| **v1.1.3** | 双域名分离、隐藏 `images/` 前缀、版本更新提示 | | **v1.1.3** | 双域名分离、隐藏 `images/` 前缀、版本更新提示 |
| **v1.1.2** | 操作日志、图库存储筛选、上传限流 | | **v1.1.2** | 操作日志、图库存储筛选、上传限流 |
| **v1.1.1** | 会话校验与存储占位修复、Docker 日志增强 | | **v1.1.1** | 会话校验与存储占位修复、Docker 日志增强 |
+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
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pichost", "name": "pichost",
"version": "1.1.4", "version": "1.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pichost", "name": "pichost",
"version": "1.1.4", "version": "1.1.5",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pichost", "name": "pichost",
"version": "1.1.4", "version": "1.1.5",
"description": "Self-hosted image hosting for your NAS / home server — Nuxt admin UI, local disk or S3-compatible storage, server-side WebP compression, referer protection, Twikoo/EasyImage compatible.", "description": "Self-hosted image hosting for your NAS / home server — Nuxt admin UI, local disk or S3-compatible storage, server-side WebP compression, referer protection, Twikoo/EasyImage compatible.",
"homepage": "https://github.com/O96u/PicHost#readme", "homepage": "https://github.com/O96u/PicHost#readme",
"repository": { "repository": {
+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,