import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { sftpUploadContract } from '@/lib/api/contracts/storage-transfer' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' import { createSftpConnection, getSftp, isPathSafe, sanitizeFileName, sanitizePath, sftpExists, } from '@/app/api/tools/sftp/utils' export const dynamic = 'force-dynamic' const logger = createLogger('SftpUploadAPI') export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, { status: 401 } ) } logger.info(`[${requestId}] Authenticated SFTP upload request via ${authResult.authType}`, { userId: authResult.userId, }) const parsed = await parseRequest(sftpUploadContract, request, {}) if (!parsed.success) return parsed.response const params = parsed.data.body const hasFiles = params.files && params.files.length > 0 const hasDirectContent = params.fileContent && params.fileName if (!hasFiles && !hasDirectContent) { return NextResponse.json( { error: 'Either files or fileContent with fileName must be provided' }, { status: 400 } ) } if (!isPathSafe(params.remotePath)) { logger.warn(`[${requestId}] Path traversal attempt detected in remotePath`) return NextResponse.json( { error: 'Invalid remote path: path traversal sequences are not allowed' }, { status: 400 } ) } logger.info(`[${requestId}] Connecting to SFTP server ${params.host}:${params.port}`) const client = await createSftpConnection({ host: params.host, port: params.port, username: params.username, password: params.password, privateKey: params.privateKey, passphrase: params.passphrase, }) try { const sftp = await getSftp(client) const remotePath = sanitizePath(params.remotePath) const uploadedFiles: Array<{ name: string; remotePath: string; size: number }> = [] if (hasFiles) { const rawFiles = params.files! logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload`) const userFiles = processFilesToUserFiles(rawFiles, requestId, logger) const totalSize = userFiles.reduce((sum, file) => sum + file.size, 0) const maxSize = 100 * 1024 * 1024 if (totalSize > maxSize) { const sizeMB = (totalSize / (1024 * 1024)).toFixed(2) return NextResponse.json( { success: false, error: `Total file size (${sizeMB}MB) exceeds limit of 100MB` }, { status: 400 } ) } let resolvedTotal = 0 for (const file of userFiles) { try { const denied = await assertToolFileAccess( file.key, authResult.userId, requestId, logger ) if (denied) return denied logger.info( `[${requestId}] Downloading file for upload: ${file.name} (${file.size} bytes)` ) const { buffer } = await downloadServableFileFromStorage(file, requestId, logger) resolvedTotal += buffer.length if (resolvedTotal > maxSize) { const sizeMB = (resolvedTotal / (1024 * 1024)).toFixed(2) return NextResponse.json( { success: false, error: `Total file size (${sizeMB}MB) exceeds limit of 100MB` }, { status: 400 } ) } const safeFileName = sanitizeFileName(file.name) const fullRemotePath = remotePath.endsWith('/') ? `${remotePath}${safeFileName}` : `${remotePath}/${safeFileName}` const sanitizedRemotePath = sanitizePath(fullRemotePath) if (!params.overwrite) { const exists = await sftpExists(sftp, sanitizedRemotePath) if (exists) { logger.warn(`[${requestId}] File ${sanitizedRemotePath} already exists, skipping`) continue } } await new Promise((resolve, reject) => { const writeStream = sftp.createWriteStream(sanitizedRemotePath, { mode: params.permissions ? Number.parseInt(params.permissions, 8) : 0o644, }) writeStream.on('error', reject) writeStream.on('close', () => resolve()) writeStream.end(buffer) }) uploadedFiles.push({ name: safeFileName, remotePath: sanitizedRemotePath, size: buffer.length, }) logger.info(`[${requestId}] Uploaded ${safeFileName} to ${sanitizedRemotePath}`) } catch (error) { const notReady = docNotReadyResponse(error) if (notReady) return notReady logger.error(`[${requestId}] Failed to upload file ${file.name}:`, error) throw new Error( `Failed to upload file "${file.name}": ${getErrorMessage(error, 'Unknown error')}` ) } } } if (hasDirectContent) { const safeFileName = sanitizeFileName(params.fileName!) const fullRemotePath = remotePath.endsWith('/') ? `${remotePath}${safeFileName}` : `${remotePath}/${safeFileName}` const sanitizedRemotePath = sanitizePath(fullRemotePath) if (!params.overwrite) { const exists = await sftpExists(sftp, sanitizedRemotePath) if (exists) { return NextResponse.json( { error: 'File already exists and overwrite is disabled' }, { status: 409 } ) } } let content: Buffer try { content = Buffer.from(params.fileContent!, 'base64') const reEncoded = content.toString('base64') if (reEncoded !== params.fileContent) { content = Buffer.from(params.fileContent!, 'utf-8') } } catch { content = Buffer.from(params.fileContent!, 'utf-8') } await new Promise((resolve, reject) => { const writeStream = sftp.createWriteStream(sanitizedRemotePath, { mode: params.permissions ? Number.parseInt(params.permissions, 8) : 0o644, }) writeStream.on('error', reject) writeStream.on('close', () => resolve()) writeStream.end(content) }) uploadedFiles.push({ name: safeFileName, remotePath: sanitizedRemotePath, size: content.length, }) logger.info(`[${requestId}] Uploaded direct content to ${sanitizedRemotePath}`) } logger.info(`[${requestId}] SFTP upload completed: ${uploadedFiles.length} file(s)`) return NextResponse.json({ success: true, uploadedFiles, message: `Successfully uploaded ${uploadedFiles.length} file(s)`, }) } finally { client.end() } } catch (error) { const errorMessage = getErrorMessage(error, 'Unknown error occurred') logger.error(`[${requestId}] SFTP upload failed:`, error) return NextResponse.json({ error: `SFTP upload failed: ${errorMessage}` }, { status: 500 }) } })