@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
55import { toError } from '@sim/utils/errors'
66import { generateId } from '@sim/utils/id'
77import { toast } from '@/components/emcn'
8+ import { DirectUploadError , runUploadStrategy } from '@/lib/uploads/client/direct-upload'
89import { resolveFileType } from '@/lib/uploads/utils/file-utils'
910
1011const logger = createLogger ( 'useFileAttachments' )
@@ -51,6 +52,44 @@ interface UseFileAttachmentsProps {
5152 isLoading ?: boolean
5253}
5354
55+ /**
56+ * Server-proxied fallback used only when cloud storage isn't configured (local dev).
57+ * Production always takes the presigned PUT path.
58+ */
59+ async function uploadViaApiFallback (
60+ file : File ,
61+ workspaceId : string
62+ ) : Promise < { path : string ; key : string } > {
63+ const formData = new FormData ( )
64+ formData . append ( 'file' , file )
65+ formData . append ( 'context' , 'mothership' )
66+ formData . append ( 'workspaceId' , workspaceId )
67+
68+ // boundary-raw-fetch: local-dev fallback when cloud storage is not configured; multipart upload incompatible with requestJson
69+ const response = await fetch ( '/api/files/upload' , { method : 'POST' , body : formData } )
70+ if ( ! response . ok ) {
71+ const errorData = ( await response . json ( ) . catch ( ( ) => ( { } ) ) ) as {
72+ message ?: string
73+ error ?: string
74+ }
75+ throw new Error (
76+ errorData . message || errorData . error || `Failed to upload file: ${ response . status } `
77+ )
78+ }
79+ const data = ( await response . json ( ) ) as {
80+ fileInfo ?: { path ?: string ; key ?: string }
81+ path ?: string
82+ key ?: string
83+ url ?: string
84+ }
85+ const path = data . fileInfo ?. path ?? data . path ?? data . url
86+ const key = data . fileInfo ?. key ?? data . key
87+ if ( ! path || ! key ) {
88+ throw new Error ( 'Invalid upload response: missing path or key' )
89+ }
90+ return { path, key }
91+ }
92+
5493/**
5594 * Custom hook to manage file attachments including upload, drag/drop, and preview
5695 * Handles S3 presigned URL uploads and preview URL generation
@@ -115,6 +154,10 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
115154 logger . error ( 'User ID not available for file upload' )
116155 return
117156 }
157+ if ( ! workspaceId ) {
158+ logger . error ( 'workspaceId required for mothership uploads' )
159+ return
160+ }
118161
119162 const files = Array . from ( fileList )
120163 if ( files . length === 0 ) return
@@ -134,49 +177,34 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
134177
135178 setAttachedFiles ( ( prev ) => [ ...prev , ...placeholders ] )
136179
180+ const presignedEndpoint = `/api/files/presigned?type=mothership&workspaceId=${ encodeURIComponent ( workspaceId ) } `
181+
137182 await Promise . all (
138183 files . map ( async ( file , i ) => {
139184 const placeholder = placeholders [ i ]
140185 try {
141- const formData = new FormData ( )
142- formData . append ( 'file' , file )
143- formData . append ( 'context' , 'mothership' )
144- if ( workspaceId ) {
145- formData . append ( 'workspaceId' , workspaceId )
186+ let result : { path : string ; key : string }
187+ try {
188+ result = await runUploadStrategy ( {
189+ file,
190+ workspaceId,
191+ context : 'mothership' ,
192+ presignedEndpoint,
193+ } )
194+ } catch ( error ) {
195+ if ( error instanceof DirectUploadError && error . code === 'FALLBACK_REQUIRED' ) {
196+ result = await uploadViaApiFallback ( file , workspaceId )
197+ } else {
198+ throw error
199+ }
146200 }
147201
148- // boundary-raw-fetch: multipart/form-data upload (FileUpload boundary), incompatible with requestJson which JSON-stringifies bodies
149- const uploadResponse = await fetch ( '/api/files/upload' , {
150- method : 'POST' ,
151- body : formData ,
152- } )
153-
154- if ( ! uploadResponse . ok ) {
155- const errorData = await uploadResponse . json ( ) . catch ( ( ) => ( {
156- message : `Upload failed: ${ uploadResponse . status } ` ,
157- } ) )
158- throw new Error (
159- errorData . message ||
160- errorData . error ||
161- `Failed to upload file: ${ uploadResponse . status } `
162- )
163- }
164-
165- const uploadData = await uploadResponse . json ( )
166-
167- logger . info (
168- `File uploaded successfully: ${ uploadData . fileInfo ?. path || uploadData . path } `
169- )
202+ logger . info ( `File uploaded successfully: ${ result . path } ` )
170203
171204 setAttachedFiles ( ( prev ) =>
172205 prev . map ( ( f ) =>
173206 f . id === placeholder . id
174- ? {
175- ...f ,
176- path : uploadData . fileInfo ?. path || uploadData . path || uploadData . url ,
177- key : uploadData . fileInfo ?. key || uploadData . key ,
178- uploading : false ,
179- }
207+ ? { ...f , path : result . path , key : result . key , uploading : false }
180208 : f
181209 )
182210 )
0 commit comments