Compare commits
No commits in common. "main" and "codex/watermark-qiniu-direct-20261111" have entirely different histories.
main
...
codex/wate
@ -976,8 +976,7 @@ export async function POST(request: NextRequest) {
|
||||
const annotatedFilename = `canvas-local-edit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.png`;
|
||||
const annotatedUpload = await uploadFile(
|
||||
annotatedFilename,
|
||||
toArrayBuffer(annotatedBuffer),
|
||||
{ preferQiniuForm: true }
|
||||
toArrayBuffer(annotatedBuffer)
|
||||
);
|
||||
const prompt = buildLocalEditPrompt(regions);
|
||||
const manualRegionReferenceImageUrls = regions.flatMap((region) =>
|
||||
|
||||
@ -35,9 +35,7 @@ async function resolveStoredQiniuImageUrl(source: string): Promise<string | null
|
||||
!url.pathname.startsWith(prefix)) return null;
|
||||
|
||||
const objectKey = url.pathname.slice(prefix.length);
|
||||
return /^uploads\/(?:direct\/[0-9a-f-]{36}\/)?[\w.-]+$/i.test(objectKey)
|
||||
? url.toString()
|
||||
: null;
|
||||
return /^uploads\/[\w.-]+$/.test(objectKey) ? url.toString() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Check, ImageIcon, Loader2, UserRound } from "lucide-react";
|
||||
import { AgentPreviewImage } from "@/components/agents/AgentPreviewImage";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import { normalizeAgentWorkflowBrandModelConfig } from "@/lib/agents/workflow-brand-model-config";
|
||||
@ -230,11 +229,12 @@ export function AgentWorkflowBrandModelPicker({
|
||||
isDark ? "bg-white/[0.04]" : "bg-muted/40"
|
||||
)}
|
||||
>
|
||||
<AgentPreviewImage
|
||||
<img
|
||||
src={image.imageUrl}
|
||||
alt={image.displayName || `${normalizedBrand} 模特图`}
|
||||
className="h-full w-full object-cover"
|
||||
previewWidth={360}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
|
||||
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
|
||||
|
||||
@ -2482,11 +2482,14 @@ export function ImageNode({
|
||||
[compareButtonVisualSize]
|
||||
);
|
||||
const shouldUseHighFidelityDisplayImage =
|
||||
isSelected ||
|
||||
isHovered ||
|
||||
isCompareMode ||
|
||||
boxCutoutMode ||
|
||||
localEditMode ||
|
||||
cropPanelOpen ||
|
||||
imageAdjustPanelOpen ||
|
||||
fullscreenPreviewOpen ||
|
||||
isThisNodePicking;
|
||||
const displayImageWidth = useMemo(() => {
|
||||
const devicePixelRatio =
|
||||
|
||||
@ -99,7 +99,6 @@ const DEFAULT_POLL_INTERVAL_SECONDS = 8;
|
||||
const DEFAULT_MAX_POLL_SECONDS = 600;
|
||||
const REFERENCE_DOWNLOAD_ATTEMPTS = 3;
|
||||
const REFERENCE_DOWNLOAD_TIMEOUT_MS = 120_000;
|
||||
const REFERENCE_CDN_TIMEOUT_MS = 10_000;
|
||||
const REFERENCE_CACHE_LOOKUP_TIMEOUT_MS = 15_000;
|
||||
const MAX_REFERENCE_DOWNLOAD_BYTES = IMAGE_UPLOAD_MAX_BYTES;
|
||||
const REFERENCE_STAGING_CACHE_MS = 60_000;
|
||||
@ -683,44 +682,43 @@ async function findStagedReference(
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchReferenceImage(mediaUrl: string, preferCdn = false) {
|
||||
const response = await fetch(mediaUrl, {
|
||||
cache: preferCdn ? "default" : "no-store",
|
||||
redirect: preferCdn ? "error" : "follow",
|
||||
headers: { "User-Agent": "yedir-ai-vip98k-staging/1.0" },
|
||||
signal: AbortSignal.timeout(preferCdn ? REFERENCE_CDN_TIMEOUT_MS : REFERENCE_DOWNLOAD_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok || (preferCdn && response.status !== 200)) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(`source returned HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const mimeType = response.headers.get("content-type") || "image/png";
|
||||
if (preferCdn && !/^image\//i.test(mimeType) && !/^application\/octet-stream\b/i.test(mimeType)) {
|
||||
await response.body?.cancel();
|
||||
throw new Error("CDN response was not an image");
|
||||
}
|
||||
const declaredLength = Number(response.headers.get("content-length"));
|
||||
if (declaredLength > MAX_REFERENCE_DOWNLOAD_BYTES) {
|
||||
await response.body?.cancel();
|
||||
throw new Vip98kReferenceTooLargeError();
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
if (buffer.byteLength > MAX_REFERENCE_DOWNLOAD_BYTES) {
|
||||
throw new Vip98kReferenceTooLargeError();
|
||||
}
|
||||
if ((declaredLength > 0 && buffer.byteLength !== declaredLength) || (preferCdn && !buffer.byteLength)) {
|
||||
throw new Error("source image download was incomplete");
|
||||
}
|
||||
return { buffer, mimeType };
|
||||
}
|
||||
|
||||
async function downloadReferenceImage(mediaUrl: string) {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 1; attempt <= REFERENCE_DOWNLOAD_ATTEMPTS; attempt += 1) {
|
||||
try {
|
||||
return await fetchReferenceImage(mediaUrl);
|
||||
const response = await fetch(mediaUrl, {
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
headers: { "User-Agent": "yedir-ai-vip98k-staging/1.0" },
|
||||
signal: AbortSignal.timeout(REFERENCE_DOWNLOAD_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`source returned HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const declaredLength = Number(response.headers.get("content-length"));
|
||||
if (
|
||||
Number.isFinite(declaredLength) &&
|
||||
declaredLength > MAX_REFERENCE_DOWNLOAD_BYTES
|
||||
) {
|
||||
throw new Vip98kReferenceTooLargeError();
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
if (buffer.byteLength > MAX_REFERENCE_DOWNLOAD_BYTES) {
|
||||
throw new Vip98kReferenceTooLargeError();
|
||||
}
|
||||
if (
|
||||
Number.isFinite(declaredLength) &&
|
||||
declaredLength > 0 &&
|
||||
buffer.byteLength !== declaredLength
|
||||
) {
|
||||
throw new Error("source image download was incomplete");
|
||||
}
|
||||
return {
|
||||
buffer,
|
||||
mimeType: response.headers.get("content-type") || "image/png",
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (error instanceof Vip98kReferenceTooLargeError) throw error;
|
||||
@ -740,22 +738,6 @@ async function downloadReferenceImage(mediaUrl: string) {
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadReferenceImageFromCdn(mediaUrl: string, stagingConfig: ReferenceStagingConfig) {
|
||||
if (stagingConfig.storageConfig.provider !== "qiniu-kodo") return null;
|
||||
const cdnUrl = process.env.NEXT_PUBLIC_CANVAS_CDN_ORIGIN || "";
|
||||
if (!URL.canParse(cdnUrl) || !getStorageObjectKeyFromPublicUrl(mediaUrl, stagingConfig.sourcePublicUrl)) return null;
|
||||
const source = new URL(mediaUrl);
|
||||
if (source.origin !== new URL(cdnUrl).origin || source.search || source.username || source.password) return null;
|
||||
|
||||
try {
|
||||
return await fetchReferenceImage(mediaUrl, true);
|
||||
} catch (error) {
|
||||
if (error instanceof Vip98kReferenceTooLargeError) throw error;
|
||||
logger.warn("reference CDN download failed; trying source storage", { sourceHost: source.hostname });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadReferenceImageFromStorage(
|
||||
mediaUrl: string,
|
||||
stagingConfig: ReferenceStagingConfig,
|
||||
@ -870,10 +852,9 @@ async function stageReferenceImage(
|
||||
}
|
||||
|
||||
const downloadStartedAt = Date.now();
|
||||
const cdn = await downloadReferenceImageFromCdn(mediaUrl, stagingConfig);
|
||||
const storage = cdn ? null : await downloadReferenceImageFromStorage(mediaUrl, stagingConfig);
|
||||
const downloaded =
|
||||
cdn || storage || (await downloadReferenceImage(mediaUrl));
|
||||
(await downloadReferenceImageFromStorage(mediaUrl, stagingConfig)) ||
|
||||
(await downloadReferenceImage(mediaUrl));
|
||||
const downloadMs = Date.now() - downloadStartedAt;
|
||||
const normalizeStartedAt = Date.now();
|
||||
const normalized = await normalizeReferenceImage(
|
||||
@ -891,7 +872,6 @@ async function stageReferenceImage(
|
||||
cacheStagedReference(cacheKey, uploadedUrl, runtimeConfig);
|
||||
logger.info("reference image staged", {
|
||||
sourceHost: getUrlHost(mediaUrl),
|
||||
sourceDelivery: cdn ? "cdn" : storage ? "storage" : "public-url",
|
||||
targetHost: getUrlHost(uploadedUrl),
|
||||
sourceBytes: downloaded.buffer.byteLength,
|
||||
stagedBytes: normalized.buffer.byteLength,
|
||||
|
||||
@ -3444,13 +3444,11 @@ async function applyLocalEditCompositeToImages(params: {
|
||||
const [originalUpload, thumbnailUpload] = await Promise.all([
|
||||
uploadFile(
|
||||
`canvas-local-edit-composited-${suffix}.png`,
|
||||
bufferToArrayBuffer(mergedBuffer),
|
||||
{ preferQiniuForm: true }
|
||||
bufferToArrayBuffer(mergedBuffer)
|
||||
),
|
||||
uploadFile(
|
||||
`canvas-local-edit-composited-thumb-${suffix}.png`,
|
||||
bufferToArrayBuffer(thumbnailBuffer),
|
||||
{ preferQiniuForm: true }
|
||||
bufferToArrayBuffer(thumbnailBuffer)
|
||||
),
|
||||
]);
|
||||
mergedImages.push({
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
import { normalizeLegacyAliyunStorageUrl } from "./storage/retired-storage";
|
||||
|
||||
const IMAGE_PROXY_PATH = "/api/image-proxy";
|
||||
const CDN_PREVIEW_WIDTHS = [64, 160, 192, 240, 320, 360, 480, 520, 640, 720, 840, 960, 1280, 1600, 2048, 2560, 2600, 3200, 4096];
|
||||
|
||||
function getConfiguredCdnUrl(): URL | undefined {
|
||||
try {
|
||||
return new URL(process.env.NEXT_PUBLIC_CANVAS_CDN_ORIGIN || "");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Error fallbacks must keep unavailable legacy sources behind archive resolution. */
|
||||
export function buildImageFallbackUrl(value: string | null | undefined): string {
|
||||
@ -111,7 +102,10 @@ export function isAlibabaModelImageUrl(value: string): boolean {
|
||||
|
||||
function isTrustedDirectStorageImageHost(hostname: string): boolean {
|
||||
const normalized = hostname.trim().toLowerCase();
|
||||
let configuredCdnHost = "";
|
||||
try { configuredCdnHost = new URL(process.env.NEXT_PUBLIC_CANVAS_CDN_ORIGIN || "").hostname; } catch { /* optional */ }
|
||||
return (
|
||||
(configuredCdnHost !== "" && normalized === configuredCdnHost) ||
|
||||
normalized === "yybb.top" ||
|
||||
normalized.endsWith(".qiniucs.com") ||
|
||||
normalized.endsWith(".qiniudn.com") ||
|
||||
@ -133,29 +127,20 @@ function resolveTrustedDirectStorageDisplayUrl(
|
||||
): string | null {
|
||||
try {
|
||||
const target = new URL(url);
|
||||
const configuredCdn = getConfiguredCdnUrl();
|
||||
const isConfiguredCdn = target.origin === configuredCdn?.origin;
|
||||
if (
|
||||
target.hostname !== configuredCdn?.hostname &&
|
||||
!isTrustedDirectStorageImageHost(target.hostname) &&
|
||||
!isFirstPartyQiniuAsset(target)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestedWidth = normalizeWidth(options.width);
|
||||
let width = requestedWidth;
|
||||
let quality = normalizeQuality(options.quality);
|
||||
const width = normalizeWidth(options.width);
|
||||
const quality = normalizeQuality(options.quality);
|
||||
const format = options.format;
|
||||
if ((!width && !quality && !format) || target.search) {
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
if (isConfiguredCdn && format === "webp" && requestedWidth) {
|
||||
width = CDN_PREVIEW_WIDTHS.find((size) => size >= requestedWidth) ?? requestedWidth;
|
||||
if (quality && quality >= 84 && quality <= 92) quality = 88;
|
||||
}
|
||||
|
||||
const operations = ["imageView2", "2"];
|
||||
if (width) operations.push("w", String(width));
|
||||
if (format) operations.push("format", format === "jpeg" ? "jpg" : format);
|
||||
|
||||
@ -3,7 +3,6 @@ import { isRetiredStorageUrl, isLegacyAliyunStorageConfig, RETIRED_STORAGE_MESSA
|
||||
|
||||
import { DeleteObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { put } from "@vercel/blob";
|
||||
import { createHmac } from "node:crypto";
|
||||
import {
|
||||
getDefaultStorageConfig,
|
||||
getStorageConfigById,
|
||||
@ -240,48 +239,12 @@ export async function maybeEnqueueStorageMigrationTask(params: {
|
||||
await maybeEnqueueAliyunMigrationTask(params);
|
||||
}
|
||||
|
||||
async function uploadQiniuForm(
|
||||
config: S3UploadConfig,
|
||||
key: string,
|
||||
buffer: ArrayBuffer,
|
||||
contentType: string
|
||||
) {
|
||||
const encode = (value: Buffer) =>
|
||||
value.toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
const policy = encode(Buffer.from(JSON.stringify({
|
||||
scope: `${config.bucket}:${key}`,
|
||||
deadline: Math.floor(Date.now() / 1000) + 120,
|
||||
insertOnly: 1,
|
||||
})));
|
||||
const signature = encode(createHmac("sha1", config.secretAccessKey).update(policy).digest());
|
||||
const form = new FormData();
|
||||
form.append("token", `${config.accessKeyId}:${signature}:${policy}`);
|
||||
form.append("key", key);
|
||||
form.append("file", new Blob([buffer], { type: contentType }), key.split("/").at(-1));
|
||||
|
||||
const startedAt = Date.now();
|
||||
const response = await fetch("https://upload.qiniup.com/", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
redirect: "error",
|
||||
signal: AbortSignal.timeout(45_000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel();
|
||||
throw new Error(`Qiniu form upload returned HTTP ${response.status}`);
|
||||
}
|
||||
const result = await response.json() as { key?: string };
|
||||
if (result.key !== key) throw new Error("Qiniu form upload returned an unexpected key");
|
||||
logger.info("qiniu_form_upload_completed", { bytes: buffer.byteLength, elapsedMs: Date.now() - startedAt });
|
||||
}
|
||||
|
||||
// S3/MinIO 上传函数(使用 AWS SDK)
|
||||
async function uploadToS3(
|
||||
filename: string,
|
||||
buffer: ArrayBuffer,
|
||||
config: S3UploadConfig,
|
||||
s3Client = createS3Client(config),
|
||||
preferQiniuForm = false
|
||||
s3Client = createS3Client(config)
|
||||
) {
|
||||
const {
|
||||
endpoint,
|
||||
@ -297,14 +260,6 @@ async function uploadToS3(
|
||||
const contentType = getContentType(filename);
|
||||
|
||||
try {
|
||||
if (preferQiniuForm && config.provider === "qiniu-kodo" && config.region === "cn-east-1") {
|
||||
try {
|
||||
await uploadQiniuForm(config, key, buffer, contentType);
|
||||
return { url: buildS3PublicUrl(config, key), pathname: key, contentType };
|
||||
} catch (error) {
|
||||
logger.warn("[Upload] Qiniu form upload failed, retrying the same object through S3", error);
|
||||
}
|
||||
}
|
||||
logger.debug(`[S3 Upload] Uploading to: ${normalizedEndpoint}/${bucket}/${key}`);
|
||||
|
||||
// 上传文件
|
||||
@ -596,8 +551,7 @@ async function uploadWithAnyEnabledStorageConfig(
|
||||
// 上传文件到存储服务
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
buffer: ArrayBuffer,
|
||||
options?: { preferQiniuForm?: boolean }
|
||||
buffer: ArrayBuffer
|
||||
): Promise<UploadedFileResult> {
|
||||
logger.debug(`[Upload] Starting upload for file: ${filename}`);
|
||||
|
||||
@ -618,7 +572,7 @@ export async function uploadFile(
|
||||
logger.debug(`[Upload] Using database S3 config: ${selectedStorageConfig?.name}`);
|
||||
logger.debug(`[Upload] Endpoint: ${dbS3Config.endpoint}`);
|
||||
logger.debug(`[Upload] Bucket: ${dbS3Config.bucket}`);
|
||||
const uploaded = await uploadToS3(filename, buffer, dbS3Config, undefined, options?.preferQiniuForm);
|
||||
const uploaded = await uploadToS3(filename, buffer, dbS3Config);
|
||||
lastKnownGoodDbS3Config = dbS3Config;
|
||||
await maybeEnqueueAliyunMigrationTask({
|
||||
uploaded,
|
||||
|
||||
@ -7,86 +7,15 @@ import { serverLoader } from "../helpers/canvas-download-server-loader";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
test.describe("Qiniu image delivery", () => {
|
||||
test("coalesces nearby configured CDN preview sizes and quality while preserving original URLs", () => {
|
||||
const source = "https://cdn.example/uploads/sample.png";
|
||||
const { buildImageProxyUrl: build } = serverLoader({}, {
|
||||
NEXT_PUBLIC_CANVAS_CDN_ORIGIN: "https://cdn.example",
|
||||
})("lib/image-delivery.ts");
|
||||
expect(build(source, { width: 329, quality: 86, format: "webp" })).toBe(
|
||||
`${source}?imageView2/2/w/360/format/webp/q/88`,
|
||||
);
|
||||
expect(build(source, { width: 331, quality: 92, format: "webp" })).toBe(
|
||||
build(source, { width: 360, quality: 88, format: "webp" }),
|
||||
);
|
||||
expect(build(source, { width: 640, quality: 76, format: "webp" })).toContain("w/640/format/webp/q/76");
|
||||
expect(build(source, { width: 960, quality: 92, format: "webp" })).toContain("w/960/format/webp/q/88");
|
||||
expect(build(`${source}?token=signed`, { width: 331, format: "webp" })).toBe(`${source}?token=signed`);
|
||||
expect(build(source)).toBe(source);
|
||||
expect(new URL(build(source, { delivery: "original" }), "https://app.example").searchParams.get("url")).toBe(source);
|
||||
expect(build("https://yybb.top/uploads/sample.png", { width: 331, quality: 92, format: "webp" })).toContain("w/331/format/webp/q/92");
|
||||
});
|
||||
|
||||
for (const mode of ["cdn", "storage", "signed"] as const) {
|
||||
test(`98K reference staging reads complete image bytes via ${mode}`, async () => {
|
||||
const original = await sharp({ create: { width: 64, height: 64, channels: 3, background: "#a45623" } }).png().toBuffer();
|
||||
const cdn = "https://cdn.example";
|
||||
const source = `${cdn}/media/uploads/reference.png${mode === "signed" ? "?token=signed" : ""}`;
|
||||
const calls: string[] = [];
|
||||
const command = (operation: string) => class {
|
||||
operation = operation;
|
||||
constructor(public input: { Key: string; Body?: Buffer }) {}
|
||||
};
|
||||
const config = { provider: "qiniu-kodo", bucket: "images", publicUrl: `${cdn}/media` };
|
||||
const load = serverLoader({
|
||||
"./vip98k-chat": {},
|
||||
"@/lib/db/queries": { getDefaultStorageConfig: async () => config, getSystemSetting: async () => null },
|
||||
"@/lib/storage/image-archive": {},
|
||||
"@aws-sdk/client-s3": {
|
||||
GetObjectCommand: command("get"), HeadObjectCommand: command("head"), PutObjectCommand: command("put"),
|
||||
},
|
||||
"@/lib/storage/service": {
|
||||
toDbS3UploadConfig: () => config,
|
||||
buildS3PublicUrl: (_: unknown, key: string) => `${config.publicUrl}/${key}`,
|
||||
createS3Client: () => ({ send: async ({ operation, input }: { operation: string; input: { Key: string; Body: Buffer } }) => {
|
||||
calls.push(operation);
|
||||
if (operation === "head") return {};
|
||||
if (operation === "get") {
|
||||
expect(input.Key).toBe("uploads/reference.png");
|
||||
return { Body: { transformToByteArray: async () => original }, ContentLength: original.length };
|
||||
}
|
||||
expect((await sharp(input.Body).metadata()).format).toBe("jpeg");
|
||||
return {};
|
||||
} }),
|
||||
},
|
||||
"@/lib/observability/logger": { createLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }) },
|
||||
fetch: async (url: string, init: RequestInit) => {
|
||||
if (url === source) {
|
||||
calls.push("cdn");
|
||||
expect(init.redirect).toBe("error");
|
||||
return mode === "cdn"
|
||||
? new Response(original, { headers: { "content-type": "image/png", "content-length": String(original.length) } })
|
||||
: new Response(original.subarray(0, 4), { headers: { "content-type": "image/png", "content-length": String(original.length) } });
|
||||
}
|
||||
calls.push("submit");
|
||||
expect(String(init.body)).toContain("vip98k-cache-v2");
|
||||
return Response.json({ taskId: "test-task", task_id: "test-task" });
|
||||
},
|
||||
}, { NEXT_PUBLIC_CANVAS_CDN_ORIGIN: cdn });
|
||||
const { submitVip98kExternalTask } = load("lib/ai/providers/vip98k.ts");
|
||||
await submitVip98kExternalTask({ apiKey: "test", modelId: "google/nano-banana-2", kind: "image", imageUrls: [source], durable: true });
|
||||
expect(calls).toEqual(["head", ...(mode !== "signed" ? ["cdn"] : []), ...(mode !== "cdn" ? ["get"] : []), "put", "submit"]);
|
||||
});
|
||||
}
|
||||
|
||||
for (const [source, reusesSource] of [
|
||||
["https://cdn.example/media/uploads/source.png", true],
|
||||
["https://cdn.example/media/uploads/direct/1a868070-0d5b-4c99-937d-f10d6dc59612/source.png", true],
|
||||
["https://cdn.example/media/uploads/source.png?token=temporary", false],
|
||||
["https://cdn.example/media-other/uploads/source.png", false],
|
||||
] as const) {
|
||||
for (const source of [
|
||||
"https://cdn.example/media/uploads/source.png",
|
||||
"https://cdn.example/media/uploads/source.png?token=temporary",
|
||||
"https://cdn.example/media-other/uploads/source.png",
|
||||
]) {
|
||||
test(`watermark removal handles ${source} according to its storage origin and signature`, async () => {
|
||||
const storedResult = "https://cdn.example/media/uploads/result.png";
|
||||
const uploadedInput = "https://cdn.example/media/uploads/staged.png";
|
||||
const reusesSource = source === "https://cdn.example/media/uploads/source.png";
|
||||
const transferred: string[] = [];
|
||||
const uploaded: string[] = [];
|
||||
const saved: string[] = [];
|
||||
@ -137,47 +66,6 @@ test.describe("Qiniu image delivery", () => {
|
||||
});
|
||||
}
|
||||
|
||||
for (const formWorks of [true, false]) {
|
||||
test(`local edit storage ${formWorks ? "uses Qiniu form upload" : "reuses the same key on S3 fallback"}`, async () => {
|
||||
const bytes = new Uint8Array([1, 2, 3]).buffer;
|
||||
const config = {
|
||||
id: "fixture", type: "qiniu-kodo", isEnabled: true, region: "cn-east-1",
|
||||
endpoint: "https://s3.example", publicUrl: "https://cdn.example",
|
||||
bucket: "images", accessKeyId: "fixture-ak", secretAccessKey: "fixture-sk",
|
||||
};
|
||||
let formKey = "";
|
||||
const s3Keys: string[] = [];
|
||||
const storage = serverLoader({
|
||||
"@vercel/blob": {},
|
||||
"@aws-sdk/client-s3": {
|
||||
PutObjectCommand: class { constructor(public input: { Key: string }) {} },
|
||||
S3Client: class {
|
||||
async send(command: { input: { Key: string } }) { s3Keys.push(command.input.Key); }
|
||||
},
|
||||
},
|
||||
"@/lib/db/queries": {
|
||||
getDefaultStorageConfig: async () => config,
|
||||
getSystemSetting: async () => ({ value: { enabled: false } }),
|
||||
},
|
||||
"@/lib/storage/migration-db": {},
|
||||
"@/lib/observability/logger": { createLogger: () => ({ debug() {}, warn() {}, info() {}, error() {} }) },
|
||||
fetch: async (url: string, init: RequestInit) => {
|
||||
expect(url).toBe("https://upload.qiniup.com/");
|
||||
const form = init.body as FormData;
|
||||
formKey = String(form.get("key"));
|
||||
const token = String(form.get("token"));
|
||||
const policy = JSON.parse(Buffer.from(token.split(":")[2], "base64url").toString());
|
||||
expect(policy).toMatchObject({ scope: `images:${formKey}`, insertOnly: 1 });
|
||||
expect((form.get("file") as File).size).toBe(3);
|
||||
return formWorks ? Response.json({ key: formKey }) : new Response(null, { status: 503 });
|
||||
},
|
||||
})("lib/storage/service.ts");
|
||||
const saved = await storage.uploadFile("local-edit.png", bytes, { preferQiniuForm: true });
|
||||
expect(saved).toMatchObject({ pathname: formKey, url: "https://cdn.example/" + formKey });
|
||||
expect(s3Keys).toEqual(formWorks ? [] : [formKey]);
|
||||
});
|
||||
}
|
||||
|
||||
test("model result buckets use our thumbnail endpoint without forbidden OSS transforms", () => {
|
||||
const source = "https://dashscope-a717.oss-accelerate.aliyuncs.com/output.png";
|
||||
const preview = new URL(buildImageProxyUrl(source, { width: 960, format: "webp" }), "https://app.example");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user