88import * as path from "path" ;
99import { Readable } from "stream" ;
1010
11+ import { getLogger } from "magic-hour/logger" ;
1112import { UploadUrlsClient } from "magic-hour/resources/v1/files/upload-urls" ;
1213
1314export type FileInput =
@@ -17,6 +18,25 @@ export type FileInput =
1718 | File
1819 | NodeJS . ReadableStream ;
1920
21+ /**
22+ * Check if the given string is a valid HTTP/HTTPS URL.
23+ */
24+ function isUrl ( str : string ) : boolean {
25+ try {
26+ const url = new URL ( str ) ;
27+ return url . protocol === "http:" || url . protocol === "https:" ;
28+ } catch {
29+ return false ;
30+ }
31+ }
32+
33+ /**
34+ * Check if the given string is an already-uploaded path (api-assets/).
35+ */
36+ function isAlreadyUploaded ( str : string ) : boolean {
37+ return str . startsWith ( "api-assets/" ) ;
38+ }
39+
2040/**
2141 * Determine file type and extension from file path or name.
2242 */
@@ -128,14 +148,20 @@ export class FilesClient extends CoreResourceClient {
128148 * a file path that can be used as input for other Magic Hour API endpoints.
129149 * The file type is automatically detected from the file extension.
130150 *
151+ * If a URL (http:// or https://) or an already-uploaded path (api-assets/...)
152+ * is provided, it will be returned as-is without uploading.
153+ *
131154 * @param file - The file to upload. Can be:
132- * - **string**: Path to a local file (e.g., "/path/to/image.jpg")
155+ * - **string**: Path to a local file (e.g., "/path/to/image.jpg"),
156+ * a URL (e.g., "https://example.com/image.jpg"), or
157+ * an already-uploaded path (e.g., "api-assets/id/1234.png")
133158 * - **Buffer**: File content as buffer (requires extension detection via other means)
134159 * - **Readable**: Node.js readable stream (must have a 'path' property)
135160 * - **File**: File object (browser environment)
136161 *
137- * @returns The uploaded file's path in Magic Hour's storage system.
138- * This path can be used as input for other API endpoints.
162+ * @returns The file path that can be used as input for other API endpoints.
163+ * For local files, this will be the uploaded path in Magic Hour's storage.
164+ * For URLs and already-uploaded paths, this will be the input string as-is.
139165 *
140166 * @throws {Error } If the specified local file doesn't exist.
141167 * @throws {Error } If the file type is not supported.
@@ -151,6 +177,10 @@ export class FilesClient extends CoreResourceClient {
151177 * const filePath = await client.v1.files.uploadFile("/path/to/your/image.jpg");
152178 * console.log(`Uploaded file: ${filePath}`);
153179 *
180+ * // URLs are returned as-is (no upload needed)
181+ * const urlPath = await client.v1.files.uploadFile("https://example.com/image.jpg");
182+ * console.log(urlPath); // "https://example.com/image.jpg"
183+ *
154184 * // Use the uploaded file in other API calls
155185 * const result = await client.v1.aiImageUpscaler.create({
156186 * assets: { imageFilePath: filePath },
@@ -159,7 +189,29 @@ export class FilesClient extends CoreResourceClient {
159189 * ```
160190 */
161191 async uploadFile ( file : FileInput ) : Promise < string > {
192+ const logger = getLogger ( ) ;
193+
194+ // If the input is a URL or already-uploaded path, return it as-is
195+ if ( typeof file === "string" ) {
196+ if ( isUrl ( file ) ) {
197+ logger . debug ( `Skipping upload for ${ file } since it is a valid URL` ) ;
198+ return file ;
199+ } else if ( isAlreadyUploaded ( file ) ) {
200+ logger . debug (
201+ `Skipping upload for ${ file } since it is a valid already-uploaded path` ,
202+ ) ;
203+ return file ;
204+ } else {
205+ logger . debug (
206+ `Processing file input for upload ${ file } as a local file` ,
207+ ) ;
208+ }
209+ }
210+
162211 const { filePath, fileData, fileType, extension } = processFileInput ( file ) ;
212+ logger . debug (
213+ `File processed: type=${ fileType } , extension=${ extension } , source=${ filePath ? "path" : "data" } ` ,
214+ ) ;
163215
164216 // Create upload URL
165217 const response = await this . uploadUrls . create ( {
@@ -179,25 +231,32 @@ export class FilesClient extends CoreResourceClient {
179231 if ( ! uploadInfo ) {
180232 throw new Error ( "Upload info is missing from server response" ) ;
181233 }
234+ logger . debug ( `Received upload URL, target path: ${ uploadInfo . filePath } ` ) ;
182235
183236 // Prepare file content
184237 let content : Buffer ;
185238 if ( filePath ) {
186239 content = fs . readFileSync ( filePath ) ;
240+ logger . debug ( `Read ${ content . length } bytes from local file: ${ filePath } ` ) ;
187241 } else if ( fileData ) {
188242 if ( Buffer . isBuffer ( fileData ) ) {
189243 content = fileData ;
244+ logger . debug ( `Using buffer data: ${ content . length } bytes` ) ;
190245 } else if ( fileData instanceof Readable ) {
191246 // For streams, read all data into buffer
192247 const chunks : Buffer [ ] = [ ] ;
193248 for await ( const chunk of fileData ) {
194249 chunks . push ( chunk ) ;
195250 }
196251 content = Buffer . concat ( chunks ) ;
252+ logger . debug ( `Read ${ content . length } bytes from stream` ) ;
197253 } else if ( typeof File !== "undefined" && fileData instanceof File ) {
198254 // File object - convert to buffer
199255 const arrayBuffer = await fileData . arrayBuffer ( ) ;
200256 content = Buffer . from ( arrayBuffer ) ;
257+ logger . debug (
258+ `Read ${ content . length } bytes from File object: ${ fileData . name } ` ,
259+ ) ;
201260 } else {
202261 throw new Error ( "Unsupported file data type" ) ;
203262 }
@@ -222,6 +281,7 @@ export class FilesClient extends CoreResourceClient {
222281 ) ;
223282 }
224283
284+ logger . debug ( `Upload complete: ${ uploadInfo . filePath } ` ) ;
225285 return uploadInfo . filePath ;
226286 }
227287}
0 commit comments