@@ -10,9 +10,9 @@ OF ANY KIND, either express or implied. See the License for the specific languag
1010governing permissions and limitations under the License.
1111*/
1212
13- const lib = require ( ' @adobe/aio-lib-files' ) ;
14- const https = require ( ' https' ) ;
15- const http = require ( ' http' ) ;
13+ const lib = require ( " @adobe/aio-lib-files" ) ;
14+ const https = require ( " https" ) ;
15+ const http = require ( " http" ) ;
1616
1717/**
1818 * AIO Files Service - A clean abstraction for Adobe I/O Files operations
@@ -30,9 +30,9 @@ class AIOFilesService {
3030 */
3131 async init ( ) {
3232 if ( ! this . filesClient ) {
33- this . logger . info ( ' Initializing AIO Files client' ) ;
33+ this . logger . info ( " Initializing AIO Files client" ) ;
3434 this . filesClient = await lib . init ( ) ;
35- this . logger . info ( ' AIO Files client initialized successfully' ) ;
35+ this . logger . info ( " AIO Files client initialized successfully" ) ;
3636 }
3737 }
3838
@@ -64,17 +64,19 @@ class AIOFilesService {
6464 const startFetch = Date . now ( ) ;
6565 const fileData = await this . _fetchFileStream ( url ) ;
6666 const fetchDuration = Date . now ( ) - startFetch ;
67-
67+
6868 this . logger . info ( `File fetched in ${ fetchDuration } ms` ) ;
6969 this . logger . info ( `Content-Type: ${ fileData . contentType } ` ) ;
70- this . logger . info ( `Content-Length: ${ this . _formatBytes ( fileData . contentLength ) } ` ) ;
70+ this . logger . info (
71+ `Content-Length: ${ this . _formatBytes ( fileData . contentLength ) } `
72+ ) ;
7173
7274 // Upload to AIO Files
7375 const filePath = `uploads/${ id } /${ name } ` ;
7476 const startUpload = Date . now ( ) ;
7577 await this . filesClient . write ( filePath , fileData . stream ) ;
7678 const uploadDuration = Date . now ( ) - startUpload ;
77-
79+
7880 this . logger . info ( `File uploaded to ${ filePath } in ${ uploadDuration } ms` ) ;
7981
8082 const totalDuration = Date . now ( ) - startFetch ;
@@ -85,13 +87,13 @@ class AIOFilesService {
8587 metadata : {
8688 contentType : fileData . contentType ,
8789 size : this . _formatBytes ( fileData . contentLength ) ,
88- sizeBytes : fileData . contentLength
90+ sizeBytes : fileData . contentLength ,
8991 } ,
9092 performance : {
9193 fetchDurationMs : fetchDuration ,
9294 uploadDurationMs : uploadDuration ,
93- totalDurationMs : totalDuration
94- }
95+ totalDurationMs : totalDuration ,
96+ } ,
9597 } ;
9698 }
9799
@@ -106,10 +108,12 @@ class AIOFilesService {
106108
107109 this . logger . info ( `Generating presigned URL for: ${ filePath } ` ) ;
108110 const url = await this . filesClient . generatePresignURL ( filePath , {
109- expiryInSeconds
111+ expiryInSeconds,
110112 } ) ;
111-
112- this . logger . info ( `Presigned URL generated (expires in ${ expiryInSeconds } s)` ) ;
113+
114+ this . logger . info (
115+ `Presigned URL generated (expires in ${ expiryInSeconds } s)`
116+ ) ;
113117 return url ;
114118 }
115119
@@ -123,15 +127,15 @@ class AIOFilesService {
123127
124128 this . logger . info ( `Retrieving metadata for: ${ filePath } ` ) ;
125129 const properties = await this . filesClient . getProperties ( filePath ) ;
126-
130+
127131 return {
128132 path : filePath ,
129133 size : this . _formatBytes ( properties . contentLength || 0 ) ,
130134 sizeBytes : properties . contentLength || 0 ,
131135 contentType : properties . contentType ,
132136 lastModified : properties . lastModified ,
133137 isDirectory : properties . isDirectory ,
134- isPublic : properties . isPublic
138+ isPublic : properties . isPublic ,
135139 } ;
136140 }
137141
@@ -146,8 +150,8 @@ class AIOFilesService {
146150
147151 this . logger . info ( `Writing file to: ${ filePath } ` ) ;
148152 await this . filesClient . write ( filePath , content ) ;
149- this . logger . info ( ' File written successfully' ) ;
150-
153+ this . logger . info ( " File written successfully" ) ;
154+
151155 return filePath ;
152156 }
153157
@@ -157,32 +161,62 @@ class AIOFilesService {
157161 * @param {string } url - The URL to fetch
158162 * @returns {Promise<Object> } Stream and metadata
159163 */
160- async _fetchFileStream ( url ) {
164+ /**
165+ * Fetch a file from URL and return a readable stream with metadata.
166+ * Handles HTTP(S) redirects up to a maximum number of hops.
167+ * @private
168+ * @param {string } url - The URL to fetch
169+ * @returns {Promise<Object> } Stream and metadata
170+ */
171+ async _fetchFileStream ( url , _redirectCount = 0 ) {
172+ const MAX_REDIRECTS = 5 ;
161173 return new Promise ( ( resolve , reject ) => {
162- const protocol = url . startsWith ( 'https' ) ? https : http ;
163-
164- protocol . get ( url , ( response ) => {
165- // Handle redirects
166- if ( response . statusCode === 301 || response . statusCode === 302 ) {
167- if ( response . headers . location ) {
168- this . _fetchFileStream ( response . headers . location )
169- . then ( resolve )
170- . catch ( reject ) ;
174+ const protocol = url . startsWith ( "https" ) ? https : http ;
175+ protocol
176+ . get ( url , ( response ) => {
177+ // Handle HTTP redirects (3xx)
178+ if ( [ 301 , 302 , 303 , 307 , 308 ] . includes ( response . statusCode ) ) {
179+ if ( response . headers . location ) {
180+ if ( _redirectCount >= MAX_REDIRECTS ) {
181+ reject (
182+ new Error ( `Too many redirects when fetching file: ${ url } ` )
183+ ) ;
184+ return ;
185+ }
186+ // Support for relative Location header
187+ const newUrl = new URL ( response . headers . location , url ) . toString ( ) ;
188+ this . _fetchFileStream ( newUrl , _redirectCount + 1 )
189+ . then ( resolve )
190+ . catch ( reject ) ;
191+ return ;
192+ } else {
193+ reject (
194+ new Error (
195+ `Redirected with no location header when fetching file: ${ url } `
196+ )
197+ ) ;
198+ return ;
199+ }
200+ }
201+
202+ if ( response . statusCode !== 200 ) {
203+ reject (
204+ new Error ( `Failed to fetch file: HTTP ${ response . statusCode } ` )
205+ ) ;
171206 return ;
172207 }
173- }
174-
175- if ( response . statusCode !== 200 ) {
176- reject ( new Error ( `Failed to fetch file: HTTP ${ response . statusCode } ` ) ) ;
177- return ;
178- }
179-
180- resolve ( {
181- stream : response ,
182- contentType : response . headers [ 'content-type' ] || 'application/octet-stream' ,
183- contentLength : parseInt ( response . headers [ 'content-length' ] || '0' , 10 )
184- } ) ;
185- } ) . on ( 'error' , reject ) ;
208+
209+ resolve ( {
210+ stream : response ,
211+ contentType :
212+ response . headers [ "content-type" ] || "application/octet-stream" ,
213+ contentLength : parseInt (
214+ response . headers [ "content-length" ] || "0" ,
215+ 10
216+ ) ,
217+ } ) ;
218+ } )
219+ . on ( "error" , reject ) ;
186220 } ) ;
187221 }
188222
@@ -193,11 +227,11 @@ class AIOFilesService {
193227 * @returns {string } Formatted string
194228 */
195229 _formatBytes ( bytes ) {
196- if ( bytes === 0 ) return ' 0 Bytes' ;
230+ if ( bytes === 0 ) return " 0 Bytes" ;
197231 const k = 1024 ;
198- const sizes = [ ' Bytes' , 'KB' , 'MB' , 'GB' , 'TB' ] ;
232+ const sizes = [ " Bytes" , "KB" , "MB" , "GB" , "TB" ] ;
199233 const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
200- return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ] ;
234+ return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + " " + sizes [ i ] ;
201235 }
202236}
203237
0 commit comments