@beer-garden/addons
Version:
Awesome Addons for Angular Schema Form.
328 lines (283 loc) • 9.21 kB
JavaScript
import angular from "angular";
import CryptoJS from 'crypto-js';
class FileUploader {
constructor(url_prefix) {
this.file = null;
this.numChunks = 0;
this.failed = false;
this.fileId = null;
this.fileValid = false;
this.chunksComplete = 0;
this.chunkSize = 255 * 1024;
this.apiPath = url_prefix + "api/vbeta/chunks/";
}
reset(scope) {
// Have to do this first to avoid divide by zero (checks the numChunks field)
this.updateProgressBar(scope, 0);
// The normal reset routine
this.file = null;
this.numChunks = 0;
this.failed = false;
this.fileId = null;
this.fileValid = false;
this.chunksComplete = 0;
this.setVisible(scope.fileFailed, false, "", true);
this.setVisible(scope.fileCompleted, false, "", true);
}
setVisible(object, visible, msg = "", reset = false) {
if (visible) {
object.setAttribute("style", "visibility: visible");
if ((msg != "" && msg != null) || reset) {
object.setAttribute("title", msg);
}
} else {
object.setAttribute("style", "visibility: hidden");
if ((msg != "" && msg != null) || reset) {
object.setAttribute("Title", msg);
}
}
}
updateProgressBar(scope, number) {
scope.fileProgressBar.setAttribute(
"value",
Math.ceil((number / this.numChunks) * 100)
);
}
checkFileStatus(scope) {
$.get(this.apiPath + "?file_id=" + this.fileId + "&verify=true")
.done((data) => {
if ("valid" in data) {
// Convert the value to a boolean
this.fileValid = !!data["valid"];
if (this.fileValid) {
this.setVisible(scope.fileCompleted, true, data["message"]);
} else {
this.setVisible(scope.fileFailed, true, data["message"]);
}
} else {
this.setVisible(scope.fileFailed, true, data["message"]);
}
})
.fail(() => {
this.setVisible(
scope.fileFailed,
true,
"Could reach Beer Garden to verify file."
);
});
}
buildRequest(scope, offset, retries) {
var reader = new FileReader();
reader.onload = (e) => {
$.post(
this.apiPath + "?file_id=" + this.fileId,
JSON.stringify({
data: e.target.result.split(",")[1],
offset: offset,
})
)
.done(() => {
this.chunksComplete++;
this.updateProgressBar(scope, this.chunksComplete);
if (this.chunksComplete >= this.numChunks && !this.failed) {
this.checkFileStatus(scope);
}
})
.fail(() => {
if (retries > 0) {
this.buildRequest(offset, retries - 1);
} else if (!this.failed) {
this.failed = true;
this.setVisible(
scope.fileFailed,
true,
"File upload failed; tried to send chunk: " +
offset +
" too many times."
);
this.fileValid = false;
}
});
};
var chunk = this.file.slice(
offset * this.chunkSize,
offset * this.chunkSize + this.chunkSize
);
reader.readAsDataURL(chunk);
}
calculateMD5(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const wordArray = CryptoJS.lib.WordArray.create(event.target.result);
const hash = CryptoJS.MD5(wordArray);
resolve(hash.toString(CryptoJS.enc.Hex));
};
reader.onerror = (error) => {
reject(error);
};
reader.readAsArrayBuffer(file);
});
}
async uploadFile(file, ngModel, scope) {
this.file = file;
this.numChunks = Math.ceil(file.size / this.chunkSize);
this.md5_sum = await this.calculateMD5(file);
$.get(
this.apiPath +
"id/?file_name=" +
encodeURIComponent(file.name) +
"&file_size=" +
this.file.size +
"&chunk_size=" +
this.chunkSize +
"&md5_sum=" +
this.md5_sum
)
.done((data) => {
this.fileId = data["details"]["file_id"];
if (this.fileId && !!data["details"]["operation_complete"]) {
ngModel.$setViewValue(data);
scope.$apply();
for (
let offset = 0;
offset < this.file.size;
offset += this.chunkSize
) {
this.buildRequest(scope, Math.ceil(offset / this.chunkSize), 2);
}
} else {
this.setVisible(
scope.fileFailed,
true,
"File upload failed; could not retrieve a FileID for upload, message: " +
data["details"]["message"]
);
this.fileValid = false;
}
})
.fail(() => {
this.setVisible(
scope.fileFailed,
true,
"Could not reach Beer Garden to request a file ID."
);
this.fileValid = false;
});
}
}
fileUploadDirective.$inject = ["$rootScope"];
function fileUploadDirective($rootScope) {
return {
restrict: "A",
require: "ngModel",
scope: true,
priority: 500,
link: function (scope, element, attrs, ngModel) {
scope.fileUploader = new FileUploader($rootScope.config.urlPrefix);
scope.hasFile = false;
scope.file = undefined;
scope.ngModel = ngModel;
scope.fileName = undefined;
// Used to trigger the click() event on the hidden file input field.
scope.fileInput = element[0].querySelector(".file-upload-field");
scope.fileProgressBar = element[0].querySelector(
".file-upload-progress-bar"
);
scope.fileCompleted = element[0].querySelector(".file-upload-completed");
scope.fileFailed = element[0].querySelector(".file-upload-failed");
scope.validateField = function (value) {
if (value === null || value === undefined) {
if (scope.form.required && !scope.fileUploader.fileValid) {
// 302 is the error code for required
ngModel.$setValidity("tv4-302", false);
}
}
};
// Since we are not using the sf-validate directive, we need
// to manually listen for the validate event and call our validation
// function. This basically just applies the required validator.
scope.$on("schemaFormValidate", function () {
scope.validateField(ngModel.$viewValue);
});
var validateFile = function (file) {
var valid = true;
var schema = scope.$eval(attrs.fileUpload).schema;
if (file.size > parseInt(schema.maxSize, 10)) {
valid = false;
ngModel.$setValidity("maxFileUploadSize", false);
} else {
ngModel.$setValidity("maxFileUploadSize", true);
}
if (file.size < parseInt(schema.minSize, 10)) {
valid = false;
ngModel.$setValidity("minFileUploadSize", false);
} else {
ngModel.$setValidity("minFileUploadSize", true);
}
scope.$apply();
return valid;
};
var getFile = function (file) {
if (confirm("Would you like to upload this file?")) {
// Reset all errors so we start with a clean slate
Object.keys(ngModel.$error).forEach(function (k) {
ngModel.$setValidity(k, true);
});
if (!file) {
return;
}
if (!validateFile(file)) {
return;
}
scope.file = file;
scope.fileName = file.name;
scope.hasFile = true;
scope.file.ext = file.name.split(".").slice(-1)[0];
scope.fileUploader.uploadFile(file, ngModel, scope);
}
};
scope.removeFile = function (e) {
// $.delete not supported by jquery, use this instead.
var http = new XMLHttpRequest();
var url =
scope.fileUploader.apiPath + "?file_id=" + scope.fileUploader.fileId;
http.open("DELETE", url, true);
http.send("");
e.preventDefault();
e.stopPropagation();
scope.file = undefined;
scope.hasFile = false;
scope.fileName = undefined;
scope.fileUploader.reset(scope);
ngModel.$setViewValue(undefined);
};
angular.element(scope.fileInput).bind("change", function (e) {
getFile(e.target.files[0]);
});
},
};
}
function formatFileSize(size) {
var sizeToReturn = undefined;
if (angular.isDefined(size) && size !== null) {
var formattedSize = undefined;
var sizeType = undefined;
if (size > 1024 * 1024 * 1024) {
formattedSize = (size / 1024 / 1024 / 1024).toFixed(1);
sizeType = "GB";
} else if (size > 1024 * 1024) {
formattedSize = (size / 1024 / 1024).toFixed(1);
sizeType = "MB";
} else if (size > 1024 * 1024) {
formattedSize = (size / 1024).toFixed(1);
sizeType = "KB";
} else {
formattedSize = size;
sizeType = "B";
}
sizeToReturn = formattedSize + " " + sizeType;
}
return sizeToReturn;
}
export { fileUploadDirective, formatFileSize };