release: v1.1.1 — session fixes, storage cleanup, logging
This commit is contained in:
1 parent
acc64751ef
commit
901fefb3f5
20 files changed
+231
-37
No files matched your search
+14
-1
@@ -2,6 +2,18 @@
|
||||
|
||||
本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。
|
||||
|
||||
## [1.1.1] - 2026-08-26
|
||||
|
||||
### 修复
|
||||
|
||||
- 移除启动时自动创建的空白「S3 兼容存储」占位;升级后自动清理旧占位记录
|
||||
- 修复管理页切换路由时反复闪现「正在验证登录状态」
|
||||
- 修复静默会话校验后设置 / 统计 / API 页不加载数据
|
||||
|
||||
### 改进
|
||||
|
||||
- Docker 日志:4xx/5xx 分级输出,记录错误原因与堆栈;上传/启动任务失败写入详细错误
|
||||
|
||||
## [1.1.0] - 2026-08-26
|
||||
|
||||
### 新增
|
||||
@@ -83,8 +95,9 @@
|
||||
|
||||
### 路线图
|
||||
|
||||
- **v1.1.0**(当前 `main`):S3、Cloudflare R2 等多后端对象存储
|
||||
- **v1.1.1**(当前 `main`):会话校验与存储占位修复、Docker 日志增强
|
||||
|
||||
[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
|
||||
[1.0.3]: https://github.com/O96u/PicHost/releases/tag/v1.0.3
|
||||
|
||||
+3
-2
@@ -32,7 +32,7 @@
|
||||
|
||||
| Branch | Notes |
|
||||
| ------ | ----- |
|
||||
| [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.0** — local disk + multi-backend object storage (R2 / COS / OSS / AWS), [`/storage`](docs/README.md) UI, hybrid URLs |
|
||||
| [**main**](https://github.com/O96u/PicHost/tree/main) (default) | **v1.1.1** — local disk + multi-backend object storage (R2 / COS / OSS / AWS), [`/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.
|
||||
@@ -193,7 +193,8 @@ npm test # Unit tests
|
||||
|
||||
| Version | Plan |
|
||||
| ---------- | -------------------------------------------------------------- |
|
||||
| **v1.1.0** | Multi-backend object storage, hybrid URLs, storage UI (current `main`) |
|
||||
| **v1.1.1** | Session check fixes, storage placeholder cleanup, Docker logging (current `main`) |
|
||||
| **v1.1.0** | Multi-backend object storage, hybrid URLs, storage UI |
|
||||
| **v1.0.4** | Password visibility toggle, legacy ADMIN_SECRET migration |
|
||||
| **v1.0.3** | CI and Docker multi-arch build fixes |
|
||||
| **v1.0.2** | i18n, mobile menus, stats/upload UI, Docker password reset |
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
| 分支 | 说明 |
|
||||
| ---- | ---- |
|
||||
| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.0** 主线:本地磁盘 + 多对象存储后端(R2 / COS / OSS / AWS)、[`/storage`](docs/README.md) 存储管理、混合直链 |
|
||||
| [**main**](https://github.com/O96u/PicHost/tree/main)(默认) | **v1.1.1** 主线:本地磁盘 + 多对象存储后端(R2 / COS / OSS / AWS)、[`/storage`](docs/README.md) 存储管理、混合直链 |
|
||||
| [**cloudflare**](https://github.com/O96u/PicHost/tree/cloudflare) | **Cloudflare R2 专用线**:面向「只用 R2」的部署,预设与配置更聚焦 R2 |
|
||||
|
||||
日常使用、需要管理多种存储后端,请直接用 **main**。
|
||||
@@ -198,7 +198,8 @@ npm test # 单元测试
|
||||
|
||||
| 版本 | 说明 |
|
||||
| ---------- | ------------------------------------------------- |
|
||||
| **v1.1.0** | S3 / R2 多后端对象存储、混合直链、存储管理页(当前 main) |
|
||||
| **v1.1.1** | 会话校验与存储占位修复、Docker 日志增强(当前 main) |
|
||||
| **v1.1.0** | S3 / R2 多后端对象存储、混合直链、存储管理页 |
|
||||
| **v1.0.4** | 密码显示切换、遗留 ADMIN_SECRET 迁移对齐 |
|
||||
| **v1.0.3** | CI 与 Docker 多架构构建修复 |
|
||||
| **v1.0.2** | 中英文、移动端菜单、统计/上传 UI、Docker 重置密码 |
|
||||
|
||||
@@ -71,8 +71,10 @@ export function useAuth() {
|
||||
const status = useState<AuthStatus>('auth-status', () => 'checking')
|
||||
const user = useState<AuthUser | null>('auth-user', () => null)
|
||||
const authStatus = useState<AuthStatusResponse | null>('auth-status-response', () => null)
|
||||
/** 是否已完成至少一次会话校验(用于避免路由切换时反复全屏 loading) */
|
||||
const hasValidatedOnce = useState('auth-validated-once', () => false)
|
||||
|
||||
const isChecking = computed(() => status.value === 'checking')
|
||||
const isChecking = computed(() => status.value === 'checking' && !hasValidatedOnce.value)
|
||||
const isAuthenticated = computed(() => status.value === 'authenticated')
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
|
||||
@@ -84,10 +86,14 @@ export function useAuth() {
|
||||
return data
|
||||
}
|
||||
|
||||
async function checkSession() {
|
||||
status.value = 'checking'
|
||||
async function checkSession(options?: { force?: boolean }) {
|
||||
const silent = !options?.force && hasValidatedOnce.value
|
||||
if (!silent) {
|
||||
status.value = 'checking'
|
||||
}
|
||||
try {
|
||||
const data = await fetchStatus()
|
||||
hasValidatedOnce.value = true
|
||||
if (data.user) {
|
||||
user.value = data.user
|
||||
status.value = 'authenticated'
|
||||
@@ -97,6 +103,7 @@ export function useAuth() {
|
||||
status.value = 'unauthenticated'
|
||||
return false
|
||||
} catch {
|
||||
hasValidatedOnce.value = true
|
||||
user.value = null
|
||||
status.value = 'unauthenticated'
|
||||
return false
|
||||
@@ -129,6 +136,7 @@ export function useAuth() {
|
||||
user.value = result.user
|
||||
}
|
||||
status.value = 'authenticated'
|
||||
hasValidatedOnce.value = true
|
||||
await fetchStatus()
|
||||
return { ok: true }
|
||||
} catch (error: unknown) {
|
||||
@@ -159,6 +167,7 @@ export function useAuth() {
|
||||
)
|
||||
user.value = result.user
|
||||
status.value = 'authenticated'
|
||||
hasValidatedOnce.value = true
|
||||
await fetchStatus()
|
||||
return { ok: true }
|
||||
} catch (error: unknown) {
|
||||
@@ -194,6 +203,7 @@ export function useAuth() {
|
||||
)
|
||||
user.value = result.user
|
||||
status.value = 'authenticated'
|
||||
hasValidatedOnce.value = true
|
||||
await fetchStatus()
|
||||
return { ok: true }
|
||||
} catch (error: unknown) {
|
||||
@@ -229,6 +239,7 @@ export function useAuth() {
|
||||
)
|
||||
user.value = result.user
|
||||
status.value = 'authenticated'
|
||||
hasValidatedOnce.value = true
|
||||
await fetchStatus()
|
||||
return { ok: true }
|
||||
} catch (error: unknown) {
|
||||
@@ -271,6 +282,7 @@ export function useAuth() {
|
||||
status.value = 'unauthenticated'
|
||||
user.value = null
|
||||
authStatus.value = null
|
||||
hasValidatedOnce.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -240,10 +240,13 @@ onMounted(async () => {
|
||||
return
|
||||
}
|
||||
await checkSession()
|
||||
if (isAuthenticated.value) {
|
||||
await loadSettings()
|
||||
}
|
||||
})
|
||||
|
||||
watch(isAuthenticated, async (authed) => {
|
||||
if (authed) {
|
||||
watch(isAuthenticated, async (authed, prev) => {
|
||||
if (authed && prev === false) {
|
||||
await nextTick()
|
||||
await loadSettings()
|
||||
} else {
|
||||
|
||||
@@ -161,11 +161,15 @@ onMounted(async () => {
|
||||
await checkSession()
|
||||
if (isAuthenticated.value && !isAdmin.value) {
|
||||
await navigateTo('/')
|
||||
return
|
||||
}
|
||||
if (isAuthenticated.value && isAdmin.value) {
|
||||
await loadPage()
|
||||
}
|
||||
})
|
||||
|
||||
watch(isAuthenticated, async (authed) => {
|
||||
if (authed) {
|
||||
watch(isAuthenticated, async (authed, prev) => {
|
||||
if (authed && prev === false) {
|
||||
if (!isAdmin.value) {
|
||||
await navigateTo('/')
|
||||
return
|
||||
|
||||
@@ -316,6 +316,9 @@ onMounted(async () => {
|
||||
return
|
||||
}
|
||||
await checkSession()
|
||||
if (isAuthenticated.value) {
|
||||
await refreshAll()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pichost",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pichost",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pichost",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"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,5 +1,6 @@
|
||||
import { requireAdminAuth } from '../../utils/access'
|
||||
import { createApiError } from '../../utils/api-error'
|
||||
import { logException } from '../../utils/logger'
|
||||
import {
|
||||
listActivityLogs,
|
||||
type LogAction,
|
||||
@@ -58,7 +59,7 @@ export default defineEventHandler(async (event) => {
|
||||
totalPages: result.totalPages
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[logs] list failed', error)
|
||||
logException('activity logs list failed', error)
|
||||
createApiError(event, 'INVALID_REQUEST', '读取操作记录失败', 500)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getImageUserFilter, requireUserAuth } from '../utils/access'
|
||||
import { createApiError } from '../utils/api-error'
|
||||
import { logException } from '../utils/logger'
|
||||
import { countUsers, getActivityStats } from '../utils/db'
|
||||
import {
|
||||
countImages,
|
||||
@@ -46,7 +47,7 @@ export default defineEventHandler(async (event) => {
|
||||
byFolder: scoped.byFolder
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[stats] failed', error)
|
||||
logException('stats failed', error)
|
||||
createApiError(event, 'INVALID_REQUEST', '读取统计失败', 500)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { clientIp, logError, logException, logWarn, serializeError } from '../utils/logger'
|
||||
|
||||
/**
|
||||
* 记录未捕获的 H3/Nitro 错误(含 createApiError 抛出的 4xx/5xx)。
|
||||
*/
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
nitroApp.hooks.hook('error', (error, { event }) => {
|
||||
const serialized = serializeError(error)
|
||||
const status = typeof serialized.statusCode === 'number'
|
||||
? serialized.statusCode
|
||||
: 500
|
||||
|
||||
if (!event) {
|
||||
logError('unhandled error', serialized)
|
||||
return
|
||||
}
|
||||
|
||||
const path = getRequestURL(event).pathname
|
||||
const method = getMethod(event)
|
||||
const base = {
|
||||
method,
|
||||
path,
|
||||
status,
|
||||
ip: clientIp(event),
|
||||
message: serialized.message,
|
||||
...(serialized.statusMessage ? { reason: serialized.statusMessage } : {}),
|
||||
...(serialized.data ? { data: serialized.data } : {})
|
||||
}
|
||||
|
||||
if (!path.startsWith('/api/')) {
|
||||
if (status >= 500) {
|
||||
logException(`${method} ${path}`, error, base)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (status >= 500) {
|
||||
logException('API error', error, base)
|
||||
} else if (status >= 400) {
|
||||
logWarn('API rejected', base)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clientIp, logInfo } from '../utils/logger'
|
||||
import { clientIp, logError, logInfo, logWarn } from '../utils/logger'
|
||||
|
||||
/**
|
||||
* 只记录 /api/*,避免 /images、/twikoo 直链刷屏。
|
||||
@@ -19,11 +19,24 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
const status = getResponseStatus(event)
|
||||
const started = event.context._reqStartedAt as number | undefined
|
||||
const ms = started ? Date.now() - started : undefined
|
||||
const reason = status >= 400 ? getResponseStatusText(event) : undefined
|
||||
|
||||
logInfo(`${method} ${path}`, {
|
||||
const detail: Record<string, unknown> = {
|
||||
status,
|
||||
ms,
|
||||
ip: clientIp(event)
|
||||
})
|
||||
}
|
||||
if (reason && reason !== String(status)) {
|
||||
detail.reason = reason
|
||||
}
|
||||
|
||||
const line = `${method} ${path}`
|
||||
if (status >= 500) {
|
||||
logError(line, detail)
|
||||
} else if (status >= 400) {
|
||||
logWarn(line, detail)
|
||||
} else {
|
||||
logInfo(line, detail)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { getDataDir } from '../utils/data-dir'
|
||||
import { runAutoDeleteCleanup } from '../utils/auto-delete'
|
||||
import { migrateLocalImagesToIndex } from '../utils/image-index'
|
||||
import { ensureDefaultBackends } from '../utils/storage-backends'
|
||||
import { logInfo } from '../utils/logger'
|
||||
import { logException, logInfo } from '../utils/logger'
|
||||
|
||||
export default defineNitroPlugin(() => {
|
||||
const port = process.env.NITRO_PORT || process.env.PORT || '6892'
|
||||
@@ -20,11 +20,15 @@ export default defineNitroPlugin(() => {
|
||||
if (count > 0) {
|
||||
logInfo('migrated local images to index', { count })
|
||||
}
|
||||
}).catch((error) => {
|
||||
logException('migrate local images to index failed', error)
|
||||
})
|
||||
|
||||
void runAutoDeleteCleanup().then((result) => {
|
||||
if (!result.skipped && (result.deleted > 0 || result.failed > 0)) {
|
||||
logInfo('startup auto-delete', { ...result })
|
||||
}
|
||||
}).catch((error) => {
|
||||
logException('startup auto-delete failed', error)
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { insertActivityLog, listAdminUserIds, listUserAutoDeletePolicies, type A
|
||||
import { getGlobalAutoDeletePolicy } from './env'
|
||||
import { deleteImage, type StoredImage } from './storage'
|
||||
import { listImageKeysForAutoDelete } from './image-index'
|
||||
import { logInfo, logWarn } from './logger'
|
||||
import { logInfo, logWarn, serializeError } from './logger'
|
||||
|
||||
export interface AutoDeleteResult {
|
||||
deleted: number
|
||||
@@ -95,7 +95,7 @@ export async function runAutoDeleteCleanup(): Promise<AutoDeleteResult> {
|
||||
})
|
||||
deleted++
|
||||
} catch (error) {
|
||||
logWarn('auto-delete failed', { key: image.key, error })
|
||||
logWarn('auto-delete failed', { key: image.key, error: serializeError(error) })
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@ import { DatabaseSync } from 'node:sqlite'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { getDataDir } from './data-dir'
|
||||
import { logException } from './logger'
|
||||
|
||||
export type LogAction = 'upload' | 'delete'
|
||||
export type LogSource = 'web' | 'api' | 'twikoo'
|
||||
@@ -404,7 +405,10 @@ export function insertActivityLog(input: ActivityLogInput): void {
|
||||
createdAt
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[activity-log] insert failed', error)
|
||||
logException('activity-log insert failed', error, {
|
||||
action: input.action,
|
||||
key: input.key
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { serializeError } from './logger'
|
||||
|
||||
describe('serializeError', () => {
|
||||
it('extracts message and status from H3-like errors', () => {
|
||||
const error = new Error('bucket not found') as Error & {
|
||||
statusCode: number
|
||||
statusMessage: string
|
||||
data: { error: { code: string } }
|
||||
}
|
||||
error.statusCode = 404
|
||||
error.statusMessage = '存储桶不存在'
|
||||
error.data = { error: { code: 'NOT_FOUND' } }
|
||||
|
||||
expect(serializeError(error)).toMatchObject({
|
||||
name: 'Error',
|
||||
message: 'bucket not found',
|
||||
statusCode: 404,
|
||||
statusMessage: '存储桶不存在',
|
||||
data: { error: { code: 'NOT_FOUND' } }
|
||||
})
|
||||
})
|
||||
|
||||
it('includes stack for 5xx errors at info log level', () => {
|
||||
const error = new Error('db locked') as Error & { statusCode: number }
|
||||
error.statusCode = 500
|
||||
error.stack = 'Error: db locked\n at test'
|
||||
|
||||
const serialized = serializeError(error)
|
||||
expect(serialized.stack).toBe(error.stack)
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,12 @@ const LEVEL_ORDER: Record<LogLevel, number> = {
|
||||
error: 40
|
||||
}
|
||||
|
||||
type H3LikeError = Error & {
|
||||
statusCode?: number
|
||||
statusMessage?: string
|
||||
data?: unknown
|
||||
}
|
||||
|
||||
function currentLevel(): LogLevel {
|
||||
const raw = (process.env.LOG_LEVEL || 'info').toLowerCase()
|
||||
if (raw === 'debug' || raw === 'info' || raw === 'warn' || raw === 'error') {
|
||||
@@ -27,6 +33,35 @@ function formatLine(level: LogLevel, message: string, extra?: Record<string, unk
|
||||
return `[${time}] [${level.toUpperCase()}] ${message}${payload}`
|
||||
}
|
||||
|
||||
/** 将 unknown / Error / H3Error 转为可 JSON 输出的字段,便于 Docker 日志排查 */
|
||||
export function serializeError(error: unknown): Record<string, unknown> {
|
||||
if (error instanceof Error) {
|
||||
const h3 = error as H3LikeError
|
||||
const out: Record<string, unknown> = {
|
||||
name: error.name,
|
||||
message: error.message
|
||||
}
|
||||
if (h3.statusCode !== undefined) out.statusCode = h3.statusCode
|
||||
if (h3.statusMessage) out.statusMessage = h3.statusMessage
|
||||
if (h3.data !== undefined) out.data = h3.data
|
||||
if (error.stack && shouldLog('debug')) out.stack = error.stack
|
||||
else if (error.stack && (h3.statusCode ?? 500) >= 500) out.stack = error.stack
|
||||
if ('cause' in error && error.cause !== undefined) {
|
||||
out.cause = serializeError(error.cause)
|
||||
}
|
||||
return out
|
||||
}
|
||||
if (typeof error === 'string') return { message: error }
|
||||
if (error && typeof error === 'object') {
|
||||
try {
|
||||
return { detail: JSON.parse(JSON.stringify(error)) }
|
||||
} catch {
|
||||
return { message: String(error) }
|
||||
}
|
||||
}
|
||||
return { message: String(error) }
|
||||
}
|
||||
|
||||
export function logDebug(message: string, extra?: Record<string, unknown>) {
|
||||
if (!shouldLog('debug')) return
|
||||
console.debug(formatLine('debug', message, extra))
|
||||
@@ -47,6 +82,11 @@ export function logError(message: string, extra?: Record<string, unknown>) {
|
||||
console.error(formatLine('error', message, extra))
|
||||
}
|
||||
|
||||
/** message + 原始 error 对象(自动 serialize) */
|
||||
export function logException(message: string, error: unknown, extra?: Record<string, unknown>) {
|
||||
logError(message, { ...extra, error: serializeError(error) })
|
||||
}
|
||||
|
||||
export function clientIp(event: Parameters<typeof getRequestIP>[0]): string {
|
||||
return getRequestIP(event, { xForwardedFor: true }) || '-'
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { buildImageItem, sanitizeOriginalName } from './image-response'
|
||||
import { getWebpQuality } from './env'
|
||||
import { insertActivityLog } from './db'
|
||||
import { putImage } from './storage'
|
||||
import { logInfo, logWarn } from './logger'
|
||||
import { logInfo, logException } from './logger'
|
||||
|
||||
/** 可转 WebP 的位图格式;SVG(矢量)、ICO、已是 WebP 的原样存储 */
|
||||
const WEBP_CONVERTIBLE: readonly AllowedMimeType[] = [
|
||||
@@ -136,8 +136,8 @@ export async function processSingleImageUpload(
|
||||
size: compressed.bytes.byteLength,
|
||||
userId: input.userId ?? null
|
||||
})
|
||||
} catch {
|
||||
logWarn('upload failed: storage error', {
|
||||
} catch (error) {
|
||||
logException('upload failed: storage error', error, {
|
||||
name: originalName,
|
||||
source: input.source ?? 'web'
|
||||
})
|
||||
|
||||
@@ -71,18 +71,37 @@ export function ensureDefaultBackends(): void {
|
||||
`).run(LOCAL_BACKEND_ID, '本地磁盘')
|
||||
}
|
||||
|
||||
const s3 = db.prepare(`
|
||||
SELECT id FROM storage_backends WHERE id = ?
|
||||
`).get(S3_BACKEND_ID)
|
||||
cleanupPlaceholderS3Backend(db)
|
||||
}
|
||||
|
||||
if (!s3) {
|
||||
db.prepare(`
|
||||
INSERT INTO storage_backends
|
||||
(id, name, type, config_json, secret_json, serving_mode, public_url,
|
||||
quota_bytes, enabled, is_default, sort_order)
|
||||
VALUES (?, ?, 's3', '{}', '{}', 'proxy', '', NULL, 0, 0, 1)
|
||||
`).run(S3_BACKEND_ID, 'S3 兼容存储')
|
||||
/** 移除早期版本自动种下的空 S3 占位(未配置、未启用、非默认、无图片) */
|
||||
function cleanupPlaceholderS3Backend(db: DatabaseSync): void {
|
||||
const row = db.prepare(`
|
||||
SELECT * FROM storage_backends WHERE id = ?
|
||||
`).get(S3_BACKEND_ID) as StorageBackendRow | undefined
|
||||
|
||||
if (!row || row.type !== 's3' || row.is_default === 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = parseS3Config(row.config_json)
|
||||
const secrets = parseS3Secrets(row.secret_json)
|
||||
const configured = Boolean(
|
||||
config?.bucket?.trim()
|
||||
|| config?.endpoint?.trim()
|
||||
|| secrets?.accessKeyId?.trim()
|
||||
|| secrets?.secretAccessKey?.trim()
|
||||
)
|
||||
if (configured) {
|
||||
return
|
||||
}
|
||||
|
||||
const usage = getBackendUsageStats(S3_BACKEND_ID)
|
||||
if (usage.count > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM storage_backends WHERE id = ?').run(S3_BACKEND_ID)
|
||||
}
|
||||
|
||||
function rowToInfo(row: StorageBackendRow, _maskSecrets = true): StorageBackendInfo & {
|
||||
|
||||
Reference in new issue
Block a user