Spaces:
Running
Running
File size: 7,699 Bytes
b1cfe1b 96bdf6c c7d34c1 6dd78ad c7d34c1 6dd78ad c7d34c1 96bdf6c c7d34c1 6dd78ad c7d34c1 6dd78ad c7d34c1 b1cfe1b c7d34c1 6dd78ad c7d34c1 6dd78ad b1cfe1b 6dd78ad | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | import { AgentApiError } from './api-error-response';
import { resolveImageOutputDir } from './server-runtime';
import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';
export type ImageDimensions = {
width: number | null;
height: number | null;
};
export type DetectedImageFormat = {
outputFormat: 'png' | 'jpeg' | 'webp';
mimeType: string;
};
type ImageFormatFallback = DetectedImageFormat['outputFormat'] | 'jpg';
export function mimeTypeForOutputFormat(outputFormat: string): string {
if (outputFormat === 'jpeg' || outputFormat === 'jpg') return 'image/jpeg';
if (outputFormat === 'webp') return 'image/webp';
return 'image/png';
}
export function detectImageFormat(buffer: Buffer, fallbackOutputFormat: ImageFormatFallback): DetectedImageFormat {
if (isPng(buffer)) return { outputFormat: 'png', mimeType: 'image/png' };
if (isJpeg(buffer)) return { outputFormat: 'jpeg', mimeType: 'image/jpeg' };
if (isWebp(buffer)) return { outputFormat: 'webp', mimeType: 'image/webp' };
const outputFormat = fallbackOutputFormat === 'jpg' ? 'jpeg' : fallbackOutputFormat;
return {
outputFormat,
mimeType: mimeTypeForOutputFormat(fallbackOutputFormat)
};
}
export async function writeFileAtomic(filepath: string, buffer: Buffer): Promise<void> {
await fs.mkdir(path.dirname(filepath), { recursive: true });
const tmpPath = `${filepath}.tmp-${crypto.randomUUID()}`;
try {
await fs.writeFile(tmpPath, buffer);
await fs.rename(tmpPath, filepath);
} catch (error) {
try {
await deleteFileIfExists(tmpPath);
} catch (cleanupError) {
console.error('清理临时产物文件失败。', cleanupError);
}
throw error;
}
}
export async function deleteFileIfExists(filepath: string): Promise<boolean> {
try {
await fs.unlink(filepath);
return true;
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
return false;
}
throw error;
}
}
export type MovedFileForDeletion = {
originalPath: string;
tempPath: string;
};
export async function moveFileIfExists(filepath: string): Promise<MovedFileForDeletion | undefined> {
const tempPath = `${filepath}.purge-${crypto.randomUUID()}`;
try {
await fs.rename(filepath, tempPath);
return { originalPath: filepath, tempPath };
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
return undefined;
}
throw error;
}
}
export async function restoreMovedFile(file: MovedFileForDeletion): Promise<void> {
await fs.rename(file.tempPath, file.originalPath);
}
export async function discardMovedFile(file: MovedFileForDeletion): Promise<void> {
await fs.rm(file.tempPath, { force: true, recursive: true });
}
export async function moveArtifactFilesForDeletion(filepaths: string[]): Promise<MovedFileForDeletion[]> {
const movedFiles: MovedFileForDeletion[] = [];
try {
for (const filepath of filepaths) {
const moved = await moveFileIfExists(filepath);
if (moved) {
movedFiles.push(moved);
}
}
return movedFiles;
} catch (error) {
await restoreArtifactFiles(movedFiles);
throw error;
}
}
export async function restoreArtifactFiles(files: MovedFileForDeletion[]): Promise<void> {
await Promise.allSettled(files.map((file) => restoreMovedFile(file)));
}
export async function discardArtifactFiles(files: MovedFileForDeletion[]): Promise<void> {
await Promise.allSettled(files.map((file) => discardMovedFile(file)));
}
export function assertArtifactFilepathAllowed(filepath: string): void {
if (isArtifactFilepathAllowed(filepath)) return;
throw new AgentApiError({
code: 'artifact_not_found',
message: '产物文件路径位于已配置图片目录之外。',
status: 404,
retryable: false
});
}
export function isArtifactFilepathAllowed(filepath: string): boolean {
const resolvedFilepath = path.resolve(filepath);
const resolvedOutputDir = resolveImageOutputDir();
return resolvedFilepath === resolvedOutputDir || resolvedFilepath.startsWith(`${resolvedOutputDir}${path.sep}`);
}
export async function deleteArtifactFileIfAllowed(filepath: string): Promise<boolean> {
assertArtifactFilepathAllowed(filepath);
return deleteFileIfExists(filepath);
}
export function readImageDimensions(buffer: Buffer): ImageDimensions {
const png = readPngDimensions(buffer);
if (png.width !== null) return png;
const jpeg = readJpegDimensions(buffer);
if (jpeg.width !== null) return jpeg;
const webp = readWebpDimensions(buffer);
return webp;
}
function readPngDimensions(buffer: Buffer): ImageDimensions {
if (buffer.length >= 24 && isPng(buffer) && buffer.toString('ascii', 12, 16) === 'IHDR') {
return {
width: buffer.readUInt32BE(16),
height: buffer.readUInt32BE(20)
};
}
return { width: null, height: null };
}
function readJpegDimensions(buffer: Buffer): ImageDimensions {
if (!isJpeg(buffer)) {
return { width: null, height: null };
}
let offset = 2;
while (offset + 9 < buffer.length) {
if (buffer[offset] !== 0xff) {
offset += 1;
continue;
}
const marker = buffer[offset + 1];
const length = buffer.readUInt16BE(offset + 2);
if (length < 2) break;
if (
(marker >= 0xc0 && marker <= 0xc3) ||
(marker >= 0xc5 && marker <= 0xc7) ||
(marker >= 0xc9 && marker <= 0xcb)
) {
return {
height: buffer.readUInt16BE(offset + 5),
width: buffer.readUInt16BE(offset + 7)
};
}
offset += 2 + length;
}
return { width: null, height: null };
}
function readWebpDimensions(buffer: Buffer): ImageDimensions {
if (!isWebp(buffer) || buffer.length < 30) {
return { width: null, height: null };
}
const chunk = buffer.toString('ascii', 12, 16);
if (chunk === 'VP8X' && buffer.length >= 30) {
return {
width: 1 + buffer.readUIntLE(24, 3),
height: 1 + buffer.readUIntLE(27, 3)
};
}
if (chunk === 'VP8 ' && buffer.length >= 30) {
return {
width: buffer.readUInt16LE(26) & 0x3fff,
height: buffer.readUInt16LE(28) & 0x3fff
};
}
if (chunk === 'VP8L' && buffer.length >= 25) {
const b0 = buffer[21];
const b1 = buffer[22];
const b2 = buffer[23];
const b3 = buffer[24];
return {
width: 1 + (((b1 & 0x3f) << 8) | b0),
height: 1 + (((b3 & 0x0f) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6))
};
}
return { width: null, height: null };
}
function isPng(buffer: Buffer): boolean {
return (
buffer.length >= 8 &&
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47 &&
buffer[4] === 0x0d &&
buffer[5] === 0x0a &&
buffer[6] === 0x1a &&
buffer[7] === 0x0a
);
}
function isJpeg(buffer: Buffer): boolean {
return buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8;
}
function isWebp(buffer: Buffer): boolean {
return (
buffer.length >= 12 && buffer.toString('ascii', 0, 4) === 'RIFF' && buffer.toString('ascii', 8, 12) === 'WEBP'
);
}
|