(
client: TrpcClient,
filePath: string,
options: UploadLocalFileOptions = {},
)
| 55 | * @returns the created file record |
| 56 | */ |
| 57 | export const uploadLocalFile = async ( |
| 58 | client: TrpcClient, |
| 59 | filePath: string, |
| 60 | options: UploadLocalFileOptions = {}, |
| 61 | ) => { |
| 62 | const resolved = path.resolve(filePath); |
| 63 | if (!fs.existsSync(resolved)) { |
| 64 | throw new Error(`File not found: ${resolved}`); |
| 65 | } |
| 66 | |
| 67 | const stat = fs.statSync(resolved); |
| 68 | if (!stat.isFile()) { |
| 69 | throw new Error(`Not a file: ${resolved}`); |
| 70 | } |
| 71 | |
| 72 | const fileName = path.basename(resolved); |
| 73 | const fileBuffer = fs.readFileSync(resolved); |
| 74 | |
| 75 | // Compute SHA-256 hash for deduplication |
| 76 | const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); |
| 77 | |
| 78 | const ext = path.extname(fileName).toLowerCase().slice(1); |
| 79 | const fileType = detectMimeType(fileName); |
| 80 | |
| 81 | const date = new Date().toLocaleDateString('en-CA'); // YYYY-MM-DD |
| 82 | |
| 83 | // 1. Dedup: if the same bytes are already stored (and the object still |
| 84 | // exists), skip the S3 upload entirely and reuse the existing url. |
| 85 | const existing = (await client.file.checkFileHash.mutate({ hash })) as { |
| 86 | isExist?: boolean; |
| 87 | url?: string; |
| 88 | }; |
| 89 | |
| 90 | let pathname: string; |
| 91 | if (existing?.isExist && existing.url) { |
| 92 | pathname = existing.url; |
| 93 | } else { |
| 94 | // 2. Get a pre-signed upload URL and PUT the bytes to S3 |
| 95 | pathname = ext ? `files/${date}/${hash}.${ext}` : `files/${date}/${hash}`; |
| 96 | const presigned = await client.upload.createS3PreSignedUrl.mutate({ pathname }); |
| 97 | |
| 98 | const presignedUrl = typeof presigned === 'string' ? presigned : (presigned as any).url; |
| 99 | const uploadRes = await fetch(presignedUrl, { |
| 100 | body: fileBuffer, |
| 101 | headers: { 'Content-Type': fileType }, |
| 102 | method: 'PUT', |
| 103 | }); |
| 104 | if (!uploadRes.ok) { |
| 105 | throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | // 3. Create the file record |
| 110 | return await client.file.createFile.mutate({ |
| 111 | fileType, |
| 112 | hash, |
| 113 | knowledgeBaseId: options.knowledgeBaseId, |
| 114 | metadata: { |
no test coverage detected