fix: deduplicate attachment file writes (P0-1)
Upload route now passes persisted filePath to claude-client, which reuses it instead of writing the file again. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
5878b36ff6
commit
f3c49a9e49
2 files changed
+40
-27
No files matched your search
@@ -30,13 +30,14 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Save user message — persist file metadata so attachments survive page reload
|
||||
let savedContent = content;
|
||||
let fileMeta: Array<{ id: string; name: string; type: string; size: number; filePath: string }> | undefined;
|
||||
if (files && files.length > 0) {
|
||||
const workDir = session.working_directory || process.cwd();
|
||||
const uploadDir = path.join(workDir, '.codepilot-uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
const fileMeta = files.map((f) => {
|
||||
fileMeta = files.map((f) => {
|
||||
const safeName = path.basename(f.name).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const filePath = path.join(uploadDir, `${Date.now()}-${safeName}`);
|
||||
const buffer = Buffer.from(f.data, 'base64');
|
||||
@@ -81,15 +82,21 @@ export async function POST(request: NextRequest) {
|
||||
abortController.abort();
|
||||
});
|
||||
|
||||
// Convert file attachments to the format expected by streamClaude
|
||||
// Convert file attachments to the format expected by streamClaude.
|
||||
// Include filePath from the already-saved files so claude-client can
|
||||
// reference the on-disk copies instead of writing them again.
|
||||
const fileAttachments: FileAttachment[] | undefined = files && files.length > 0
|
||||
? files.map((f, i) => ({
|
||||
id: f.id || `file-${Date.now()}-${i}`,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
data: f.data,
|
||||
}))
|
||||
? files.map((f, i) => {
|
||||
const meta = fileMeta?.find((m: { id: string }) => m.id === f.id);
|
||||
return {
|
||||
id: f.id || `file-${Date.now()}-${i}`,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
data: f.data,
|
||||
filePath: meta?.filePath,
|
||||
};
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Stream Claude response, using SDK session ID for resume if available
|
||||
|
||||
+24
-18
@@ -135,26 +135,32 @@ function extractTokenUsage(msg: SDKResultMessage): TokenUsage | null {
|
||||
* Returns a ReadableStream of SSE-formatted strings.
|
||||
*/
|
||||
/**
|
||||
* Save non-image file attachments to a temporary upload directory
|
||||
* and return the file paths. The files are placed in .codepilot-uploads/
|
||||
* under the working directory so Claude's Read tool can access them.
|
||||
* Get file paths for non-image attachments. If the file already has a
|
||||
* persisted filePath (written by the uploads route), reuse it. Otherwise
|
||||
* fall back to writing the file to .codepilot-uploads/.
|
||||
*/
|
||||
function saveUploadedFiles(files: FileAttachment[], workDir: string): string[] {
|
||||
const uploadDir = path.join(workDir, '.codepilot-uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
const savedPaths: string[] = [];
|
||||
function getUploadedFilePaths(files: FileAttachment[], workDir: string): string[] {
|
||||
const paths: string[] = [];
|
||||
let uploadDir: string | undefined;
|
||||
for (const file of files) {
|
||||
// Sanitize filename to prevent directory traversal
|
||||
const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const timestamp = Date.now();
|
||||
const filePath = path.join(uploadDir, `${timestamp}-${safeName}`);
|
||||
const buffer = Buffer.from(file.data, 'base64');
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
savedPaths.push(filePath);
|
||||
if (file.filePath) {
|
||||
paths.push(file.filePath);
|
||||
} else {
|
||||
// Fallback: write file to disk (should not happen in normal flow)
|
||||
if (!uploadDir) {
|
||||
uploadDir = path.join(workDir, '.codepilot-uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
const safeName = path.basename(file.name).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const filePath = path.join(uploadDir, `${Date.now()}-${safeName}`);
|
||||
const buffer = Buffer.from(file.data, 'base64');
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
paths.push(filePath);
|
||||
}
|
||||
}
|
||||
return savedPaths;
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function streamClaude(options: ClaudeStreamOptions): ReadableStream<string> {
|
||||
@@ -409,7 +415,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
let textPrompt = prompt;
|
||||
if (nonImageFiles.length > 0) {
|
||||
const workDir = workingDirectory || process.cwd();
|
||||
const savedPaths = saveUploadedFiles(nonImageFiles, workDir);
|
||||
const savedPaths = getUploadedFilePaths(nonImageFiles, workDir);
|
||||
const fileReferences = savedPaths
|
||||
.map((p, i) => `[User attached file: ${p} (${nonImageFiles[i].name})]`)
|
||||
.join('\n');
|
||||
|
||||
Reference in new issue
Block a user