Skip to content

Commit

Permalink
Multiple files upload refactor (#1755)
Browse files Browse the repository at this point in the history
- 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>
  • Loading branch information
Alevsk committed Mar 24, 2022
1 parent 8772c15 commit bc1cb82
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -763,135 +763,135 @@ const ListObjects = ({
path: string,
folderPath: string
) => {
if (files.length > 0) {
openList();
let nextFile = files.pop();
let uploadPromise = (file: File) => {
return new Promise((resolve, reject) => {
let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
const fileName = file.name;
const blobFile = new Blob([file], { type: file.type });

let encodedPath = "";
const relativeFolderPath =
get(file, "webkitRelativePath", "") !== ""
? get(file, "webkitRelativePath", "")
: folderPath;

if (path !== "" || relativeFolderPath !== "") {
const finalFolderPath = relativeFolderPath
.split("/")
.slice(0, -1)
.join("/");

encodedPath = encodeFileName(
`${path}${finalFolderPath}${
!finalFolderPath.endsWith("/") ? "/" : ""
}`
);
}
let uploadPromise = (file: File) => {
return new Promise((resolve, reject) => {
let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
const fileName = file.name;
const blobFile = new Blob([file], { type: file.type });

let encodedPath = "";
const relativeFolderPath =
get(file, "webkitRelativePath", "") !== ""
? get(file, "webkitRelativePath", "")
: folderPath;

if (path !== "" || relativeFolderPath !== "") {
const finalFolderPath = relativeFolderPath
.split("/")
.slice(0, -1)
.join("/");

encodedPath = encodeFileName(
`${path}${finalFolderPath}${
!finalFolderPath.endsWith("/") ? "/" : ""
}`
);
}

if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}
if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}

const identity = encodeFileName(
`${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`
);
const identity = encodeFileName(
`${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`
);

setNewObject({
bucketName,
done: false,
instanceID: identity,
percentage: 0,
prefix: `${decodeFileName(encodedPath)}${fileName}`,
type: "upload",
waitingForFile: false,
});

let xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl, true);

setNewObject({
bucketName,
done: false,
instanceID: identity,
percentage: 0,
prefix: `${decodeFileName(encodedPath)}${fileName}`,
type: "upload",
waitingForFile: false,
});

let xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl, true);

const areMultipleFiles = files.length > 1;
const errorMessage = `An error occurred while uploading the file${
areMultipleFiles ? "s" : ""
}.`;
const okMessage = `Object${
areMultipleFiles ? "s" : ``
} uploaded successfully.`;

xhr.withCredentials = false;
xhr.onload = function (event) {
if (
xhr.status === 401 ||
xhr.status === 403 ||
xhr.status === 400 ||
xhr.status === 500
) {
if (xhr.response) {
const areMultipleFiles = files.length > 1;
let errorMessage = `An error occurred while uploading the file${
areMultipleFiles ? "s" : ""
}.`;

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);
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);
errorMessage = err.detailedMessage;
} catch (e) {
errorMessage = "something went wrong";
}
}
};

xhr.upload.addEventListener("error", (event) => {
setSnackBarMessage(errorMessage);
});

xhr.upload.addEventListener("progress", (event) => {
const progress = Math.floor((event.loaded * 100) / event.total);
reject({ status: xhr.status, message: errorMessage });
}
};

updateProgress(identity, progress);
});
xhr.upload.addEventListener("error", (event) => {
reject(errorMessage);
return;
});

xhr.onerror = () => {
setSnackBarMessage(errorMessage);
reject(errorMessage);
};
xhr.onloadend = () => {
if (files.length === 0) {
setLoading(true);
}
};
xhr.upload.addEventListener("progress", (event) => {
const progress = Math.floor((event.loaded * 100) / event.total);

const formData = new FormData();
if (file.size !== undefined) {
formData.append(file.size.toString(), blobFile, fileName);
updateProgress(identity, progress);
});

xhr.send(formData);
xhr.onerror = () => {
reject(errorMessage);
return;
};
xhr.onloadend = () => {
if (files.length === 0) {
setLoading(true);
}
});
};

if (nextFile) {
uploadPromise(nextFile!)
.then(() => {
console.info("done uploading file");
})
.catch((err) => {
console.error("error uploading file,", err);
});
}
};

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);
Expand All @@ -902,7 +902,7 @@ const ListObjects = ({
internalPaths,
openList,
setNewObject,
setSnackBarMessage,
setErrorSnackMessage,
updateProgress,
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const ObjectHandled = ({
<Fragment>
<div
className={`${classes.container} ${
!objectToDisplay.done ? "inProgress" : ""
objectToDisplay.percentage !== 100 ? "inProgress" : ""
}`}
>
<div className={classes.clearListIcon}>
Expand All @@ -129,9 +129,9 @@ const ObjectHandled = ({
deleteFromList(objectToDisplay.instanceID);
}}
className={`${classes.closeButton} hideOnProgress showOnHover`}
disabled={!objectToDisplay.done}
disabled={objectToDisplay.percentage !== 100}
>
<span className={classes.closeIcon}></span>
<span className={classes.closeIcon} />
</button>
</div>
<div className={classes.objectDetails}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,18 @@ const ProgressBarWrapper = ({
withLabel,
size = "regular",
}: IProgressBarWrapper) => {
let color: any;
if (value === 100 && ready) {
color = "success";
} else if (value === 100 && !ready) {
color = "error";
} else {
color = "primary";
}
const propsComponent: LinearProgressProps = {
variant: indeterminate && !ready ? "indeterminate" : "determinate",
value: ready ? 100 : value,
color: ready ? "success" : "primary",
color: color,
};
if (withLabel) {
return <LinearProgressWithLabel {...propsComponent} />;
Expand Down
2 changes: 1 addition & 1 deletion portal-ui/src/screens/Console/ObjectBrowser/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export function objectBrowserReducer(
};
case OBJECT_MANAGER_CLEAN_LIST:
const nonCompletedList = state.objectManager.objectsToManage.filter(
(item) => !item.done
(item) => item.percentage !== 100
);

return {
Expand Down

0 comments on commit bc1cb82

Please sign in to comment.