-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Feat/file block write #3665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+461
−16
Merged
Feat/file block write #3665
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
c1e4f01
Add file write and delete operations
7d0baa4
Add file block write operation
fdb6039
Fix lint
031e12f
--wip-- [skip ci]
80cba4d
Allow loop-in-loop workflow edits
ebbd26c
Fix type error
ec7f8f8
Merge branch 'staging' into feat/file-block-write
3f55f06
Merge branch 'staging' into feat/file-block-write
439ce7e
Remove file id input, output link correctly
8a789f4
Add append tool
f397b7d
fix lint
70099c7
Address feedback
d5352a7
Handle writing to same file name gracefully
2d76881
Removed mime type from append block
c18d1db
Add lock for file append operation
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { checkInternalAuth } from '@/lib/auth/hybrid' | ||
| import { acquireLock, releaseLock } from '@/lib/core/config/redis' | ||
| import { ensureAbsoluteUrl } from '@/lib/core/utils/urls' | ||
| import { | ||
| downloadWorkspaceFile, | ||
| getWorkspaceFileByName, | ||
| updateWorkspaceFileContent, | ||
| uploadWorkspaceFile, | ||
| } from '@/lib/uploads/contexts/workspace/workspace-file-manager' | ||
| import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' | ||
|
|
||
| export const dynamic = 'force-dynamic' | ||
|
|
||
| const logger = createLogger('FileManageAPI') | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| const auth = await checkInternalAuth(request, { requireWorkflowId: false }) | ||
| if (!auth.success) { | ||
| return NextResponse.json({ success: false, error: auth.error }, { status: 401 }) | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url) | ||
| const userId = auth.userId || searchParams.get('userId') | ||
|
|
||
| if (!userId) { | ||
| return NextResponse.json({ success: false, error: 'userId is required' }, { status: 400 }) | ||
| } | ||
|
|
||
| let body: Record<string, unknown> | ||
| try { | ||
| body = await request.json() | ||
| } catch { | ||
| return NextResponse.json({ success: false, error: 'Invalid JSON body' }, { status: 400 }) | ||
| } | ||
|
|
||
| const workspaceId = (body.workspaceId as string) || searchParams.get('workspaceId') | ||
| if (!workspaceId) { | ||
| return NextResponse.json({ success: false, error: 'workspaceId is required' }, { status: 400 }) | ||
| } | ||
|
|
||
| const operation = body.operation as string | ||
|
|
||
| try { | ||
| switch (operation) { | ||
| case 'write': { | ||
| const fileName = body.fileName as string | undefined | ||
| const content = body.content as string | undefined | ||
| const contentType = body.contentType as string | undefined | ||
|
|
||
| if (!fileName) { | ||
| return NextResponse.json( | ||
| { success: false, error: 'fileName is required for write operation' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| if (!content && content !== '') { | ||
| return NextResponse.json( | ||
| { success: false, error: 'content is required for write operation' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(fileName)) | ||
| const fileBuffer = Buffer.from(content ?? '', 'utf-8') | ||
| const result = await uploadWorkspaceFile( | ||
| workspaceId, | ||
| userId, | ||
| fileBuffer, | ||
| fileName, | ||
| mimeType | ||
| ) | ||
|
|
||
| logger.info('File created', { | ||
| fileId: result.id, | ||
| name: fileName, | ||
| size: fileBuffer.length, | ||
| }) | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
| data: { | ||
| id: result.id, | ||
| name: result.name, | ||
| size: fileBuffer.length, | ||
| url: ensureAbsoluteUrl(result.url), | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| case 'append': { | ||
| const fileName = body.fileName as string | undefined | ||
| const content = body.content as string | undefined | ||
|
|
||
| if (!fileName) { | ||
| return NextResponse.json( | ||
| { success: false, error: 'fileName is required for append operation' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| if (!content && content !== '') { | ||
| return NextResponse.json( | ||
| { success: false, error: 'content is required for append operation' }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const existing = await getWorkspaceFileByName(workspaceId, fileName) | ||
| if (!existing) { | ||
| return NextResponse.json( | ||
| { success: false, error: `File not found: "${fileName}"` }, | ||
| { status: 404 } | ||
| ) | ||
| } | ||
|
|
||
| const lockKey = `file-append:${workspaceId}:${existing.id}` | ||
| const lockValue = `${Date.now()}-${Math.random().toString(36).slice(2)}` | ||
| const acquired = await acquireLock(lockKey, lockValue, 30) | ||
| if (!acquired) { | ||
| return NextResponse.json( | ||
| { success: false, error: 'File is busy, please retry' }, | ||
| { status: 409 } | ||
| ) | ||
| } | ||
|
|
||
| try { | ||
| const existingBuffer = await downloadWorkspaceFile(existing) | ||
| const finalContent = existingBuffer.toString('utf-8') + content | ||
| const fileBuffer = Buffer.from(finalContent, 'utf-8') | ||
| await updateWorkspaceFileContent(workspaceId, existing.id, userId, fileBuffer) | ||
|
|
||
| logger.info('File appended', { | ||
| fileId: existing.id, | ||
| name: existing.name, | ||
| size: fileBuffer.length, | ||
| }) | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
| data: { | ||
| id: existing.id, | ||
| name: existing.name, | ||
| size: fileBuffer.length, | ||
| url: ensureAbsoluteUrl(existing.path), | ||
| }, | ||
| }) | ||
| } finally { | ||
| await releaseLock(lockKey, lockValue) | ||
| } | ||
| } | ||
|
|
||
| default: | ||
| return NextResponse.json( | ||
| { success: false, error: `Unknown operation: ${operation}. Supported: write, append` }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : 'Unknown error' | ||
| logger.error('File operation failed', { operation, error: message }) | ||
| return NextResponse.json({ success: false, error: message }, { status: 500 }) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.