Multiple files upload refactor (#1755)

- failed uploaded objects progress bar shows in red color
- fixed bug in where failed uploaded objects cannot be removed from
  listed objects in ObjectManager
- display delete button for failed upload objects
- display setErrorSnackMessage component after done uploading all
  objects with number of failed objects
- fixed race condition bug during multiple objects upload, now we are
  using Promise.allSettled to handle synchronization between uploads

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Lenin Alevski
2022-03-24 00:11:29 -07:00
committed by GitHub
parent 8772c158c6
commit bc1cb820d1
4 changed files with 133 additions and 125 deletions

View File

@@ -763,135 +763,135 @@ const ListObjects = ({
path: string, path: string,
folderPath: string folderPath: string
) => { ) => {
if (files.length > 0) { let uploadPromise = (file: File) => {
openList(); return new Promise((resolve, reject) => {
let nextFile = files.pop(); let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
let uploadPromise = (file: File) => { const fileName = file.name;
return new Promise((resolve, reject) => { const blobFile = new Blob([file], { type: file.type });
let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
const fileName = file.name;
const blobFile = new Blob([file], { type: file.type });
let encodedPath = ""; let encodedPath = "";
const relativeFolderPath = const relativeFolderPath =
get(file, "webkitRelativePath", "") !== "" get(file, "webkitRelativePath", "") !== ""
? get(file, "webkitRelativePath", "") ? get(file, "webkitRelativePath", "")
: folderPath; : folderPath;
if (path !== "" || relativeFolderPath !== "") { if (path !== "" || relativeFolderPath !== "") {
const finalFolderPath = relativeFolderPath const finalFolderPath = relativeFolderPath
.split("/") .split("/")
.slice(0, -1) .slice(0, -1)
.join("/"); .join("/");
encodedPath = encodeFileName( encodedPath = encodeFileName(
`${path}${finalFolderPath}${ `${path}${finalFolderPath}${
!finalFolderPath.endsWith("/") ? "/" : "" !finalFolderPath.endsWith("/") ? "/" : ""
}` }`
);
}
if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}
const identity = encodeFileName(
`${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`
); );
}
setNewObject({ if (encodedPath !== "") {
bucketName, uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
done: false, }
instanceID: identity,
percentage: 0,
prefix: `${decodeFileName(encodedPath)}${fileName}`,
type: "upload",
waitingForFile: false,
});
let xhr = new XMLHttpRequest(); const identity = encodeFileName(
xhr.open("POST", uploadUrl, true); `${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`
);
const areMultipleFiles = files.length > 1; setNewObject({
const errorMessage = `An error occurred while uploading the file${ bucketName,
areMultipleFiles ? "s" : "" done: false,
}.`; instanceID: identity,
const okMessage = `Object${ percentage: 0,
areMultipleFiles ? "s" : `` prefix: `${decodeFileName(encodedPath)}${fileName}`,
} uploaded successfully.`; type: "upload",
waitingForFile: false,
xhr.withCredentials = false;
xhr.onload = function (event) {
if (
xhr.status === 401 ||
xhr.status === 403 ||
xhr.status === 400 ||
xhr.status === 500
) {
if (xhr.response) {
const err = JSON.parse(xhr.response);
setSnackBarMessage(err.detailedMessage);
} else {
setSnackBarMessage(errorMessage);
}
}
if (xhr.status === 413) {
setSnackBarMessage("Error - File size too large");
}
if (xhr.status === 200) {
completeObject(identity);
if (files.length === 0) {
setSnackBarMessage(okMessage);
}
}
resolve(xhr.status);
if (files.length > 0) {
let nFile = files.pop();
if (nFile) {
return uploadPromise(nFile);
}
}
};
xhr.upload.addEventListener("error", (event) => {
setSnackBarMessage(errorMessage);
});
xhr.upload.addEventListener("progress", (event) => {
const progress = Math.floor((event.loaded * 100) / event.total);
updateProgress(identity, progress);
});
xhr.onerror = () => {
setSnackBarMessage(errorMessage);
reject(errorMessage);
};
xhr.onloadend = () => {
if (files.length === 0) {
setLoading(true);
}
};
const formData = new FormData();
if (file.size !== undefined) {
formData.append(file.size.toString(), blobFile, fileName);
xhr.send(formData);
}
}); });
};
if (nextFile) { let xhr = new XMLHttpRequest();
uploadPromise(nextFile!) xhr.open("POST", uploadUrl, true);
.then(() => {
console.info("done uploading file"); const areMultipleFiles = files.length > 1;
}) let errorMessage = `An error occurred while uploading the file${
.catch((err) => { areMultipleFiles ? "s" : ""
console.error("error uploading file,", err); }.`;
});
} const errorMessages: any = {
413: "Error - File size too large",
};
xhr.withCredentials = false;
xhr.onload = function (event) {
// resolve promise only when HTTP code is ok
if (xhr.status >= 200 && xhr.status < 300) {
completeObject(identity);
resolve({ status: xhr.status });
} else {
// reject promise if there was a server error
if (errorMessages[xhr.status]) {
errorMessage = errorMessages[xhr.status];
} else if (xhr.response) {
try {
const err = JSON.parse(xhr.response);
errorMessage = err.detailedMessage;
} catch (e) {
errorMessage = "something went wrong";
}
}
reject({ status: xhr.status, message: errorMessage });
}
};
xhr.upload.addEventListener("error", (event) => {
reject(errorMessage);
return;
});
xhr.upload.addEventListener("progress", (event) => {
const progress = Math.floor((event.loaded * 100) / event.total);
updateProgress(identity, progress);
});
xhr.onerror = () => {
reject(errorMessage);
return;
};
xhr.onloadend = () => {
if (files.length === 0) {
setLoading(true);
}
};
const formData = new FormData();
if (file.size !== undefined) {
formData.append(file.size.toString(), blobFile, fileName);
xhr.send(formData);
}
});
};
const uploadFilePromises: any = [];
// open object manager
openList();
for (let i = 0; i < files.length; i++) {
const file = files[i];
uploadFilePromises.push(uploadPromise(file));
} }
Promise.allSettled(uploadFilePromises).then((results: Array<any>) => {
const errors = results.filter(
(result) => result.status === "rejected"
);
if (errors.length > 0) {
const totalFiles = uploadFilePromises.length;
const successUploadedFiles =
uploadFilePromises.length - errors.length;
const err: ErrorResponseHandler = {
errorMessage: "There were some errors during file upload",
detailedError: `Uploaded files ${successUploadedFiles}/${totalFiles}`,
};
console.log("upload results", results);
setErrorSnackMessage(err);
}
});
}; };
upload(files, bucketName, pathPrefix, folderPath); upload(files, bucketName, pathPrefix, folderPath);
@@ -902,7 +902,7 @@ const ListObjects = ({
internalPaths, internalPaths,
openList, openList,
setNewObject, setNewObject,
setSnackBarMessage, setErrorSnackMessage,
updateProgress, updateProgress,
] ]
); );

View File

@@ -120,7 +120,7 @@ const ObjectHandled = ({
<Fragment> <Fragment>
<div <div
className={`${classes.container} ${ className={`${classes.container} ${
!objectToDisplay.done ? "inProgress" : "" objectToDisplay.percentage !== 100 ? "inProgress" : ""
}`} }`}
> >
<div className={classes.clearListIcon}> <div className={classes.clearListIcon}>
@@ -129,9 +129,9 @@ const ObjectHandled = ({
deleteFromList(objectToDisplay.instanceID); deleteFromList(objectToDisplay.instanceID);
}} }}
className={`${classes.closeButton} hideOnProgress showOnHover`} className={`${classes.closeButton} hideOnProgress showOnHover`}
disabled={!objectToDisplay.done} disabled={objectToDisplay.percentage !== 100}
> >
<span className={classes.closeIcon}></span> <span className={classes.closeIcon} />
</button> </button>
</div> </div>
<div className={classes.objectDetails}> <div className={classes.objectDetails}>

View File

@@ -68,10 +68,18 @@ const ProgressBarWrapper = ({
withLabel, withLabel,
size = "regular", size = "regular",
}: IProgressBarWrapper) => { }: IProgressBarWrapper) => {
let color: any;
if (value === 100 && ready) {
color = "success";
} else if (value === 100 && !ready) {
color = "error";
} else {
color = "primary";
}
const propsComponent: LinearProgressProps = { const propsComponent: LinearProgressProps = {
variant: indeterminate && !ready ? "indeterminate" : "determinate", variant: indeterminate && !ready ? "indeterminate" : "determinate",
value: ready ? 100 : value, value: ready ? 100 : value,
color: ready ? "success" : "primary", color: color,
}; };
if (withLabel) { if (withLabel) {
return <LinearProgressWithLabel {...propsComponent} />; return <LinearProgressWithLabel {...propsComponent} />;

View File

@@ -202,7 +202,7 @@ export function objectBrowserReducer(
}; };
case OBJECT_MANAGER_CLEAN_LIST: case OBJECT_MANAGER_CLEAN_LIST:
const nonCompletedList = state.objectManager.objectsToManage.filter( const nonCompletedList = state.objectManager.objectsToManage.filter(
(item) => !item.done (item) => item.percentage !== 100
); );
return { return {