fix: stage vip98k media on reachable storage

This commit is contained in:
134 2026-09-07 12:17:51 +08:00 committed by root
parent 0a364f537f
commit 6a30795bb8

View File

@ -1,13 +1,9 @@
import "server-only"; import "server-only";
import { GetObjectCommand } from "@aws-sdk/client-s3"; import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { createOpenAI } from "@ai-sdk/openai"; import { createOpenAI } from "@ai-sdk/openai";
import sharp from "sharp"; import sharp from "sharp";
import { import { getDefaultStorageConfig } from "@/lib/db/queries";
getDefaultStorageConfig,
getStorageConfigById,
getSystemSetting,
} from "@/lib/db/queries";
import { import {
bindVip98kImages, bindVip98kImages,
selectVip98kTaskInvocation, selectVip98kTaskInvocation,
@ -22,11 +18,12 @@ import {
import { createLogger } from "@/lib/observability/logger"; import { createLogger } from "@/lib/observability/logger";
import { createDeferredUploadedImage } from "@/lib/storage/image-archive"; import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
import { import {
buildS3PublicUrl,
buildStorageObjectKey,
createS3Client, createS3Client,
type S3UploadConfig, type S3UploadConfig,
toDbS3UploadConfig, toDbS3UploadConfig,
uploadFile, uploadFile,
uploadFileWithS3Config,
} from "@/lib/storage/service"; } from "@/lib/storage/service";
const logger = createLogger("vip98k-provider"); const logger = createLogger("vip98k-provider");
@ -68,13 +65,11 @@ const REFERENCE_DOWNLOAD_ATTEMPTS = 3;
const REFERENCE_DOWNLOAD_TIMEOUT_MS = 120_000; const REFERENCE_DOWNLOAD_TIMEOUT_MS = 120_000;
const MAX_REFERENCE_DOWNLOAD_BYTES = 40 * 1024 * 1024; const MAX_REFERENCE_DOWNLOAD_BYTES = 40 * 1024 * 1024;
const REFERENCE_STAGING_CACHE_MS = 60_000; const REFERENCE_STAGING_CACHE_MS = 60_000;
const STORAGE_FAILOVER_CONFIG_KEY = "storage_failover_config"; const REFERENCE_STAGING_PREFIX = "vip98k-references";
type ReferenceStagingConfig = { type ReferenceStagingConfig = {
sourcePublicUrl: string; sourcePublicUrl: string;
sourceDownloadConfig?: S3UploadConfig; storageConfig: S3UploadConfig;
targetPublicUrl?: string;
uploadConfig: S3UploadConfig;
}; };
let referenceStagingConfigCache: let referenceStagingConfigCache:
@ -468,22 +463,6 @@ async function archiveOutputImage(imageUrl: string): Promise<UploadedImage> {
); );
} }
function readStorageFailoverConfig(value: unknown) {
let parsed = value;
if (typeof parsed === "string") {
try {
parsed = JSON.parse(parsed);
} catch {
parsed = null;
}
}
const record = toRecord(parsed);
return {
enabled: record?.enabled === true,
backupStorageConfigId: String(record?.backupStorageConfigId || "").trim(),
};
}
async function resolveReferenceStagingConfig() { async function resolveReferenceStagingConfig() {
const now = Date.now(); const now = Date.now();
if (referenceStagingConfigCache?.expiresAt && referenceStagingConfigCache.expiresAt > now) { if (referenceStagingConfigCache?.expiresAt && referenceStagingConfigCache.expiresAt > now) {
@ -492,29 +471,13 @@ async function resolveReferenceStagingConfig() {
let value: ReferenceStagingConfig | null = null; let value: ReferenceStagingConfig | null = null;
try { try {
const [sourceStorage, failoverSetting] = await Promise.all([ const sourceStorage = await getDefaultStorageConfig();
getDefaultStorageConfig(), const storageConfig = toDbS3UploadConfig(sourceStorage);
getSystemSetting({ key: STORAGE_FAILOVER_CONFIG_KEY }), if (sourceStorage?.publicUrl && storageConfig) {
]); value = {
const failover = readStorageFailoverConfig(failoverSetting?.value); sourcePublicUrl: sourceStorage.publicUrl,
if ( storageConfig,
sourceStorage?.publicUrl && };
failover.enabled &&
failover.backupStorageConfigId &&
failover.backupStorageConfigId !== sourceStorage.id
) {
const targetStorage = await getStorageConfigById({
id: failover.backupStorageConfigId,
});
const uploadConfig = toDbS3UploadConfig(targetStorage);
if (uploadConfig) {
value = {
sourcePublicUrl: sourceStorage.publicUrl,
sourceDownloadConfig: toDbS3UploadConfig(sourceStorage) || undefined,
targetPublicUrl: targetStorage?.publicUrl || uploadConfig.publicUrl,
uploadConfig,
};
}
} }
} catch (error) { } catch (error) {
logger.warn("reference staging config lookup failed", { logger.warn("reference staging config lookup failed", {
@ -596,7 +559,6 @@ async function downloadReferenceImageFromStorage(
mediaUrl: string, mediaUrl: string,
stagingConfig: ReferenceStagingConfig, stagingConfig: ReferenceStagingConfig,
) { ) {
if (!stagingConfig.sourceDownloadConfig) return null;
const objectKey = getStorageObjectKeyFromPublicUrl( const objectKey = getStorageObjectKeyFromPublicUrl(
mediaUrl, mediaUrl,
stagingConfig.sourcePublicUrl, stagingConfig.sourcePublicUrl,
@ -604,9 +566,9 @@ async function downloadReferenceImageFromStorage(
if (!objectKey) return null; if (!objectKey) return null;
try { try {
const response = await createS3Client(stagingConfig.sourceDownloadConfig).send( const response = await createS3Client(stagingConfig.storageConfig).send(
new GetObjectCommand({ new GetObjectCommand({
Bucket: stagingConfig.sourceDownloadConfig.bucket, Bucket: stagingConfig.storageConfig.bucket,
Key: objectKey, Key: objectKey,
}), }),
{ abortSignal: AbortSignal.timeout(REFERENCE_DOWNLOAD_TIMEOUT_MS) }, { abortSignal: AbortSignal.timeout(REFERENCE_DOWNLOAD_TIMEOUT_MS) },
@ -642,6 +604,27 @@ async function downloadReferenceImageFromStorage(
} }
} }
async function uploadReferenceImage(
buffer: Buffer,
stagingConfig: ReferenceStagingConfig,
) {
const key = buildStorageObjectKey("reference.jpg", REFERENCE_STAGING_PREFIX);
await createS3Client(stagingConfig.storageConfig).send(
new PutObjectCommand({
Bucket: stagingConfig.storageConfig.bucket,
Key: key,
Body: buffer,
ContentType: "image/jpeg",
CacheControl: "public, max-age=31536000, immutable",
...(stagingConfig.storageConfig.provider === "qiniu-kodo"
? {}
: { ACL: "public-read" as const }),
}),
{ abortSignal: AbortSignal.timeout(REFERENCE_DOWNLOAD_TIMEOUT_MS) },
);
return buildS3PublicUrl(stagingConfig.storageConfig, key);
}
async function normalizeReferenceImage(buffer: Buffer, mimeType: string) { async function normalizeReferenceImage(buffer: Buffer, mimeType: string) {
try { try {
const normalized = await sharp(buffer, { failOn: "none" }) const normalized = await sharp(buffer, { failOn: "none" })
@ -673,19 +656,14 @@ async function stageReferenceImage(
downloaded.buffer, downloaded.buffer,
downloaded.mimeType, downloaded.mimeType,
); );
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const uploadedUrl = await uploadReferenceImage(normalized.buffer, stagingConfig);
const uploaded = await uploadFileWithS3Config(
`vip98k-reference-${suffix}.${normalized.extension}`,
toArrayBuffer(normalized.buffer),
stagingConfig.uploadConfig,
);
logger.info("reference image staged", { logger.info("reference image staged", {
sourceHost: getUrlHost(mediaUrl), sourceHost: getUrlHost(mediaUrl),
targetHost: getUrlHost(uploaded.url), targetHost: getUrlHost(uploadedUrl),
sourceBytes: downloaded.buffer.byteLength, sourceBytes: downloaded.buffer.byteLength,
stagedBytes: normalized.buffer.byteLength, stagedBytes: normalized.buffer.byteLength,
}); });
return uploaded.url; return uploadedUrl;
} }
async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "video") { async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "video") {
@ -693,12 +671,15 @@ async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "vide
if (/^https?:\/\//i.test(normalized)) { if (/^https?:\/\//i.test(normalized)) {
if (mediaKind !== "image") return normalized; if (mediaKind !== "image") return normalized;
const stagingConfig = await resolveReferenceStagingConfig(); const stagingConfig = await resolveReferenceStagingConfig();
const sourceObjectKey = stagingConfig
? getStorageObjectKeyFromPublicUrl(normalized, stagingConfig.sourcePublicUrl)
: null;
if ( if (
!stagingConfig || !stagingConfig ||
sourceObjectKey?.startsWith(`${REFERENCE_STAGING_PREFIX}/`) ||
!shouldStageVip98kReferenceUrl({ !shouldStageVip98kReferenceUrl({
mediaUrl: normalized, mediaUrl: normalized,
sourcePublicUrl: stagingConfig.sourcePublicUrl, sourcePublicUrl: stagingConfig.sourcePublicUrl,
targetPublicUrl: stagingConfig.targetPublicUrl,
}) })
) { ) {
return normalized; return normalized;
@ -717,13 +698,12 @@ async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "vide
const extension = detectExtension(parsed.mimeType); const extension = detectExtension(parsed.mimeType);
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const stagingConfig = await resolveReferenceStagingConfig(); const stagingConfig = await resolveReferenceStagingConfig();
if (stagingConfig) { if (stagingConfig && mediaKind === "image") {
const uploaded = await uploadFileWithS3Config( const normalizedImage = await normalizeReferenceImage(
`vip98k-ref-${suffix}.${extension}`, parsed.buffer,
toArrayBuffer(parsed.buffer), parsed.mimeType,
stagingConfig.uploadConfig,
); );
return uploaded.url; return uploadReferenceImage(normalizedImage.buffer, stagingConfig);
} }
const uploaded = await uploadFile( const uploaded = await uploadFile(
`vip98k-ref-${suffix}.${extension}`, `vip98k-ref-${suffix}.${extension}`,