/ src / server / uploads / files.ts
files.ts
 1  import { randomUUID } from 'node:crypto'
 2  import { readFile, writeFile } from 'node:fs/promises'
 3  import path from 'node:path'
 4  
 5  import type { UploadAsset, UploadKind } from '@/lib/shared/chat'
 6  import { getStoragePaths } from '@/server/storage/db'
 7  import { saveUploadRecord } from '@/server/storage/chat-store'
 8  
 9  export async function saveUploadFile(input: {
10    sessionId: string
11    file: File
12    kind: UploadKind
13  }): Promise<UploadAsset> {
14    const safeName = sanitizeFileName(input.file.name || `${input.kind}-${randomUUID()}`)
15    const ext = path.extname(safeName)
16    const base = path.basename(safeName, ext)
17    const diskFileName = `${Date.now()}-${randomUUID()}-${base}${ext}`
18    const { uploadsDir } = getStoragePaths()
19    const diskPath = path.join(uploadsDir, diskFileName)
20  
21    const bytes = new Uint8Array(await input.file.arrayBuffer())
22    await writeFile(diskPath, bytes)
23  
24    return saveUploadRecord({
25      sessionId: input.sessionId,
26      kind: input.kind,
27      fileName: safeName,
28      mimeType: input.file.type || guessMimeType(input.kind),
29      size: input.file.size,
30      diskPath,
31    })
32  }
33  
34  export async function readUploadFile(diskPath: string): Promise<Uint8Array> {
35    return readFile(diskPath)
36  }
37  
38  function sanitizeFileName(value: string): string {
39    const name = value.replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^_+/, '')
40    return name.slice(0, 160) || 'upload'
41  }
42  
43  function guessMimeType(kind: UploadKind): string {
44    return kind === 'audio' ? 'audio/webm' : 'application/octet-stream'
45  }