Skip to content

Commit b684cad

Browse files
authored
fix: aio upload handles redirect (#72)
1 parent b3d257e commit b684cad

File tree

2 files changed

+79
-44
lines changed

2 files changed

+79
-44
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ config.json
1515

1616
# Adobe I/O console config
1717
console.json
18+
console*.json
1819

1920
# Test output
2021
junit.xml

genstudio-external-dam-app/src/genstudiopem/actions/aio-lib-files/AIOFilesService.js

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ OF ANY KIND, either express or implied. See the License for the specific languag
1010
governing 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

Comments
 (0)