feat: enhance storage provider management and localization support

- Added support for new storage providers including Backblaze B2 and GitHub.
- Introduced a new UI schema for storage provider configuration, allowing for better user experience.
- Updated localization files to include new keys for storage provider fields and usage metrics.
- Refactored existing storage provider logic to accommodate new categories and improve overall structure.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-20 00:16:46 +08:00
parent bc9c87d008
commit dc23b2868e
68 changed files with 1973 additions and 553 deletions

View File

@@ -13,7 +13,7 @@ import type {
BuilderPluginESMImporter,
BuilderPluginEventPayloads,
} from '../plugins/types.js'
import type { StorageProviderFactory } from '../storage/factory.js'
import type { StorageProviderFactory, StorageProviderRegistrationOptions } from '../storage/factory.js'
import type { StorageConfig } from '../storage/index.js'
import { StorageFactory, StorageManager } from '../storage/index.js'
import type { BuilderConfig, UserBuilderSettings } from '../types/config.js'
@@ -580,8 +580,16 @@ export class AfilmoryBuilder {
return this.ensureStorageManager()
}
registerStorageProvider(provider: string, factory: StorageProviderFactory): void {
StorageFactory.registerProvider(provider, factory)
setStorageManager(manager: StorageManager): void {
this.storageManager = manager
}
registerStorageProvider(
provider: string,
factory: StorageProviderFactory,
options?: StorageProviderRegistrationOptions,
): void {
StorageFactory.registerProvider(provider, factory, options)
if (this.getStorageConfig().provider === provider) {
this.storageManager = null

View File

@@ -36,7 +36,20 @@ export type {
BuilderPluginHooks,
BuilderPluginReference,
} from './plugins/types.js'
export type { ProgressCallback, ScanProgress, StorageConfig, StorageObject, StorageProvider } from './storage/index.js'
export type {
LocalStorageConfig,
LocalStorageProviderName,
ProgressCallback,
RemoteStorageConfig,
RemoteStorageProviderName,
ScanProgress,
StorageConfig,
StorageObject,
StorageProvider,
StorageProviderCategory,
} from './storage/index.js'
export type { StorageProviderFactory, StorageProviderRegistrationOptions } from './storage/index.js'
export { LOCAL_STORAGE_PROVIDERS, REMOTE_STORAGE_PROVIDERS } from './storage/index.js'
export { StorageFactory, StorageManager } from './storage/index.js'
export type { BuilderConfig, BuilderConfigInput } from './types/config.js'
export type { AfilmoryManifest, CameraInfo, LensInfo } from './types/manifest.js'

View File

@@ -11,6 +11,7 @@ export interface PhotoExecutionContext {
storageConfig: StorageConfig
normalizeStorageKey: (key: string) => string
loggers?: PhotoProcessingLoggers
prefetchedBuffers?: Map<string, Buffer>
}
const photoContextStorage = new AsyncLocalStorage<PhotoExecutionContext>()

View File

@@ -48,11 +48,11 @@ export async function preprocessImage(
photoKey: string,
): Promise<{ rawBuffer: Buffer; processedBuffer: Buffer } | null> {
const loggers = getGlobalLoggers()
const { storageManager } = getPhotoExecutionContext()
const { storageManager, prefetchedBuffers } = getPhotoExecutionContext()
try {
// 获取图片数据
const rawImageBuffer = await storageManager.getFile(photoKey)
const rawImageBuffer = prefetchedBuffers?.get(photoKey) ?? (await storageManager.getFile(photoKey))
if (!rawImageBuffer) {
loggers.image.error(`无法获取图片数据:${photoKey}`)
return null

View File

@@ -13,9 +13,13 @@ export default function b2StoragePlugin(options: B2StoragePluginOptions = {}): B
name: `afilmory:storage:${providerName}`,
hooks: {
onInit: ({ registerStorageProvider }) => {
registerStorageProvider(providerName, (config) => {
return new B2StorageProvider(config as B2Config)
})
registerStorageProvider(
providerName,
(config) => {
return new B2StorageProvider(config as B2Config)
},
{ category: 'remote' },
)
},
},
}

View File

@@ -13,9 +13,13 @@ export default function eagleStoragePlugin(options: EagleStoragePluginOptions =
name: `afilmory:storage:${providerName}`,
hooks: {
onInit: ({ registerStorageProvider }) => {
registerStorageProvider(providerName, (config) => {
return new EagleStorageProvider(config as EagleConfig)
})
registerStorageProvider(
providerName,
(config) => {
return new EagleStorageProvider(config as EagleConfig)
},
{ category: 'local' },
)
},
/**
* Inject Eagle image metadata (name, tags) into manifest items before saving.

View File

@@ -13,9 +13,13 @@ export default function githubStoragePlugin(options: GitHubStoragePluginOptions
name: `afilmory:storage:${providerName}`,
hooks: {
onInit: ({ registerStorageProvider }) => {
registerStorageProvider(providerName, (config) => {
return new GitHubStorageProvider(config as GitHubConfig)
})
registerStorageProvider(
providerName,
(config) => {
return new GitHubStorageProvider(config as GitHubConfig)
},
{ category: 'remote' },
)
},
},
}

View File

@@ -13,9 +13,13 @@ export default function localStoragePlugin(options: LocalStoragePluginOptions =
name: `afilmory:storage:${providerName}`,
hooks: {
onInit: ({ registerStorageProvider }) => {
registerStorageProvider(providerName, (config) => {
return new LocalStorageProvider(config as LocalConfig)
})
registerStorageProvider(
providerName,
(config) => {
return new LocalStorageProvider(config as LocalConfig)
},
{ category: 'local' },
)
},
},
}

View File

@@ -13,9 +13,13 @@ export default function s3StoragePlugin(options: S3StoragePluginOptions = {}): B
name: `afilmory:storage:${providerName}`,
hooks: {
onInit: ({ registerStorageProvider }) => {
registerStorageProvider(providerName, (config) => {
return new S3StorageProvider(config as S3Config)
})
registerStorageProvider(
providerName,
(config) => {
return new S3StorageProvider(config as S3Config)
},
{ category: 'remote' },
)
},
},
}

View File

@@ -1,15 +1,37 @@
import type { StorageConfig, StorageProvider } from './interfaces.js'
import type { StorageConfig, StorageProvider, StorageProviderCategory } from './interfaces.js'
import { LOCAL_STORAGE_PROVIDERS, REMOTE_STORAGE_PROVIDERS } from './interfaces.js'
export type StorageProviderFactory<T extends StorageConfig = StorageConfig> = (config: T) => StorageProvider
export type StorageProviderRegistrationOptions = {
category?: StorageProviderCategory
}
type StorageProviderRegistration = {
factory: StorageProviderFactory
category: StorageProviderCategory
}
const BUILTIN_PROVIDER_CATEGORY = new Map<string, StorageProviderCategory>([
...REMOTE_STORAGE_PROVIDERS.map((provider) => [provider, 'remote'] as const),
...LOCAL_STORAGE_PROVIDERS.map((provider) => [provider, 'local'] as const),
])
export class StorageFactory {
private static providers = new Map<string, StorageProviderFactory>()
private static providers = new Map<string, StorageProviderRegistration>()
/**
* Register or override a storage provider factory.
*/
static registerProvider(provider: string, factory: StorageProviderFactory): void {
StorageFactory.providers.set(provider, factory)
static registerProvider(
provider: string,
factory: StorageProviderFactory,
options?: StorageProviderRegistrationOptions,
): void {
StorageFactory.providers.set(provider, {
factory,
category: StorageFactory.resolveCategory(provider, options),
})
}
/**
@@ -18,16 +40,36 @@ export class StorageFactory {
* @returns 存储提供商实例
*/
static createProvider(config: StorageConfig): StorageProvider {
const factory = StorageFactory.providers.get(config.provider)
const registration = StorageFactory.providers.get(config.provider)
if (!factory) {
if (!registration) {
throw new Error(`Unsupported storage provider: ${config.provider as string}`)
}
return factory(config)
return registration.factory(config)
}
static getRegisteredProviders(): string[] {
return Array.from(StorageFactory.providers.keys())
static getRegisteredProviders(category?: StorageProviderCategory): string[] {
const entries = Array.from(StorageFactory.providers.entries())
if (!category) {
return entries.map(([provider]) => provider)
}
return entries.filter(([, registration]) => registration.category === category).map(([provider]) => provider)
}
static getProviderCategory(provider: string): StorageProviderCategory | null {
return StorageFactory.providers.get(provider)?.category ?? BUILTIN_PROVIDER_CATEGORY.get(provider) ?? null
}
private static resolveCategory(
provider: string,
options?: StorageProviderRegistrationOptions,
): StorageProviderCategory {
if (options?.category) {
return options.category
}
return BUILTIN_PROVIDER_CATEGORY.get(provider) ?? 'remote'
}
}

View File

@@ -1,8 +1,22 @@
import './providers/register.js'
// 导出接口
export type { ProgressCallback, ScanProgress, StorageConfig, StorageObject, StorageProvider } from './interfaces.js'
export type {
LocalStorageConfig,
LocalStorageProviderName,
ProgressCallback,
RemoteStorageConfig,
RemoteStorageProviderName,
ScanProgress,
StorageConfig,
StorageObject,
StorageProvider,
StorageProviderCategory,
} from './interfaces.js'
export { LOCAL_STORAGE_PROVIDERS, REMOTE_STORAGE_PROVIDERS } from './interfaces.js'
// 导出工厂类
export type { StorageProviderFactory } from './factory.js'
export type { StorageProviderFactory, StorageProviderRegistrationOptions } from './factory.js'
export { StorageFactory } from './factory.js'
// 导出管理器

View File

@@ -216,3 +216,14 @@ export interface CustomStorageConfig {
}
export type StorageConfig = S3Config | B2Config | GitHubConfig | EagleConfig | LocalConfig | CustomStorageConfig
export const REMOTE_STORAGE_PROVIDERS = ['s3', 'b2', 'github'] as const
export const LOCAL_STORAGE_PROVIDERS = ['eagle', 'local'] as const
export type RemoteStorageProviderName = (typeof REMOTE_STORAGE_PROVIDERS)[number]
export type LocalStorageProviderName = (typeof LOCAL_STORAGE_PROVIDERS)[number]
export type StorageProviderCategory = 'remote' | 'local'
export type RemoteStorageConfig = Extract<StorageConfig, { provider: RemoteStorageProviderName }>
export type LocalStorageConfig = Extract<StorageConfig, { provider: LocalStorageProviderName }>

View File

@@ -2,7 +2,7 @@ import { StorageFactory } from './factory.js'
import type { StorageConfig, StorageObject, StorageProvider, StorageUploadOptions } from './interfaces.js'
export class StorageManager {
private provider: StorageProvider
protected provider: StorageProvider
private readonly excludeFilters: Array<(key: string) => boolean> = []
constructor(config: StorageConfig) {

View File

@@ -0,0 +1,59 @@
import type { StorageProviderFactory } from '../factory.js'
import { StorageFactory } from '../factory.js'
import type {
B2Config,
EagleConfig,
GitHubConfig,
LocalConfig,
S3Config,
StorageProviderCategory,
} from '../interfaces.js'
import { B2StorageProvider } from './b2-provider.js'
import { EagleStorageProvider } from './eagle-provider.js'
import { GitHubStorageProvider } from './github-provider.js'
import { LocalStorageProvider } from './local-provider.js'
import { S3StorageProvider } from './s3-provider.js'
type BuiltinProviderRegistration = {
name: string
factory: StorageProviderFactory
category: StorageProviderCategory
}
const BUILTIN_PROVIDER_REGISTRATIONS: BuiltinProviderRegistration[] = [
{
name: 's3',
category: 'remote',
factory: (config) => new S3StorageProvider(config as S3Config),
},
{
name: 'b2',
category: 'remote',
factory: (config) => new B2StorageProvider(config as B2Config),
},
{
name: 'github',
category: 'remote',
factory: (config) => new GitHubStorageProvider(config as GitHubConfig),
},
{
name: 'local',
category: 'local',
factory: (config) => new LocalStorageProvider(config as LocalConfig),
},
{
name: 'eagle',
category: 'local',
factory: (config) => new EagleStorageProvider(config as EagleConfig),
},
]
for (const registration of BUILTIN_PROVIDER_REGISTRATIONS) {
StorageFactory.registerProvider(registration.name, registration.factory, {
category: registration.category,
})
}
export function getBuiltinStorageProviders(): readonly BuiltinProviderRegistration[] {
return BUILTIN_PROVIDER_REGISTRATIONS
}