forked from boos/BoosAPI
feat: implement proxy input file resolution and storage
- Added `inputFiles.ts` for handling input file resolution, including validation and normalization of file types. - Introduced `proxyFileStore.ts` for managing file storage, retrieval, and deletion with owner-based isolation. - Created tests for proxy file storage functionality in `proxyFileStore.test.ts`. - Developed `proxyInputFileResolver.ts` to resolve input files in OpenAI and response formats. - Added utility functions for handling reasoning signatures in `reasoningTransport.ts` and corresponding tests. - Enhanced shared input file transformers in `inputFile.ts` for better file handling and type inference.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { and, desc, eq, isNull, or } from 'drizzle-orm';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { db, schema } from '../db/index.js';
|
||||
import type { ProxyResourceOwner } from '../middleware/auth.js';
|
||||
import { formatUtcSqlDateTime } from './localTimeService.js';
|
||||
|
||||
export const LOCAL_PROXY_FILE_ID_PREFIX = 'file-metapi-';
|
||||
|
||||
export type ProxyFileRecord = {
|
||||
publicId: string;
|
||||
ownerType: ProxyResourceOwner['ownerType'];
|
||||
ownerId: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
purpose: string | null;
|
||||
byteSize: number;
|
||||
sha256: string;
|
||||
contentBase64: string;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
|
||||
export type SaveProxyFileInput = ProxyResourceOwner & {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
purpose?: string | null;
|
||||
contentBase64: string;
|
||||
byteSize?: number | null;
|
||||
sha256?: string | null;
|
||||
};
|
||||
|
||||
export type CreateProxyFileInput = {
|
||||
owner: ProxyResourceOwner;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
purpose?: string | null;
|
||||
buffer: Buffer;
|
||||
};
|
||||
|
||||
function buildPublicFileId(): string {
|
||||
const timePart = Date.now().toString(36);
|
||||
const randomPart = Math.random().toString(36).slice(2, 10);
|
||||
return `${LOCAL_PROXY_FILE_ID_PREFIX}${timePart}-${randomPart}`;
|
||||
}
|
||||
|
||||
function normalizeFilename(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || 'upload.bin';
|
||||
}
|
||||
|
||||
function toByteSize(contentBase64: string): number {
|
||||
return Buffer.from(contentBase64, 'base64').byteLength;
|
||||
}
|
||||
|
||||
function rowToRecord(row: typeof schema.proxyFiles.$inferSelect): ProxyFileRecord {
|
||||
return {
|
||||
publicId: row.publicId,
|
||||
ownerType: row.ownerType as ProxyResourceOwner['ownerType'],
|
||||
ownerId: row.ownerId,
|
||||
filename: row.filename,
|
||||
mimeType: row.mimeType,
|
||||
purpose: row.purpose,
|
||||
byteSize: row.byteSize,
|
||||
sha256: row.sha256,
|
||||
contentBase64: row.contentBase64,
|
||||
createdAt: row.createdAt ?? null,
|
||||
updatedAt: row.updatedAt ?? null,
|
||||
deletedAt: row.deletedAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function ownerWhere(owner: ProxyResourceOwner) {
|
||||
return and(
|
||||
eq(schema.proxyFiles.ownerType, owner.ownerType),
|
||||
eq(schema.proxyFiles.ownerId, owner.ownerId),
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveProxyFile(input: SaveProxyFileInput): Promise<ProxyFileRecord> {
|
||||
const publicId = buildPublicFileId();
|
||||
const now = formatUtcSqlDateTime(new Date());
|
||||
const filename = normalizeFilename(input.filename);
|
||||
const byteSize = typeof input.byteSize === 'number' && Number.isFinite(input.byteSize)
|
||||
? Math.max(0, Math.trunc(input.byteSize))
|
||||
: toByteSize(input.contentBase64);
|
||||
const sha256 = typeof input.sha256 === 'string' && input.sha256.trim().length > 0
|
||||
? input.sha256.trim()
|
||||
: createHash('sha256').update(Buffer.from(input.contentBase64, 'base64')).digest('hex');
|
||||
|
||||
await db.insert(schema.proxyFiles).values({
|
||||
publicId,
|
||||
ownerType: input.ownerType,
|
||||
ownerId: input.ownerId,
|
||||
filename,
|
||||
mimeType: input.mimeType,
|
||||
purpose: input.purpose?.trim() || null,
|
||||
byteSize,
|
||||
sha256,
|
||||
contentBase64: input.contentBase64,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
}).run();
|
||||
|
||||
return (await getProxyFileByPublicIdForOwner(publicId, input))!;
|
||||
}
|
||||
|
||||
export async function createProxyFile(input: CreateProxyFileInput): Promise<ProxyFileRecord> {
|
||||
return saveProxyFile({
|
||||
ownerType: input.owner.ownerType,
|
||||
ownerId: input.owner.ownerId,
|
||||
filename: input.filename,
|
||||
mimeType: input.mimeType,
|
||||
purpose: input.purpose,
|
||||
byteSize: input.buffer.byteLength,
|
||||
sha256: createHash('sha256').update(input.buffer).digest('hex'),
|
||||
contentBase64: input.buffer.toString('base64'),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listProxyFilesByOwner(owner: ProxyResourceOwner): Promise<ProxyFileRecord[]> {
|
||||
const rows = await db.select().from(schema.proxyFiles)
|
||||
.where(and(ownerWhere(owner), isNull(schema.proxyFiles.deletedAt)))
|
||||
.orderBy(desc(schema.proxyFiles.createdAt))
|
||||
.all();
|
||||
return rows.map(rowToRecord);
|
||||
}
|
||||
|
||||
export async function getProxyFileByPublicId(publicId: string): Promise<ProxyFileRecord | null> {
|
||||
const row = await db.select().from(schema.proxyFiles)
|
||||
.where(and(
|
||||
eq(schema.proxyFiles.publicId, publicId),
|
||||
isNull(schema.proxyFiles.deletedAt),
|
||||
))
|
||||
.get();
|
||||
return row ? rowToRecord(row) : null;
|
||||
}
|
||||
|
||||
export async function getProxyFileByPublicIdForOwner(
|
||||
publicId: string,
|
||||
owner: ProxyResourceOwner,
|
||||
): Promise<ProxyFileRecord | null> {
|
||||
const row = await db.select().from(schema.proxyFiles)
|
||||
.where(and(
|
||||
eq(schema.proxyFiles.publicId, publicId),
|
||||
ownerWhere(owner),
|
||||
isNull(schema.proxyFiles.deletedAt),
|
||||
))
|
||||
.get();
|
||||
return row ? rowToRecord(row) : null;
|
||||
}
|
||||
|
||||
export async function getProxyFileContentByPublicIdForOwner(
|
||||
publicId: string,
|
||||
owner: ProxyResourceOwner,
|
||||
): Promise<{ filename: string; mimeType: string; buffer: Buffer } | null> {
|
||||
const record = await getProxyFileByPublicIdForOwner(publicId, owner);
|
||||
if (!record) return null;
|
||||
return {
|
||||
filename: record.filename,
|
||||
mimeType: record.mimeType,
|
||||
buffer: Buffer.from(record.contentBase64, 'base64'),
|
||||
};
|
||||
}
|
||||
|
||||
export async function softDeleteProxyFileByPublicIdForOwner(
|
||||
publicId: string,
|
||||
owner: ProxyResourceOwner,
|
||||
): Promise<boolean> {
|
||||
const now = formatUtcSqlDateTime(new Date());
|
||||
const result = await db.update(schema.proxyFiles)
|
||||
.set({
|
||||
deletedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(and(
|
||||
eq(schema.proxyFiles.publicId, publicId),
|
||||
ownerWhere(owner),
|
||||
or(isNull(schema.proxyFiles.deletedAt), eq(schema.proxyFiles.deletedAt, '')),
|
||||
))
|
||||
.run();
|
||||
return Number(result?.changes || 0) > 0;
|
||||
}
|
||||
|
||||
export async function softDeleteProxyFileByPublicId(
|
||||
publicId: string,
|
||||
owner: ProxyResourceOwner,
|
||||
): Promise<boolean> {
|
||||
return softDeleteProxyFileByPublicIdForOwner(publicId, owner);
|
||||
}
|
||||
|
||||
export async function softDeleteProxyFile(publicId: string): Promise<boolean> {
|
||||
const now = formatUtcSqlDateTime(new Date());
|
||||
const result = await db.update(schema.proxyFiles)
|
||||
.set({
|
||||
deletedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(and(
|
||||
eq(schema.proxyFiles.publicId, publicId),
|
||||
or(isNull(schema.proxyFiles.deletedAt), eq(schema.proxyFiles.deletedAt, '')),
|
||||
))
|
||||
.run();
|
||||
return Number(result?.changes || 0) > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user