release: v1.2.0 — unified images/ storage and legacy migration
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

Consolidate local images under data/images/, add docker migrate CLI,
startup index sync with orphan cleanup, and remove upload folder UI/API.
This commit is contained in:
O96u committed 2026-08-28 17:18:03 +08:00
1 parent 3dee0437e6
commit 469d3be735
50 files changed
+1273 -956

No files matched your search

+68 -7
View File
@@ -1,10 +1,15 @@
import type { ReadStream } from 'node:fs'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'
import type { Readable } from 'node:stream'
import { contentTypeFromKey } from './content-type'
import { getDataDir } from './data-dir'
import {
countImages as countImagesFromIndex,
deleteImageIndex,
getFolderStorageStats as getFolderStorageStatsFromIndex,
getImageIndexAsStored,
getImageIndexRow,
getUserScopedStorageStats as getUserScopedStorageStatsFromIndex,
insertImageIndex,
listFolders as listFoldersFromIndex,
@@ -13,6 +18,7 @@ import {
listImages as listImagesFromIndex,
searchImages as searchImagesFromIndex
} from './image-index'
import { DEFAULT_FOLDER, toCanonicalImageKey, validateImageKey } from './image-key'
import {
getActiveBackend,
getBackendForKey
@@ -33,6 +39,58 @@ export type {
export { getDataDir } from './data-dir'
function keyToFilePath(key: string): string {
return join(getDataDir(), ...key.split('/'))
}
async function fileExistsAtKey(key: string): Promise<boolean> {
try {
await fs.access(keyToFilePath(key))
return true
} catch {
return false
}
}
/** 删除/变更前解析 key:接受遗留 blog/… 并映射到 images/blog/… */
export async function resolveStorageImageKey(key: string): Promise<string | null> {
if (!key || typeof key !== 'string') return null
if (validateImageKey(key)) {
if (await fileExistsAtKey(key) || getImageIndexRow(key)) return key
return null
}
if (getImageIndexRow(key)) {
const canonical = toCanonicalImageKey(key)
if (canonical && (await fileExistsAtKey(canonical) || getImageIndexRow(canonical))) {
return canonical
}
if (await fileExistsAtKey(key)) return key
return canonical ?? key
}
const canonical = toCanonicalImageKey(key)
if (canonical && (await fileExistsAtKey(canonical) || getImageIndexRow(canonical))) {
return canonical
}
return null
}
function deleteImageIndexAliases(storageKey: string, requestedKey?: string) {
deleteImageIndex(storageKey)
if (requestedKey && requestedKey !== storageKey) {
deleteImageIndex(requestedKey)
}
if (storageKey.startsWith(`${DEFAULT_FOLDER}/`)) {
const legacy = storageKey.slice(DEFAULT_FOLDER.length + 1)
if (legacy && legacy !== storageKey) {
deleteImageIndex(legacy)
}
}
}
export async function putImage(
key: string,
bytes: Uint8Array,
@@ -59,11 +117,17 @@ export async function headImage(key: string): Promise<StoredImage | null> {
const indexed = getImageIndexAsStored(key)
if (indexed) {
let contentType = indexed.contentType
if (contentType === 'application/octet-stream') {
contentType = stored.contentType !== 'application/octet-stream'
? stored.contentType
: contentTypeFromKey(key)
}
return {
...stored,
backendId: backend.id,
originalName: indexed.originalName,
contentType: indexed.contentType,
contentType,
uploadedAt: indexed.uploadedAt,
userId: indexed.userId
}
@@ -90,10 +154,10 @@ export async function createImageStream(
return backend.createStream(key, range)
}
export async function deleteImage(key: string): Promise<void> {
export async function deleteImage(key: string, requestedKey?: string): Promise<void> {
const backend = await getBackendForKey(key)
await backend.delete(key)
deleteImageIndex(key)
deleteImageIndexAliases(key, requestedKey)
}
export async function listFolders(): Promise<string[]> {
@@ -106,11 +170,10 @@ export async function listImageKeys(): Promise<string[]> {
}
export async function countImages(
folder?: string,
userFilter?: number | 'admin',
backendId?: string
): Promise<number> {
return countImagesFromIndex(folder, userFilter, backendId)
return countImagesFromIndex(userFilter, backendId)
}
export async function getFolderStorageStats(
@@ -138,7 +201,6 @@ export async function getUserScopedStorageStats(userId: number): Promise<{
export async function listImages(options: {
limit: number
page?: number
folder?: string
userFilter?: number | 'admin'
backendId?: string
}): Promise<PaginatedResult<StoredImage>> {
@@ -149,7 +211,6 @@ export async function searchImages(options: {
query: string
limit: number
page?: number
folder?: string
userFilter?: number | 'admin'
backendId?: string
}): Promise<PaginatedResult<StoredImage>> {