-
Notifications
You must be signed in to change notification settings - Fork 362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
8158 bug UI not using presign for uploading objects #8365
8158 bug UI not using presign for uploading objects #8365
Conversation
🎊 PR Preview 4f7f211 has been successfully built and deployed to https://treeverse-lakeFS-preview-pr-8365.surge.sh 🕐 Build time: 0.013s 🤖 By surge-preview |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really great catch, thanks!
A few change requests and a few questions and we're done 😃
const parsedHeaders = {}; | ||
|
||
headerLines.forEach((line) => { | ||
const [key, value] = line.split(':', 2).map((part) => part.trim()); | ||
if (key && value) { | ||
parsedHeaders[key.toLowerCase()] = value; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More of a styling suggestion to initialize the parsedHeaders variable with data in a more functional way:
const parsedHeaders = {}; | |
headerLines.forEach((line) => { | |
const [key, value] = line.split(':', 2).map((part) => part.trim()); | |
if (key && value) { | |
parsedHeaders[key.toLowerCase()] = value; | |
} | |
const parsedHeaders = headerLines.reduce((acc, line) => { | |
const [key, value] = line.split(':', 2).map((part) => part.trim()); | |
if (key && value) { | |
acc[key.toLowerCase()] = value; | |
} | |
return acc; | |
}, {}); |
const checksum = extractChecksumFromResponse(uploadResponse.rawHeaders); | ||
if (uploadResponse.status >= 400) { | ||
throw new Error(`Error uploading file: HTTP ${uploadResponse.status}`); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const checksum = extractChecksumFromResponse(uploadResponse.rawHeaders); | |
if (uploadResponse.status >= 400) { | |
throw new Error(`Error uploading file: HTTP ${uploadResponse.status}`); | |
} | |
if (uploadResponse.status >= 400) { | |
throw new Error(`Error uploading file: HTTP ${uploadResponse.status}`); | |
} | |
const checksum = extractChecksumFromResponse(uploadResponse.rawHeaders); |
const parsedHeaders = {}; | ||
|
||
headerLines.forEach((line) => { | ||
const [key, value] = line.split(':', 2).map((part) => part.trim()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splitting this way means that if there's a :
in the value of the line (the value of the header) it won't appear in the array. It's possible that it will never happen but I don't really know. If you want to make sure that the array will be split into a header name and a value (which might include a :
) you can use something like:
let [key, ...value] = line.split(':')
value = value.join(':')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition, it would be great if you could extract the headers map creation out (it could be used on its own), and call it from this function, or even better, call it from the uploadFile
function, and pass it to this function.
webui/src/lib/api/index.js
Outdated
@@ -671,9 +671,8 @@ export const uploadWithProgress = (url, file, method = 'POST', onProgress = null | |||
status: xhr.status, | |||
body: xhr.responseText, | |||
contentType: xhr.getResponseHeader('Content-Type'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this left?
Doesn't rawHeaders
include this one as well?
Thank you for your review @Jonathan-Rosenberg, I corrected everything you mentioned. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (uploadResponse.status >= 400) { | ||
throw new Error(`Error uploading file: HTTP ${uploadResponse.status}`); | ||
} | ||
const checksum = extractChecksumFromResponse(uploadResponse.rawHeaders); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation looks off
webui/src/lib/api/index.js
Outdated
@@ -1125,7 +1121,7 @@ class Statistics { | |||
|
|||
class Staging { | |||
async get(repoId, branchId, path, presign = false) { | |||
const query = qs({path, presign}); | |||
const query = qs({path, presign}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indentation looks off
webui/src/lib/api/index.js
Outdated
status: xhr.status, | ||
body: xhr.responseText, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should not get deleted
Defining CORS on the S3 bucket is crucial for the upload to succeed. If the user didn't configure the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments but we're almost there!
} | ||
// fallback to ETag | ||
if (parsedHeaders['etag']) { | ||
// drop any quote and space |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation again 😆
webui/src/lib/api/index.js
Outdated
}); | ||
xhr.addEventListener('error', () => reject(new Error('Upload Failed'))); | ||
xhr.addEventListener('error', () => resolve({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain why is the error resolved instead of getting rejected?
If this is because the calling function doesn't catch
it, then it should...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for this is that I wanted to throw the correct error. This way if there is an error due to missing CORS setting the calling func will catch it and throw suitable error. In case other piece of code uses this func and upload fails for some reason - the error will just state 'upload failed'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proper way of handling errors in Promises is using the try/catch mechanism at the calling function.
This should still reject, and the error (the object returned to the reject function) can be analyzed further by the calling function.
I would like to mention in this context that this is javascript and it's s**t.
const headersString = typeof rawHeaders === 'string' ? rawHeaders : rawHeaders.toString(); | ||
const cleanedHeadersString = headersString.trim(); | ||
const headerLines = cleanedHeadersString.split('\n'); | ||
const parsedHeaders = headerLines.reduce((acc, line) => { | ||
let [key, ...value] = line.split(':'); // split into key and the rest of the value | ||
key = key.trim(); | ||
value = value.join(':').trim(); | ||
if (key && value) { | ||
acc[key.toLowerCase()] = value; | ||
} | ||
return acc; | ||
}, {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please extract these lines to a function called parseRawHeaders
(or something like that), and instead of those line run:
const parsedHeaders = parseRawHeaders(rawHeaders);
if (parsedHeaders['content-md5']) {
.....
I would actually pass the extractChecksumFromResponse
function the parsed headers as an argument and let it fetch the checksum from it.
export const parseRawHeaders = (rawHeaders) => { | ||
const headersString = typeof rawHeaders === 'string' ? rawHeaders : rawHeaders.toString(); | ||
const cleanedHeadersString = headersString.trim(); | ||
const headerLines = cleanedHeadersString.split('\n'); | ||
const parsedHeaders = headerLines.reduce((acc, line) => { | ||
let [key, ...value] = line.split(':'); // split into key and the rest of the value | ||
key = key.trim(); | ||
value = value.join(':').trim(); | ||
if (key && value) { | ||
acc[key.toLowerCase()] = value; | ||
} | ||
return acc; | ||
}, {}); | ||
return parsedHeaders; | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved this func here, as ive seen that no func was exported from the other file to this, and it is now also being used in this file.
webui/src/lib/api/index.js
Outdated
async upload(repoId, branchId, path, fileObject, onProgressFn = null) { | ||
const query = qs({path}); | ||
const uploadUrl = `${API_ENDPOINT}/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/objects?` + query; | ||
const {status, body, contentType} = await uploadWithProgress(uploadUrl, fileObject, 'POST', onProgressFn) | ||
const {status, body, rawHeaders} = await uploadWithProgress(uploadUrl, fileObject, 'POST', onProgressFn) | ||
if (status !== 201) { | ||
const contentType = rawHeaders ? parseRawHeaders(rawHeaders)['content-type'] : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This func is being used when lakefs is configured not to use presigned url. The change I introduced is due to the change in uploadWithProgress that only returns raw headers. Tested this manually, and in case of an error the func does extract the header and the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great.
Only the indentation again 😅
return response.etag.replace(/[" ]+/g, ""); | ||
return parsedHeaders['etag'].replace(/"/g, ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the regex different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weird, can't tell, switching back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex explanation (which I think will not suffice 😅 ) and the indentation, and we're off!
try { | ||
const uploadResponse = await uploadWithProgress(getResp.presigned_url, file, 'PUT', onProgress, additionalHeaders); | ||
const parsedHeaders = parseRawHeaders(uploadResponse.rawHeaders); | ||
const checksum = extractChecksumFromResponse(parsedHeaders); | ||
await staging.link(repo.id, reference.id, fpath, getResp, checksum, file.size, file.type); | ||
} catch(error) { | ||
if (error.status >= 400) { | ||
throw new Error(`Error uploading file: HTTP ${error.status}`); | ||
} | ||
if (error.status === 0) { | ||
throw new Error(`CORS settings error. Check documentation for more info.`); | ||
} | ||
throw error; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
31bff69
to
e767f86
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this and answering all of my comments!
Closes #8158
Change Description
With the added changes, it is now possible to upload a file from the UI, with a minimal IAM permissions installation.
Background
See #8158
Bug Fix
Root cause - the GET request for the presigned url did not include a checksum of the desired file.
Solution - Adding the checksum parameter to the api for getPhysicalAddress operation
Additional info
In order to upload a file to a presigned url, there is a need to configure the bucket's CORS.