diff --git a/CHANGELOG.md b/CHANGELOG.md index 910ed61..79c5ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ 本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。 +## [1.2.0] - 2026-08-28 + +### 变更 + +- 图片统一存放在 `data/images/`;遗留 `blog/`、`twikoo/` 等并列目录需迁移后升级 + +### 新增 + +- CLI 迁移工具:`docker exec pichost migrate` / `migrate --apply`(`server/cli/migrate-to-single-images.mjs`) +- 扫描 `data/` 下除 `images` 外所有顶层目录中的图片,输出 `data/mapping.json` +- 启动时自动同步图片索引(扫描磁盘、归一化遗留 key、清理孤儿记录),并打印同步日志 +- 迁移文档:[`docs/migration-to-v1.2.md`](docs/migration-to-v1.2.md) + +### 改进 + +- 启动时归一化遗留 SQLite key、清理无文件的孤儿索引;支持删除仅索引无文件的记录 + ## [1.1.5] - 2026-08-28 ### 修复 @@ -123,6 +140,7 @@ - 中英文界面(`@nuxtjs/i18n`),语言切换与主题切换独立菜单 - 移动端导航:汉堡菜单 + 分组用户菜单(语言 / 外观 / 账户) - Docker 忘记密码:`docker exec pichost reset-password`(随机密码,见 `server/cli/`) +- Docker 升级到 v1.2.0 前迁移遗留目录:`docker exec pichost migrate` / `migrate --apply` ### 改进 @@ -159,9 +177,9 @@ ### 路线图 -- **v1.1.2**(当前 `main`):操作日志、图库存储筛选、上传限流 +- **v1.2.0**(开发中):统一 `images/` 存储、遗留目录迁移 CLI、启动索引同步 -[1.1.2]: https://github.com/O96u/PicHost/releases/tag/v1.1.2 +[1.2.0]: https://github.com/O96u/PicHost/releases/tag/v1.2.0 [1.1.1]: https://github.com/O96u/PicHost/releases/tag/v1.1.1 [1.1.0]: https://github.com/O96u/PicHost/releases/tag/v1.1.0 [1.0.4]: https://github.com/O96u/PicHost/releases/tag/v1.0.4 diff --git a/Dockerfile b/Dockerfile index efb8c8b..fdd2c93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ ENV NODE_ENV=production \ COPY --from=build /app/.output ./.output COPY server/cli/reset-password.mjs ./server/cli/reset-password.mjs +COPY server/cli/migrate-to-single-images.mjs ./server/cli/migrate-to-single-images.mjs COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh diff --git a/README.en.md b/README.en.md index f11bede..274ddaf 100644 --- a/README.en.md +++ b/README.en.md @@ -32,7 +32,7 @@ | Branch | Notes | | ------ | ----- | -| [**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 | +| [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.2.0** (in progress) — unified `images/` storage, multi-backend, dual-domain isolation, [`/storage`](docs/README.md), [`migration guide`](docs/migration-to-v1.2.en.md) | | [**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. @@ -59,14 +59,14 @@ git checkout cloudflare ## Features -- **Drag, click, or Ctrl+V paste** to upload; admins can use custom `folder` paths +- **Drag, click, or Ctrl+V paste** to upload; all files under `images/` - **Server-side WebP** (sharp), Referer hotlink protection, configurable public base URL - **Multi-user** — username/password login; optional registration; users see only their images - **Upload preferences** (home card flip): client-side compression, auto-copy links, per-user auto-delete - **Multi-backend storage**: local disk + S3-compatible (Cloudflare R2 / Tencent COS / Alibaba OSS / AWS S3); admins manage backends at **Storage** (`/storage`) - **Hybrid URLs**: `proxy` (same-origin via PicHost) or `public` (302 to CDN / bucket URL) - **Gallery** — browse, search, batch delete; admin sees uploader badges -- **Stats** — upload/delete metrics, folder breakdown; admin sees registered user count +- **Stats** — upload/delete metrics, upload source breakdown; admin sees registered user count - **API** — global token (admin) + per-user tokens; in-app cURL snippets - **Twikoo** — `POST /api/index.php` (`image` + `token`) - **Zero-config Docker** — first-run web wizard, no secrets required upfront @@ -95,6 +95,27 @@ docker exec pichost reset-password # admin or regular user A random password is printed. Fails if the user does not exist. +Local: `npm run reset-password` or `npm run reset-password -- ` + +### Upgrading to v1.2.0 (legacy local directories) + +From v1.2.0, all images live under `data/images/`. Migration is two steps: + +1. **CLI** (manual): move images from every top-level folder under `data/` except `images` into `data/images/{original-dir}/...` +2. **Startup sync** (automatic): scan disk, fill SQLite index, normalize legacy keys, remove orphan rows + +```bash +docker exec pichost migrate # preview + data/mapping.json +docker exec pichost migrate --apply # move files +# upgrade image and restart — index sync logs appear on startup +``` + +Local: `npm run migrate` / `npm run migrate -- --apply`. + +Full guide (layout, logs, URLs, **other image hosts**): [`docs/migration-to-v1.2.en.md`](docs/migration-to-v1.2.en.md) + +Skip the CLI if you only use object storage or files are already under `data/images/`. + If you cloned the repo, you can also use `docker compose up -d` (see `docker-compose.yml`). ### Local development @@ -130,19 +151,19 @@ Open `http://localhost:3000/setup` to create the admin account. Data defaults to | Gallery list / search / delete | Session or token | Users: own only; admin: all | | Direct link `GET /images/...` | None (Referer rules) | Public if URL is known | -Regular users are limited to the `images/` folder; custom `folder` requires admin or global token. +All uploads are stored under `images/`. ## Storage layout ``` /data ├── pichost.db # SQLite: users, sessions, settings, storage_backends, images index -├── images/ # Default upload folder (when using local backend) -├── blog/ # Example custom folder -└── twikoo/ # Twikoo comment images +└── images/ # All images (local backend); may include migrated subpaths ``` -Pattern: `folder/YYYY/MM/randomId.webp` + `.meta.json`. With cloud backends, blobs live in the bucket; SQLite `images` tracks `backend_id` and `key`. Back up all of `/data` plus bucket data. +New uploads: `images/randomId.webp` or `images/YYYY/MM/randomId.webp`. Migrated legacy paths may look like `images/blog/...`. Metadata lives in SQLite `images`. With cloud backends, blobs live in the bucket. Back up `data/images/`, `pichost.db`, and bucket data. + +Upgrading from v1.1.x with parallel folders such as `data/blog/`: see [Upgrading to v1.2.0](#upgrading-to-v120-legacy-local-directories) or [`docs/migration-to-v1.2.en.md`](docs/migration-to-v1.2.en.md). ## API overview @@ -158,7 +179,6 @@ Upload example: ```bash curl -X POST "https://pic.example.com/api/images/upload" \ -H "Auth-Token: YOUR_TOKEN" \ - -F "folder=blog" \ -F "image=@./demo.png" ``` @@ -222,13 +242,16 @@ npm run start # Run .output npm run lint # ESLint npm run typecheck # Type check npm test # Unit tests +npm run reset-password # Reset password (optional username) +npm run migrate # Preview legacy dir migration (-- --apply to run) ``` ## Roadmap | Version | Plan | | ---------- | -------------------------------------------------------------- | -| **v1.1.5** | Gallery preview reliability: auto-retry, storage verify, no-cache errors (current `main`) | +| **v1.2.0** | Unified `images/` storage, CLI migration, startup index sync, upload source stats (in progress) | +| **v1.1.5** | Gallery preview reliability: auto-retry, storage verify, no-cache errors | | **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 | diff --git a/README.md b/README.md index 1cb9452..c0c8d30 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ | 分支 | 说明 | | ---- | ---- | -| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.5** 主线:本地磁盘 + 多对象存储、双域名双向隔离、[`/storage`](docs/README.md) 存储管理、混合直链 | +| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.2.0**(开发中)— 统一 `images/` 存储、多后端、双域名隔离、[`/storage`](docs/README.md)、[`迁移指南`](docs/migration-to-v1.2.md) | | [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2 专用线**:面向「只用 R2」的部署,预设与配置更聚焦 R2 | 日常使用、需要管理多种存储后端,请直接用 **main**。 @@ -59,14 +59,14 @@ git checkout cloudflare ## 特性 -- **拖拽 / 点击 / Ctrl+V 粘贴**上传;管理员可自定义 `folder` 目录 +- **拖拽 / 点击 / Ctrl+V 粘贴**上传;图片统一存入 `images/` - **服务端 WebP 压缩**(sharp)、Referer 防盗链、可配置访问域名 - **多用户**:账号密码登录;管理员可开放注册;普通用户仅见自己的图片 - **上传偏好**(首页卡片翻转):客户端预压缩、自动复制链接、按用户自动删除 - **多后端存储**:本地磁盘 + S3 兼容(Cloudflare R2 / 腾讯云 COS / 阿里云 OSS / AWS S3);管理员在 **存储**(`/storage`)页管理,新上传写入默认后端 - **混合直链**:`proxy`(PicHost 同源代理)或 `public`(302 到 CDN / 桶公网地址) - **图库**:浏览、搜索、批量删除;管理员可查看上传者标签 -- **统计**:上传/删除趋势、目录分布;管理员额外显示注册用户数量 +- **统计**:上传/删除趋势、上传来源分布;管理员额外显示注册用户数量 - **API**:全局 Token(管理员)+ 每用户个人 Token;后台可复制 cURL - **Twikoo**:`POST /api/index.php`(`image` + `token`) - **Docker 零配置**:首次访问 Web 引导创建管理员,无需预先写密钥 @@ -100,6 +100,29 @@ docker exec pichost reset-password 用户名 本地:`npm run reset-password` 或 `npm run reset-password -- 用户名` +### 升级到 v1.2.0(本地磁盘遗留目录) + +v1.2.0 起图片统一在 `data/images/`。迁移分两步: + +1. **CLI 搬文件**(手动,升级前后均可):把 `data/` 下除 `images` 外各顶层目录里的图片迁入 `data/images/{原目录名}/...` +2. **启动时同步索引**(自动):扫描 `data/images/` 补全 SQLite、归一化遗留 key、清理无文件的孤儿记录 + +```bash +# 1. 预览迁移并生成 data/mapping.json(旧 key → 新 key) +docker exec pichost migrate + +# 2. 确认后执行文件移动 +docker exec pichost migrate --apply + +# 3. 升级镜像并重启;启动日志会打印索引同步过程 +``` + +本地:`npm run migrate` / `npm run migrate -- --apply`。 + +详细说明(结构对比、日志含义、外链替换、**其它图床迁入**):[`docs/migration-to-v1.2.md`](docs/migration-to-v1.2.md) + +仅对象存储、或图片已在 `data/images/` 下时,可跳过 CLI,仅依赖启动同步。 + 已克隆仓库时也可用 `docker compose up -d`(见仓库内 `docker-compose.yml`)。 ### 本地开发 @@ -135,19 +158,19 @@ npm run dev | 图库列表 / 搜索 / 删除 | Session 或 Token | 普通用户仅自己;管理员全部 | | 图片直链 `GET /images/...` | 无(Referer 防盗链) | 知道 URL 即可访问 | -普通用户上传目录固定为 `images/`;自定义 `folder` 仅管理员或全局 Token 可用。 +所有上传统一存入 `images/` 目录。 ## 存储结构 ``` /data ├── pichost.db # SQLite:用户、会话、设置、storage_backends、images 索引 -├── images/ # 默认上传目录(本地后端时) -├── blog/ # 管理员自定义 folder 示例 -└── twikoo/ # Twikoo 评论图片 +└── images/ # 全部图片(本地后端时);可含迁移子路径如 file/、年/月/日/ ``` -路径规则:`目录/年/月/随机ID.webp` + 同名 `.meta.json`。云存储时文件在对应桶内,SQLite `images` 表记录 `backend_id` 与 `key`。备份请包含整个 `/data` 与云桶数据。 +路径规则:新上传为 `images/随机ID.webp` 或 `images/年/月/随机ID.webp`;迁移遗留可为更深子路径(如 `images/blog/...`)。元数据在 SQLite `images` 表;云存储时文件在对应桶内。备份请包含 `data/images/`、`pichost.db` 与云桶数据。 + +从 v1.1.x 升级且存在 `data/blog/` 等并列目录时,见 [升级到 v1.2.0](#升级到-v120本地磁盘遗留目录) 或 [`docs/migration-to-v1.2.md`](docs/migration-to-v1.2.md)。 ## API 概览 @@ -163,7 +186,6 @@ npm run dev ```bash curl -X POST "https://pic.example.com/api/images/upload" \ -H "Auth-Token: YOUR_TOKEN" \ - -F "folder=blog" \ -F "image=@./demo.png" ``` @@ -226,14 +248,17 @@ npm run build # 生产构建 npm run start # 运行 .output npm run lint # ESLint npm run typecheck # 类型检查 -npm test # 单元测试 +npm test # 单元测试 +npm run reset-password # 重置密码(可选用户名参数) +npm run migrate # 预览遗留目录迁移(加 -- --apply 执行) ``` ## 路线图 | 版本 | 说明 | | ---------- | ------------------------------------------------- | -| **v1.1.5** | 图库预览可靠性:自动重试、出图前校验、错误响应禁缓存(当前 main) | +| **v1.2.0** | 统一 `images/` 存储、遗留目录 CLI 迁移、启动索引同步、上传来源统计(开发中) | +| **v1.1.5** | 图库预览可靠性:自动重试、出图前校验、错误响应禁缓存 | | **v1.1.4** | 存储路径扁平/分组、basename 外链、双域名双向隔离 | | **v1.1.3** | 双域名分离、隐藏 `images/` 前缀、版本更新提示 | | **v1.1.2** | 操作日志、图库存储筛选、上传限流 | diff --git a/app/components/ApiCurlBlock.vue b/app/components/ApiCurlBlock.vue new file mode 100644 index 0000000..15ae6ba --- /dev/null +++ b/app/components/ApiCurlBlock.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/components/ImageCard.vue b/app/components/ImageCard.vue index b6790d2..184c2a8 100644 --- a/app/components/ImageCard.vue +++ b/app/components/ImageCard.vue @@ -1,7 +1,6 @@ - - diff --git a/app/composables/useImageList.ts b/app/composables/useImageList.ts index 6b7a9f5..1de45b3 100644 --- a/app/composables/useImageList.ts +++ b/app/composables/useImageList.ts @@ -12,20 +12,13 @@ export function useImageList() { const totalPages = ref(1) const total = ref(0) const loading = ref(false) - const folderTotal = ref(null) + const totalCount = ref(null) const loadingTotal = ref(false) const searchQuery = ref('') const activeSearch = ref('') - const activeFolder = ref('all') const activeStorageBackend = ref('all') const storageBackendOptions = ref>([]) - function folderQueryParams(): Record { - const folder = activeFolder.value.trim() - if (!folder || folder === 'all') return {} - return { folder } - } - function storageQueryParams(): Record { const backendId = activeStorageBackend.value.trim() if (!backendId || backendId === 'all') return {} @@ -33,16 +26,7 @@ export function useImageList() { } function listQueryParams(): Record { - return { - ...folderQueryParams(), - ...storageQueryParams() - } - } - - function matchesActiveFolder(key: string) { - const folder = activeFolder.value.trim() - if (!folder || folder === 'all') return true - return key.startsWith(`${folder}/`) + return storageQueryParams() } function matchesActiveStorage(item: ImageItem) { @@ -74,9 +58,9 @@ export function useImageList() { credentials: 'include', query: listQueryParams() }) - folderTotal.value = data.total + totalCount.value = data.total } catch { - folderTotal.value = null + totalCount.value = null } finally { loadingTotal.value = false } @@ -138,27 +122,13 @@ export function useImageList() { await Promise.all([fetchList(1), fetchTotal()]) } - async function initializeList(folder = 'all') { - const next = folder.trim() || 'all' - activeFolder.value = next + async function initializeList() { page.value = 1 activeSearch.value = '' searchQuery.value = '' await Promise.all([fetchList(1), fetchTotal(), loadStorageBackendOptions()]) } - async function setActiveFolder(folder: string) { - const next = folder.trim() || 'all' - if (next === activeFolder.value) return - activeFolder.value = next - page.value = 1 - if (activeSearch.value) { - await Promise.all([fetchSearch(1), fetchTotal()]) - } else { - await Promise.all([fetchList(1), fetchTotal()]) - } - } - async function setActiveStorageBackend(backendId: string) { const next = backendId.trim() || 'all' if (next === activeStorageBackend.value) return @@ -192,14 +162,12 @@ export function useImageList() { } function prependItems(newItems: ImageItem[]) { - const matched = newItems.filter(item => - matchesActiveFolder(item.key) && matchesActiveStorage(item) - ) + const matched = newItems.filter(item => matchesActiveStorage(item)) if (!matched.length || page.value !== 1) return items.value = [...matched, ...items.value] total.value += matched.length - if (folderTotal.value !== null) { - folderTotal.value += matched.length + if (totalCount.value !== null) { + totalCount.value += matched.length } } @@ -210,8 +178,8 @@ export function useImageList() { if (removed > 0) { total.value = Math.max(0, total.value - removed) } - if (folderTotal.value !== null && removed > 0) { - folderTotal.value = Math.max(0, folderTotal.value - removed) + if (totalCount.value !== null && removed > 0) { + totalCount.value = Math.max(0, totalCount.value - removed) } } @@ -223,34 +191,30 @@ export function useImageList() { }) const listSummary = computed(() => { - const folder = activeFolder.value.trim() || 'all' - const folderLabel = folder === 'all' ? t('stats.allFolders') : folder const storageLabel = activeStorageLabel.value - if (loadingTotal.value && folderTotal.value === null) { + if (loadingTotal.value && totalCount.value === null) { return storageLabel - ? `${folderLabel} · ${storageLabel} · ${t('stats.counting')}` - : `${folderLabel} · ${t('stats.counting')}` + ? `${storageLabel} · ${t('stats.counting')}` + : t('stats.counting') } if (activeSearch.value) { if (storageLabel) { return t('stats.summarySearchStorage', { - folder: folderLabel, - storage: storageLabel, q: activeSearch.value, - total: total.value + total: total.value, + storage: storageLabel }) } - return t('stats.summarySearch', { folder: folderLabel, q: activeSearch.value, total: total.value }) + return t('stats.summarySearch', { q: activeSearch.value, total: total.value }) } - const count = folderTotal.value === null ? total.value : folderTotal.value + const count = totalCount.value === null ? total.value : totalCount.value if (storageLabel) { return t('stats.summaryTotalStorage', { - folder: folderLabel, - storage: storageLabel, - total: count + total: count, + storage: storageLabel }) } - return t('stats.summaryTotal', { folder: folderLabel, total: count }) + return t('stats.summaryTotal', { total: count }) }) return { @@ -259,7 +223,6 @@ export function useImageList() { totalPages, total, loading, - activeFolder, activeStorageBackend, storageBackendOptions, searchQuery, @@ -270,7 +233,6 @@ export function useImageList() { fetchTotal, refreshList, initializeList, - setActiveFolder, setActiveStorageBackend, loadStorageBackendOptions, submitSearch, diff --git a/app/composables/useImageUpload.ts b/app/composables/useImageUpload.ts index e9d7745..ce8242a 100644 --- a/app/composables/useImageUpload.ts +++ b/app/composables/useImageUpload.ts @@ -17,8 +17,7 @@ export function useImageUpload() { async function uploadFiles( files: File[], - folder = 'images', - options?: { notify?: boolean, includeFolder?: boolean } + options?: { notify?: boolean } ) { if (uploading.value) return @@ -55,9 +54,6 @@ export function useImageUpload() { })) const formData = new FormData() - if (options?.includeFolder !== false) { - formData.append('folder', folder.trim() || 'images') - } for (const file of prepared) { formData.append('files', file, file.name) } diff --git a/app/composables/useUploadPreferences.ts b/app/composables/useUploadPreferences.ts index 8757c24..f61cca9 100644 --- a/app/composables/useUploadPreferences.ts +++ b/app/composables/useUploadPreferences.ts @@ -3,7 +3,6 @@ const STORAGE_KEY = 'pichost.upload-preferences' export type CopyFormat = 'url' | 'markdown' | 'html' export interface UploadPreferences { - defaultFolder: string compressEnabled: boolean autoCopyMarkdown: boolean copyFormat: CopyFormat @@ -11,7 +10,6 @@ export interface UploadPreferences { } const DEFAULTS: UploadPreferences = { - defaultFolder: 'images', compressEnabled: false, autoCopyMarkdown: false, copyFormat: 'markdown', @@ -36,9 +34,6 @@ function readStored(): UploadPreferences { if (!raw) return { ...DEFAULTS } const parsed = JSON.parse(raw) as Partial return { - defaultFolder: typeof parsed.defaultFolder === 'string' && parsed.defaultFolder.trim() - ? parsed.defaultFolder.trim() - : DEFAULTS.defaultFolder, compressEnabled: parsed.compressEnabled === true, autoCopyMarkdown: parsed.autoCopyMarkdown === true, copyFormat: isCopyFormat(parsed.copyFormat) ? parsed.copyFormat : DEFAULTS.copyFormat, @@ -55,7 +50,6 @@ function writeStored(prefs: UploadPreferences) { } export function useUploadPreferences() { - const defaultFolder = useState('upload-pref-folder', () => DEFAULTS.defaultFolder) const compressEnabled = useState('upload-pref-compress', () => DEFAULTS.compressEnabled) const autoCopyMarkdown = useState('upload-pref-auto-md', () => DEFAULTS.autoCopyMarkdown) const copyFormat = useState('upload-pref-copy-format', () => DEFAULTS.copyFormat) @@ -64,7 +58,6 @@ export function useUploadPreferences() { function loadPreferences() { const stored = readStored() - defaultFolder.value = stored.defaultFolder compressEnabled.value = stored.compressEnabled autoCopyMarkdown.value = stored.autoCopyMarkdown copyFormat.value = stored.copyFormat @@ -74,7 +67,6 @@ export function useUploadPreferences() { function savePreferences() { writeStored({ - defaultFolder: defaultFolder.value.trim() || DEFAULTS.defaultFolder, compressEnabled: compressEnabled.value, autoCopyMarkdown: autoCopyMarkdown.value, copyFormat: copyFormat.value, @@ -82,14 +74,13 @@ export function useUploadPreferences() { }) } - watch([defaultFolder, compressEnabled, autoCopyMarkdown, copyFormat, clientWebpQuality], () => { + watch([compressEnabled, autoCopyMarkdown, copyFormat, clientWebpQuality], () => { if (loaded.value) { savePreferences() } }) return { - defaultFolder, compressEnabled, autoCopyMarkdown, copyFormat, diff --git a/app/pages/api.vue b/app/pages/api.vue index 5c21f75..9c68685 100644 --- a/app/pages/api.vue +++ b/app/pages/api.vue @@ -52,34 +52,13 @@ const authHeader = computed(() => const authHeaderFlag = computed(() => `-H 'Auth-Token: ${authHeader.value}'`) const apiDocs = computed(() => { - const uploadDescription = isAdmin.value - ? t('api.docs.upload.descAdmin') - : t('api.docs.upload.descUser') - const uploadCurl = isAdmin.value - ? `curl -X POST "${baseUrl.value}/api/images/upload" \\ - ${authHeaderFlag.value} \\ - -F "image=@./demo.png" - -${t('api.docs.upload.curlCustomFolder')} -curl -X POST "${baseUrl.value}/api/images/upload" \\ - ${authHeaderFlag.value} \\ - -F "folder=blog" \\ - -F "image=@./demo.png"` - : `curl -X POST "${baseUrl.value}/api/images/upload" \\ + const uploadDescription = t('api.docs.upload.desc') + const uploadCurl = `curl -X POST "${baseUrl.value}/api/images/upload" \\ ${authHeaderFlag.value} \\ -F "image=@./demo.png"` - const listDescription = isAdmin.value - ? t('api.docs.list.descAdmin') - : t('api.docs.list.descUser') - const listCurl = isAdmin.value - ? `curl "${baseUrl.value}/api/images?limit=20&page=1" \\ - ${authHeaderFlag.value} - -${t('api.docs.list.curlFolderFilter')} -curl "${baseUrl.value}/api/images?folder=blog&limit=20&page=1" \\ - ${authHeaderFlag.value}` - : `curl "${baseUrl.value}/api/images?limit=20&page=1" \\ + const listDescription = t('api.docs.list.desc') + const listCurl = `curl "${baseUrl.value}/api/images?limit=20&page=1" \\ ${authHeaderFlag.value}` return [ @@ -156,10 +135,12 @@ function methodBadgeColor(method: ApiDocItem['method']) { switch (method) { case 'GET': return 'info' as const + case 'POST': + return 'warning' as const case 'DELETE': return 'error' as const default: - return 'primary' as const + return 'neutral' as const } } @@ -219,15 +200,6 @@ async function confirmRegenerate() { } } -async function copyCurl(text: string) { - try { - await navigator.clipboard.writeText(text) - toast.add({ title: t('copy.curl'), color: 'success' }) - } catch { - toast.add({ title: t('copy.failed'), color: 'error' }) - } -} - function closeConfirmRegenerate() { confirmRegenerateOpen.value = false } @@ -404,18 +376,7 @@ watch(isAuthenticated, async (authed, prev) => {

{{ t('api.curlExample') }}

-
- -
{{ doc.curl }}
-
+ @@ -431,7 +392,7 @@ watch(isAuthenticated, async (authed, prev) => {
@@ -454,18 +415,10 @@ watch(isAuthenticated, async (authed, prev) => {

{{ t('api.curlExample') }}

-
- -
{{ twikooDoc.curl }}
-
+
diff --git a/app/pages/index.vue b/app/pages/index.vue index 5b9178e..65e6b3c 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -4,17 +4,14 @@ import type { ImageItem } from '~/types/image' const { uploading, progressItems, uploadFiles } = useImageUpload() const { - defaultFolder, autoCopyMarkdown, copyFormat, loadPreferences } = useUploadPreferences() -const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus, isAdmin } - = useAuth() +const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus } = useAuth() const toast = useToast() const { t } = useI18n() -const folderOptions = ref(['images']) const sessionItems = ref([]) const deletingKeys = ref>(new Set()) const emptySelection = ref(new Set()) @@ -30,22 +27,9 @@ const showProgress = computed( ) ) -async function loadFolders() { - const data = await $fetch<{ folders: string[] }>('/api/folders', { - credentials: 'include' - }) - folderOptions.value = [...data.folders] - if (!folderOptions.value.includes(defaultFolder.value)) { - defaultFolder.value = folderOptions.value[0] ?? 'images' - } -} - async function loadPage() { try { loadPreferences() - if (isAdmin.value) { - await loadFolders() - } } catch (error: unknown) { handleAuthError(error) } @@ -112,20 +96,9 @@ async function handleUpload(files: File[]) { } try { - const uploadTarget = isAdmin.value - ? (defaultFolder.value.trim() || 'images') - : 'images' - const result = await uploadFiles(files, uploadTarget, { - includeFolder: isAdmin.value - }) + const result = await uploadFiles(files) if (result?.items.length) { sessionItems.value = [...result.items, ...sessionItems.value] - if (isAdmin.value) { - const folder = uploadTarget - if (!folderOptions.value.includes(folder)) { - folderOptions.value = [...folderOptions.value, folder] - } - } if (autoCopyMarkdown.value && result.items[0]) { const first = result.items[0] const text diff --git a/app/pages/logs.vue b/app/pages/logs.vue index 368c44b..706e684 100644 --- a/app/pages/logs.vue +++ b/app/pages/logs.vue @@ -38,8 +38,7 @@ const actionItems = computed(() => [ const sourceItems = computed(() => [ { label: t('logs.filterSourceAll'), value: 'all' as const }, { label: t('logs.sourceWeb'), value: 'web' as const }, - { label: t('logs.sourceApi'), value: 'api' as const }, - { label: t('logs.sourceTwikoo'), value: 'twikoo' as const } + { label: t('logs.sourceApi'), value: 'api' as const } ]) const userItems = computed(() => { @@ -93,8 +92,6 @@ function sourceLabel(source: LogSource) { return t('logs.sourceWeb') case 'api': return t('logs.sourceApi') - case 'twikoo': - return t('logs.sourceTwikoo') } } diff --git a/app/pages/settings/index.vue b/app/pages/settings/index.vue index d5ec014..344a441 100644 --- a/app/pages/settings/index.vue +++ b/app/pages/settings/index.vue @@ -27,12 +27,10 @@ interface SettingsResponse { } const { isChecking, isAuthenticated, checkSession, handleAuthError, fetchStatus, isAdmin } = useAuth() -const { defaultFolder, loadPreferences } = useUploadPreferences() const toast = useToast() const { t } = useI18n() const settings = ref(null) -const folderOptions = ref(['images']) const savingServer = ref(false) const refererDraft = ref('') @@ -42,7 +40,6 @@ const hideFolderInUrlDraft = ref(false) const storageUseDatePathDraft = ref(true) const allowRegistrationDraft = ref(false) const domainSeparationDraft = ref(false) -const hideFolderConfirmOpen = ref(false) interface ReleaseCheckResponse { currentVersion: string @@ -53,10 +50,6 @@ interface ReleaseCheckResponse { const releaseCheck = ref(null) -const folderSelectItems = computed(() => - folderOptions.value.map(folder => ({ label: folder, value: folder })) -) - const hasServerChanges = computed(() => { if (!settings.value) return false return refererDraft.value !== settings.value.allowedRefererHosts @@ -131,28 +124,6 @@ function handlePatchError(error: unknown, fallback: string) { toast.add({ title: fallback, color: 'error' }) } -function onHideFolderDraftChange(next: boolean | 'indeterminate') { - if (next !== true) { - hideFolderInUrlDraft.value = false - return - } - if (folderOptions.value.length > 1) { - hideFolderConfirmOpen.value = true - return - } - hideFolderInUrlDraft.value = true -} - -function confirmHideFolderEnable() { - hideFolderInUrlDraft.value = true - hideFolderConfirmOpen.value = false -} - -function cancelHideFolderEnable() { - hideFolderInUrlDraft.value = false - hideFolderConfirmOpen.value = false -} - function onDomainSeparationDraftChange(next: boolean | 'indeterminate') { const enabled = next === true domainSeparationDraft.value = enabled @@ -186,16 +157,6 @@ async function saveServerSettings() { } } -async function loadFolders() { - const data = await $fetch<{ folders: string[] }>('/api/folders', { - credentials: 'include' - }) - folderOptions.value = [...data.folders] - if (!folderOptions.value.includes(defaultFolder.value)) { - defaultFolder.value = folderOptions.value[0] ?? 'images' - } -} - async function loadSettings() { try { const data = await $fetch('/api/settings', { @@ -225,8 +186,7 @@ async function loadPage() { await navigateTo('/') return } - loadPreferences() - await Promise.all([loadSettings(), loadFolders(), checkLatestRelease()]) + await Promise.all([loadSettings(), checkLatestRelease()]) } onMounted(async () => { @@ -354,7 +314,7 @@ watch(isAuthenticated, async (authed, prev) => { :model-value="hideFolderInUrlDraft" class="mt-0.5 shrink-0" :disabled="settings.hideFolderInUrlSource === 'env'" - @update:model-value="onHideFolderDraftChange" + @update:model-value="(v) => hideFolderInUrlDraft = v === true" /> @@ -375,50 +335,11 @@ watch(isAuthenticated, async (authed, prev) => { -
-
- -

- {{ t('settings.defaultFolderHint') }} -

- -
+
+

+ {{ t('settings.domainSettings') }} +

-
-
- - - {{ sourceBadge(settings.refererSource).label }} - -
-

- {{ t('settings.refererHint') }} -

- -
-
-
- -
-

- {{ t('settings.domainSettings') }} -

- -
+ +
+
+

+ {{ t('settings.accessControl') }} +

+ + {{ sourceBadge(settings.refererSource).label }} + +
+

+ {{ t('settings.refererHint') }} +

+ +
@@ -618,57 +564,11 @@ watch(isAuthenticated, async (authed, prev) => { - - - -