Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dee0437e6 | ||
|
|
f25c89855f | ||
|
|
b0a5fbfca5 |
No files matched your search
@@ -2,6 +2,31 @@
|
||||
|
||||
本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。
|
||||
|
||||
## [1.1.5] - 2026-08-28
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 API 上传后图库预览偶发失败、需等待刷新才显示的问题
|
||||
- 图库卡片加载失败后自动重试,刷新列表时重置预览状态
|
||||
|
||||
### 改进
|
||||
|
||||
- 出图前校验存储文件实际存在,避免索引与磁盘短暂不一致
|
||||
- 图片 404/403 与域名隔离 404 禁止 CDN 缓存,降低反代误判影响
|
||||
|
||||
## [1.1.4] - 2026-08-28
|
||||
|
||||
### 新增
|
||||
|
||||
- 上传目录可按年/月分组或扁平保存(`storage_use_date_path` / `STORAGE_USE_DATE_PATH`),设置页可切换
|
||||
- 双域名分离下网站域禁止直链出图,仅图片域可访问图片路径
|
||||
|
||||
### 改进
|
||||
|
||||
- 隐藏 `images/` 前缀时外链改为仅文件名(如 `xxx.webp`);图片域仍兼容旧版 `/2026/08/xxx.webp` 短链
|
||||
- 设置页重构:功能开关、域名配置、存储路径分组展示
|
||||
- 本地磁盘索引扫描支持扁平路径;文件夹列表 `images` 置顶
|
||||
|
||||
## [1.1.3] - 2026-08-27
|
||||
|
||||
### 新增
|
||||
|
||||
+4
-2
@@ -32,7 +32,7 @@
|
||||
|
||||
| Branch | Notes |
|
||||
| ------ | ----- |
|
||||
| [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.3** — local disk + multi-backend storage, dual-domain separation, [`/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 |
|
||||
|
||||
Use **main** for the full product and multiple cloud backends.
|
||||
@@ -228,7 +228,9 @@ npm test # Unit tests
|
||||
|
||||
| Version | Plan |
|
||||
| ---------- | -------------------------------------------------------------- |
|
||||
| **v1.1.3** | Dual-domain separation, optional `images/` URL prefix, update check (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.2** | Activity logs, gallery storage filter, upload rate limits |
|
||||
| **v1.1.1** | Session check fixes, storage placeholder cleanup, Docker logging |
|
||||
| **v1.1.0** | Multi-backend object storage, hybrid URLs, storage UI |
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
| 分支 | 说明 |
|
||||
| ---- | ---- |
|
||||
| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.3** 主线:本地磁盘 + 多对象存储、双域名分离、[`/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 |
|
||||
|
||||
日常使用、需要管理多种存储后端,请直接用 **main**。
|
||||
@@ -233,7 +233,9 @@ npm test # 单元测试
|
||||
|
||||
| 版本 | 说明 |
|
||||
| ---------- | ------------------------------------------------- |
|
||||
| **v1.1.3** | 双域名分离、隐藏 `images/` 前缀、版本更新提示(当前 main) |
|
||||
| **v1.1.5** | 图库预览可靠性:自动重试、出图前校验、错误响应禁缓存(当前 main) |
|
||||
| **v1.1.4** | 存储路径扁平/分组、basename 外链、双域名双向隔离 |
|
||||
| **v1.1.3** | 双域名分离、隐藏 `images/` 前缀、版本更新提示 |
|
||||
| **v1.1.2** | 操作日志、图库存储筛选、上传限流 |
|
||||
| **v1.1.1** | 会话校验与存储占位修复、Docker 日志增强 |
|
||||
| **v1.1.0** | S3 / R2 多后端对象存储、混合直链、存储管理页 |
|
||||
|
||||
@@ -26,8 +26,48 @@ const emit = defineEmits<{
|
||||
const { formatFileSize } = useFileSize()
|
||||
const { t, locale } = useI18n()
|
||||
const imageError = ref(false)
|
||||
const retryCount = ref(0)
|
||||
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(() => [
|
||||
{ label: t('copy.url'), value: 'url' 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">
|
||||
<img
|
||||
v-if="!imageError"
|
||||
:src="image.url"
|
||||
:key="`${image.key}-${retryCount}`"
|
||||
:src="previewSrc"
|
||||
:alt="image.originalName"
|
||||
loading="lazy"
|
||||
class="absolute inset-0 size-full object-cover object-center"
|
||||
@error="imageError = true"
|
||||
@error="onPreviewError"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pichost",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pichost",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pichost",
|
||||
"version": "1.1.3",
|
||||
"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.",
|
||||
"homepage": "https://github.com/O96u/PicHost#readme",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { shouldBlockImageHostRequest, shouldBlockSiteHostImageRequest } from '../utils/host-isolation'
|
||||
|
||||
const HOST_ISOLATION_CACHE_CONTROL = 'private, no-cache, max-age=0'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
if (shouldBlockImageHostRequest(event) || shouldBlockSiteHostImageRequest(event)) {
|
||||
setHeader(event, 'Cache-Control', HOST_ISOLATION_CACHE_CONTROL)
|
||||
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
||||
}
|
||||
})
|
||||
@@ -20,7 +20,7 @@ import { getBackendRowForKey } from './storage/resolver'
|
||||
* - 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> {
|
||||
const hosts = new Set<string>()
|
||||
|
||||
+12
-3
@@ -53,13 +53,22 @@ export async function putImage(
|
||||
}
|
||||
|
||||
export async function headImage(key: string): Promise<StoredImage | null> {
|
||||
const indexed = getImageIndexAsStored(key)
|
||||
if (indexed) return indexed
|
||||
|
||||
const backend = await getBackendForKey(key)
|
||||
const stored = await backend.head(key)
|
||||
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({
|
||||
key: stored.key,
|
||||
backendId: backend.id,
|
||||
|
||||
Reference in new issue
Block a user