mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-25 07:15:36 +00:00
feat: add image processing error handling and logging
- Introduced a new error code for image processing failures. - Enhanced the DataSyncService to log effective storage configurations and errors during processing. - Updated the PhotoBuilderService to remove unused methods and streamline the code. - Refactored plugin loading to support ESM imports for better compatibility. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -14,6 +14,9 @@ export enum ErrorCode {
|
|||||||
TENANT_NOT_FOUND = 20,
|
TENANT_NOT_FOUND = 20,
|
||||||
TENANT_SUSPENDED = 21,
|
TENANT_SUSPENDED = 21,
|
||||||
TENANT_INACTIVE = 22,
|
TENANT_INACTIVE = 22,
|
||||||
|
|
||||||
|
// Image Processing
|
||||||
|
IMAGE_PROCESSING_FAILED = 30,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorDescriptor {
|
export interface ErrorDescriptor {
|
||||||
@@ -62,4 +65,9 @@ export const ERROR_CODE_DESCRIPTORS: Record<ErrorCode, ErrorDescriptor> = {
|
|||||||
httpStatus: 403,
|
httpStatus: 403,
|
||||||
message: 'Tenant is not active',
|
message: 'Tenant is not active',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ErrorCode.IMAGE_PROCESSING_FAILED]: {
|
||||||
|
httpStatus: 500,
|
||||||
|
message: 'Image processing failed',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import type {BuilderConfig, PhotoManifestItem, StorageConfig, StorageManager, StorageObject} from '@afilmory/builder';
|
import type { BuilderConfig, PhotoManifestItem, StorageConfig, StorageManager, StorageObject } from '@afilmory/builder'
|
||||||
import {
|
import { createDefaultBuilderConfig, StorageFactory } from '@afilmory/builder'
|
||||||
createDefaultBuilderConfig,
|
|
||||||
StorageFactory
|
|
||||||
} from '@afilmory/builder'
|
|
||||||
import {
|
import {
|
||||||
EagleStorageProvider,
|
EagleStorageProvider,
|
||||||
GitHubStorageProvider,
|
GitHubStorageProvider,
|
||||||
LocalStorageProvider,
|
LocalStorageProvider,
|
||||||
S3StorageProvider,
|
S3StorageProvider,
|
||||||
} from '@afilmory/builder/storage'
|
} from '@afilmory/builder/storage/index.js'
|
||||||
import type { EagleConfig, EagleRule, GitHubConfig, LocalConfig, S3Config } from '@afilmory/builder/storage/interfaces'
|
import type {
|
||||||
|
EagleConfig,
|
||||||
|
EagleRule,
|
||||||
|
GitHubConfig,
|
||||||
|
LocalConfig,
|
||||||
|
S3Config,
|
||||||
|
} from '@afilmory/builder/storage/interfaces.js'
|
||||||
import type { PhotoAssetConflictPayload, PhotoAssetConflictSnapshot, PhotoAssetManifest } from '@afilmory/db'
|
import type { PhotoAssetConflictPayload, PhotoAssetConflictSnapshot, PhotoAssetManifest } from '@afilmory/db'
|
||||||
import { CURRENT_PHOTO_MANIFEST_VERSION, photoAssets } from '@afilmory/db'
|
import { CURRENT_PHOTO_MANIFEST_VERSION, photoAssets } from '@afilmory/db'
|
||||||
|
import { createLogger } from '@afilmory/framework'
|
||||||
import { BizException, ErrorCode } from 'core/errors'
|
import { BizException, ErrorCode } from 'core/errors'
|
||||||
import { PhotoBuilderService } from 'core/modules/photo/photo.service'
|
import { PhotoBuilderService } from 'core/modules/photo/photo.service'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
@@ -68,6 +72,7 @@ interface SyncPreparation {
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class DataSyncService {
|
export class DataSyncService {
|
||||||
|
private readonly logger = createLogger('DataSyncService')
|
||||||
constructor(
|
constructor(
|
||||||
private readonly dbAccessor: DbAccessor,
|
private readonly dbAccessor: DbAccessor,
|
||||||
private readonly photoBuilderService: PhotoBuilderService,
|
private readonly photoBuilderService: PhotoBuilderService,
|
||||||
@@ -143,6 +148,8 @@ export class DataSyncService {
|
|||||||
): Promise<SyncPreparation> {
|
): Promise<SyncPreparation> {
|
||||||
const builder = this.photoBuilderService.createBuilder(builderConfig)
|
const builder = this.photoBuilderService.createBuilder(builderConfig)
|
||||||
const effectiveStorageConfig = storageConfig ?? builderConfig.storage
|
const effectiveStorageConfig = storageConfig ?? builderConfig.storage
|
||||||
|
|
||||||
|
this.logger.verbose('effectiveStorageConfig', effectiveStorageConfig)
|
||||||
this.registerStorageProviderPlugin(builder, effectiveStorageConfig)
|
this.registerStorageProviderPlugin(builder, effectiveStorageConfig)
|
||||||
if (storageConfig) {
|
if (storageConfig) {
|
||||||
this.photoBuilderService.applyStorageConfig(builder, storageConfig)
|
this.photoBuilderService.applyStorageConfig(builder, storageConfig)
|
||||||
@@ -222,7 +229,7 @@ export class DataSyncService {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const provider = storageConfig.provider as string
|
const provider = (storageConfig as StorageConfig)?.provider as string
|
||||||
const registered = StorageFactory.getRegisteredProviders()
|
const registered = StorageFactory.getRegisteredProviders()
|
||||||
if (!registered.includes(provider)) {
|
if (!registered.includes(provider)) {
|
||||||
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, {
|
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, {
|
||||||
@@ -777,7 +784,8 @@ export class DataSyncService {
|
|||||||
},
|
},
|
||||||
builder,
|
builder,
|
||||||
})
|
})
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
this.logger.error('Failed to process storage object', err)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -964,7 +972,7 @@ export class DataSyncService {
|
|||||||
const storageObject = storageObjects.find((object) => object.key === record.storageKey)
|
const storageObject = storageObjects.find((object) => object.key === record.storageKey)
|
||||||
|
|
||||||
if (!storageObject) {
|
if (!storageObject) {
|
||||||
throw new BizException(ErrorCode.COMMON_CONFLICT, {
|
throw new BizException(ErrorCode.IMAGE_PROCESSING_FAILED, {
|
||||||
message: 'Storage object no longer exists; rerun data sync before resolving.',
|
message: 'Storage object no longer exists; rerun data sync before resolving.',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -973,7 +981,7 @@ export class DataSyncService {
|
|||||||
existing: record.manifest?.data as PhotoManifestItem | undefined,
|
existing: record.manifest?.data as PhotoManifestItem | undefined,
|
||||||
})
|
})
|
||||||
if (!processResult?.item) {
|
if (!processResult?.item) {
|
||||||
throw new BizException(ErrorCode.COMMON_CONFLICT, { message: 'Failed to reprocess storage object.' })
|
throw new BizException(ErrorCode.IMAGE_PROCESSING_FAILED, { message: 'Failed to reprocess storage object.' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageSnapshot = this.createStorageSnapshot(storageObject)
|
const storageSnapshot = this.createStorageSnapshot(storageObject)
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import type {
|
|||||||
PhotoProcessorOptions,
|
PhotoProcessorOptions,
|
||||||
StorageConfig,
|
StorageConfig,
|
||||||
StorageObject,
|
StorageObject,
|
||||||
StorageProvider,
|
|
||||||
} from '@afilmory/builder'
|
} from '@afilmory/builder'
|
||||||
import { AfilmoryBuilder, processPhotoWithPipeline, StorageFactory, StorageManager } from '@afilmory/builder'
|
import { AfilmoryBuilder, processPhotoWithPipeline } from '@afilmory/builder'
|
||||||
import type { Logger as BuilderLogger } from '@afilmory/builder/logger'
|
import type { Logger as BuilderLogger } from '@afilmory/builder/logger/index.js'
|
||||||
import type { PhotoProcessingLoggers } from '@afilmory/builder/photo'
|
import type { PhotoProcessingLoggers } from '@afilmory/builder/photo/index.js'
|
||||||
import { createPhotoProcessingLoggers, setGlobalLoggers } from '@afilmory/builder/photo'
|
import { createPhotoProcessingLoggers, setGlobalLoggers } from '@afilmory/builder/photo/index.js'
|
||||||
import type { _Object } from '@aws-sdk/client-s3'
|
import type { _Object } from '@aws-sdk/client-s3'
|
||||||
import { injectable } from 'tsyringe'
|
import { injectable } from 'tsyringe'
|
||||||
|
|
||||||
@@ -42,14 +41,6 @@ export class PhotoBuilderService {
|
|||||||
return new AfilmoryBuilder(config)
|
return new AfilmoryBuilder(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
createStorageManager(config: StorageConfig): StorageManager {
|
|
||||||
return new StorageManager(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveStorageProvider(config: StorageConfig): StorageProvider {
|
|
||||||
return StorageFactory.createProvider(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyStorageConfig(builder: AfilmoryBuilder, config: StorageConfig): void {
|
applyStorageConfig(builder: AfilmoryBuilder, config: StorageConfig): void {
|
||||||
builder.getStorageManager().switchProvider(config)
|
builder.getStorageManager().switchProvider(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import type { PluginRunState } from '../plugins/manager.js'
|
|||||||
import { PluginManager } from '../plugins/manager.js'
|
import { PluginManager } from '../plugins/manager.js'
|
||||||
import type {
|
import type {
|
||||||
BuilderPluginConfigEntry,
|
BuilderPluginConfigEntry,
|
||||||
|
BuilderPluginESMImporter,
|
||||||
BuilderPluginEventPayloads,
|
BuilderPluginEventPayloads,
|
||||||
} from '../plugins/types.js'
|
} from '../plugins/types.js'
|
||||||
import { isPluginReferenceObject } from '../plugins/types.js'
|
|
||||||
import type { StorageProviderFactory } from '../storage/factory.js'
|
import type { StorageProviderFactory } from '../storage/factory.js'
|
||||||
import { StorageFactory, StorageManager } from '../storage/index.js'
|
import { StorageFactory, StorageManager } from '../storage/index.js'
|
||||||
import type { BuilderConfig } from '../types/config.js'
|
import type { BuilderConfig } from '../types/config.js'
|
||||||
@@ -555,14 +555,6 @@ export class AfilmoryBuilder {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPluginReferenceObject(ref)) {
|
|
||||||
const key = ref.resolve
|
|
||||||
if (seen.has(key)) return
|
|
||||||
seen.add(key)
|
|
||||||
references.push(ref)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginName = ref.name
|
const pluginName = ref.name
|
||||||
if (pluginName) {
|
if (pluginName) {
|
||||||
const key = `plugin:${pluginName}`
|
const key = `plugin:${pluginName}`
|
||||||
@@ -576,7 +568,7 @@ export class AfilmoryBuilder {
|
|||||||
|
|
||||||
const hasPluginWithName = (name: string): boolean => {
|
const hasPluginWithName = (name: string): boolean => {
|
||||||
return references.some((ref) => {
|
return references.some((ref) => {
|
||||||
if (typeof ref === 'string' || isPluginReferenceObject(ref)) {
|
if (typeof ref === 'string') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return ref.name === name
|
return ref.name === name
|
||||||
@@ -591,14 +583,16 @@ export class AfilmoryBuilder {
|
|||||||
this.config.repo.enable &&
|
this.config.repo.enable &&
|
||||||
!hasPluginWithName('afilmory:github-repo-sync')
|
!hasPluginWithName('afilmory:github-repo-sync')
|
||||||
) {
|
) {
|
||||||
addReference('@afilmory/builder/plugins/github-repo-sync')
|
addReference(
|
||||||
|
() => import('@afilmory/builder/plugins/github-repo-sync.js'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const storagePluginByProvider: Record<string, string> = {
|
const storagePluginByProvider: Record<string, BuilderPluginESMImporter> = {
|
||||||
s3: '@afilmory/builder/plugins/storage/s3',
|
s3: () => import('@afilmory/builder/plugins/storage/s3.js'),
|
||||||
github: '@afilmory/builder/plugins/storage/github',
|
github: () => import('@afilmory/builder/plugins/storage/github.js'),
|
||||||
eagle: '@afilmory/builder/plugins/storage/eagle',
|
eagle: () => import('@afilmory/builder/plugins/storage/eagle.js'),
|
||||||
local: '@afilmory/builder/plugins/storage/local',
|
local: () => import('@afilmory/builder/plugins/storage/local.js'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageProvider = this.config.storage.provider
|
const storageProvider = this.config.storage.provider
|
||||||
|
|||||||
@@ -96,10 +96,7 @@ export async function processImageWithSharp(
|
|||||||
if (isBitmap(imageBuffer)) {
|
if (isBitmap(imageBuffer)) {
|
||||||
try {
|
try {
|
||||||
// Convert the BMP image to JPEG format and create a new Sharp instance for the converted image.
|
// Convert the BMP image to JPEG format and create a new Sharp instance for the converted image.
|
||||||
sharpInstance = await convertBmpToJpegSharpInstance(
|
sharpInstance = await convertBmpToJpegSharpInstance(imageBuffer)
|
||||||
imageBuffer,
|
|
||||||
loggers.image.originalLogger,
|
|
||||||
)
|
|
||||||
// Update the image buffer to reflect the new JPEG data from the Sharp instance.
|
// Update the image buffer to reflect the new JPEG data from the Sharp instance.
|
||||||
processedBuffer = await sharpInstance.toBuffer()
|
processedBuffer = await sharpInstance.toBuffer()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -109,10 +106,7 @@ export async function processImageWithSharp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取图片元数据(复用 Sharp 实例)
|
// 获取图片元数据(复用 Sharp 实例)
|
||||||
const metadata = await getImageMetadataWithSharp(
|
const metadata = await getImageMetadataWithSharp(sharpInstance)
|
||||||
sharpInstance,
|
|
||||||
loggers.image.originalLogger,
|
|
||||||
)
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
loggers.image.error(`获取图片元数据失败:${photoKey}`)
|
loggers.image.error(`获取图片元数据失败:${photoKey}`)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { pathToFileURL } from 'node:url'
|
|||||||
import type {
|
import type {
|
||||||
BuilderPlugin,
|
BuilderPlugin,
|
||||||
BuilderPluginConfigEntry,
|
BuilderPluginConfigEntry,
|
||||||
|
BuilderPluginESMImporter,
|
||||||
BuilderPluginHooks,
|
BuilderPluginHooks,
|
||||||
BuilderPluginReference,
|
BuilderPluginReference,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
import { isPluginReferenceObject } from './types.js'
|
import { isPluginESMImporter } from './types.js'
|
||||||
|
|
||||||
const requireResolver = createRequire(import.meta.url)
|
const requireResolver = createRequire(import.meta.url)
|
||||||
|
|
||||||
@@ -26,16 +27,12 @@ interface NormalizedDescriptor {
|
|||||||
|
|
||||||
function normalizeDescriptor(
|
function normalizeDescriptor(
|
||||||
ref: BuilderPluginReference,
|
ref: BuilderPluginReference,
|
||||||
): NormalizedDescriptor {
|
): NormalizedDescriptor | BuilderPluginESMImporter {
|
||||||
if (typeof ref === 'string') {
|
if (typeof ref === 'string') {
|
||||||
return { specifier: ref }
|
return { specifier: ref }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return ref
|
||||||
specifier: ref.resolve,
|
|
||||||
name: ref.name,
|
|
||||||
options: ref.options,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSpecifier(
|
function resolveSpecifier(
|
||||||
@@ -127,8 +124,23 @@ export async function loadPlugins(
|
|||||||
const results: LoadedPluginDefinition[] = []
|
const results: LoadedPluginDefinition[] = []
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (typeof entry === 'string' || isPluginReferenceObject(entry)) {
|
if (typeof entry === 'string') {
|
||||||
const descriptor = normalizeDescriptor(entry)
|
const descriptor = normalizeDescriptor(entry)
|
||||||
|
|
||||||
|
if (isPluginESMImporter(descriptor)) {
|
||||||
|
const { default: pluginFactoryOrPlugin } = await descriptor()
|
||||||
|
const plugin = await instantiatePlugin(pluginFactoryOrPlugin)
|
||||||
|
const hooks = normalizeHooks(plugin)
|
||||||
|
const name = plugin.name || `lazy-loaded-plugin-${results.length}`
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
name,
|
||||||
|
hooks,
|
||||||
|
pluginOptions: undefined,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const { resolvedPath } = resolveSpecifier(descriptor.specifier, baseDir)
|
const { resolvedPath } = resolveSpecifier(descriptor.specifier, baseDir)
|
||||||
|
|
||||||
const mod = await importModule(resolvedPath)
|
const mod = await importModule(resolvedPath)
|
||||||
|
|||||||
@@ -15,19 +15,10 @@ import type {
|
|||||||
} from '../types/manifest.js'
|
} from '../types/manifest.js'
|
||||||
import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
|
import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
|
||||||
|
|
||||||
export type BuilderPluginReference =
|
export type BuilderPluginESMImporter = () => Promise<{
|
||||||
| string
|
default: (() => BuilderPlugin | Promise<BuilderPlugin>) | BuilderPlugin
|
||||||
| {
|
}>
|
||||||
resolve: string
|
export type BuilderPluginReference = string | BuilderPluginESMImporter
|
||||||
/**
|
|
||||||
* Optional name override for the plugin. Falls back to the resolved name.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Arbitrary configuration passed to the plugin factory.
|
|
||||||
*/
|
|
||||||
options?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BuilderPluginConfigEntry = BuilderPluginReference | BuilderPlugin
|
export type BuilderPluginConfigEntry = BuilderPluginReference | BuilderPlugin
|
||||||
|
|
||||||
@@ -185,8 +176,8 @@ export type BuilderPluginFactory =
|
|||||||
| (() => BuilderPlugin | Promise<BuilderPlugin>)
|
| (() => BuilderPlugin | Promise<BuilderPlugin>)
|
||||||
| ((options: unknown) => BuilderPlugin | Promise<BuilderPlugin>)
|
| ((options: unknown) => BuilderPlugin | Promise<BuilderPlugin>)
|
||||||
|
|
||||||
export function isPluginReferenceObject(
|
export function isPluginESMImporter(
|
||||||
value: BuilderPluginConfigEntry,
|
value: BuilderPluginConfigEntry,
|
||||||
): value is Exclude<BuilderPluginReference, string> {
|
): value is BuilderPluginESMImporter {
|
||||||
return typeof value === 'object' && value !== null && 'resolve' in value
|
return typeof value === 'function' && value.length === 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user