diff --git a/src/css/dvwebloader.css b/src/css/dvwebloader.css
index 944e8b4..f61dd34 100644
--- a/src/css/dvwebloader.css
+++ b/src/css/dvwebloader.css
@@ -104,3 +104,52 @@ h1 {
border-width: 2px;
border-style: solid;
}
+
+#refreshDataset {
+ float:right;
+}
+.button {
+ font-size:16px;
+}
+
+.pending {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ background-color: #f8f9fa;
+ border-left: 4px solid #007bff;
+ margin: 10px 0;
+}
+
+.spinner {
+ border: 3px solid rgba(0, 0, 0, 0.1);
+ border-radius: 50%;
+ border-top: 3px solid #007bff;
+ width: 20px;
+ height: 20px;
+ margin-right: 10px;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.button-flex-container {
+ display: flex;
+ justify-content: space-between;
+ margin: 10px 0;
+ min-height: 40px;
+}
+
+.button-left {
+ display: flex;
+ align-items: center;
+}
+
+.button-right {
+ display: flex;
+ align-items: center;
+ gap: 10px; /* Space between buttons */
+}
\ No newline at end of file
diff --git a/src/dvwebloader.html b/src/dvwebloader.html
index fc39f9f..536bff3 100644
--- a/src/dvwebloader.html
+++ b/src/dvwebloader.html
@@ -21,7 +21,13 @@
Folder Upload
diff --git a/src/js/fileupload2.js b/src/js/fileupload2.js
index 7687d16..d30c59a 100644
--- a/src/js/fileupload2.js
+++ b/src/js/fileupload2.js
@@ -1,11 +1,13 @@
import getLocalizedString from './lang.js';
let startUploadsHasBeenCalled = false;
+var isRetrievingDatasetInfo = false;
var fileList = [];
var rawFileMap = {};
var toRegisterFileList = [];
var observer2 = null;
var numDone = 0;
+var numDoneCounter = 0;
var delay = 100; //milliseconds
var draftExists = false;
var UploadState = {
@@ -34,18 +36,27 @@ var getUpId = (function() {
};
})();
//How many files are completely done
-var finishFile = (function() {
- var counter = 0;
- return function() {
- counter += 1;
- return counter;
- };
-})();
+function finishFile() {
+ numDoneCounter += 1;
+ return numDoneCounter;
+}
+
+/**
+ * Resets the upload state variables and counters
+ */
+function resetUploadState() {
+ toRegisterFileList = [];
+ fileList = [];
+ curFile = 0;
+ numDoneCounter = 0;
+ numDone = 0;
+ filesInProgress = 0;
+}
var siteUrl;
var datasetPid;
var apiKey;
-var existingFiles;
-var convertedFileNameMap;
+var existingFiles = {};
+var convertedFileNameMap = {};
var queryParams;
var dvLocale;
@@ -61,6 +72,7 @@ $(document).ready(function() {
dvLocale = queryParams.get("dvLocale");
console.log('locale: ' + dvLocale);
directUploadEnabled = true;
+ isRetrievingDatasetInfo = true;
initTranslation();
addMessage('info', 'msgGettingDatasetInfo');
fetch(siteUrl + "/api/files/fixityAlgorithm")
@@ -73,10 +85,14 @@ $(document).ready(function() {
}
}).then(checksumAlgJson => {
checksumAlgName = "MD5";
- if (checksumAlgJson != null) {
+ if (checksumAlgJson != null && checksumAlgJson.data) {
checksumAlgName = checksumAlgJson.data.message;
}
})
+ .catch(error => {
+ console.log("Error fetching fixity algorithm, using MD5: " + error);
+ checksumAlgName = "MD5";
+ })
.then(() => {
var head = document.getElementsByTagName('head')[0];
var js = document.createElement("script");
@@ -113,22 +129,13 @@ $(document).ready(function() {
var files = e.target.files; // FileList
for (let i = 0; i < files.length; ++i) {
let f = files[i];
- console.log('before ' + f.webkitRelativePath);
+ //console.log('before ' + f.webkitRelativePath);
queueFileForDirectUpload(f);
console.debug(files[i].webkitRelativePath);
// output.innerText = output.innerText + files[i].webkitRelativePath+"\n";
}
- let numExists = $('#filelist>.ui-fileupload-files .file-exists').length;
let totalFiles = Object.keys(rawFileMap).length;
- console.log('exists: ' + numExists);
- console.log('rawFileMap len: ' + totalFiles);
- if (totalFiles === numExists) {
- addMessage('info', 'msgFilesAlreadyExist');
- } else if (numExists !== 0 && totalFiles > numExists) {
- addMessage('info', 'msgUploadOnlyCheckedFiles');
- } else (
- addMessage('info', 'msgStartUpload')
- )
+ updateFileSelectionMessage();
$('label.button').hide();
// Add buttons for selecting/deselecting files
$('')
@@ -157,6 +164,24 @@ $(document).ready(function() {
};
});
+/**
+ * Updates the message displayed to the user based on file selection status
+ */
+function updateFileSelectionMessage() {
+ let numExists = $('#filelist>.ui-fileupload-files .file-exists').length;
+ let totalFiles = Object.keys(rawFileMap).length;
+ console.log('exists: ' + numExists);
+ console.log('rawFileMap len: ' + totalFiles);
+
+ if (totalFiles === numExists) {
+ addMessage('info', 'msgFilesAlreadyExist');
+ } else if (numExists !== 0 && totalFiles > numExists) {
+ addMessage('info', 'msgUploadOnlyCheckedFiles');
+ } else {
+ addMessage('info', 'msgStartUpload');
+ }
+}
+
function updateMaxFiles() {
let maxInput = $('#maxFilesInput');
let maxFiles = parseInt(maxInput.val());
@@ -230,7 +255,7 @@ function initSpanTxt(htmlId, key) {
function formatMessage(key, keyArgs) {
let msg = getLocalizedString(dvLocale, key);
-
+
if(keyArgs && Array.isArray(keyArgs)) {
for (var i = 0; i < keyArgs.length; i++) {
msg = msg.replaceAll('{'+i+'}', keyArgs[i]);
@@ -277,7 +302,17 @@ async function populatePageMetadata(data) {
$('#top').prepend(mdDiv);
}
-async function retrieveDatasetInfo() {
+/**
+ * Refreshes dataset information from Dataverse
+ * @param {boolean} isInitialLoad - Whether this is the initial load or a refresh
+ */
+async function retrieveDatasetInfo(isInitialLoad = true) {
+ isRetrievingDatasetInfo = true;
+ $('#files').prop('disabled', true);
+ if ($('#upload').length > 0) {
+ $('#upload').addClass('disabled').prop('disabled', true);
+ }
+ addMessage('info', 'msgGettingDatasetInfo');
try {
// First, check for dataset locks
const locksResponse = await $.ajax({
@@ -297,6 +332,8 @@ async function retrieveDatasetInfo() {
if (locksResponse.data && locksResponse.data.length > 0 && !isLockedInReview) {
addMessage('error', 'msgDatasetLocked');
disableUploadFunctionality();
+ addRefreshButton();
+ isRetrievingDatasetInfo = false;
return;
}
@@ -313,6 +350,8 @@ async function retrieveDatasetInfo() {
if (!permissionsResponse.data || !permissionsResponse.data.canPublishDataset) {
addMessage('error', 'msgDatasetLockedInReview');
disableUploadFunctionality();
+ addRefreshButton();
+ isRetrievingDatasetInfo = false;
return;
}
}
@@ -329,8 +368,9 @@ async function retrieveDatasetInfo() {
console.log(datasetResponse);
let data = datasetResponse.data;
console.log(data);
-
- populatePageMetadata(data);
+ if(isInitialLoad) {
+ populatePageMetadata(data);
+ }
if (data.files !== null) {
existingFiles = {};
convertedFileNameMap = {};
@@ -348,7 +388,7 @@ async function retrieveDatasetInfo() {
if ('directoryLabel' in entry) {
filepath = entry.directoryLabel + '/' + filepath;
}
- console.log("Storing: " + filepath);
+ //console.log("Storing: " + filepath);
existingFiles[filepath] = df.checksum;
if (convertedFile) {
convertedFileNameMap[removeExtension(filepath)] = filepath;
@@ -356,13 +396,155 @@ async function retrieveDatasetInfo() {
}
}
$('#files').prop('disabled', false);
- addMessage('info', 'msgReadyToStart');
+ isRetrievingDatasetInfo = false;
+ if(isInitialLoad || $('#filelist>.ui-fileupload-files').length === 0) {
+ addMessage('info', 'msgReadyToStart');
+ // Add the refresh button after initial load
+ addRefreshButton();
+ }
+
+ // Refresh listed files in case any were selected before this call finished
+ if ($('#filelist>.ui-fileupload-files .ui-fileupload-row').length > 0) {
+ refreshListedFileStates();
+ }
} catch (error) {
+ isRetrievingDatasetInfo = false;
+ $('#files').prop('disabled', false);
console.log('Error:', error);
addMessage('error', 'msgErrorRetrievingDataset');
+ }
+}
+
+/**
+ * Adds a refresh button to the UI
+ */
+function addRefreshButton() {
+ if ($('#refreshDataset').length === 0) {
+ $('#button-container .button-right').append(
+ $('')
+ .prop('id', 'refreshDataset')
+ .text(getLocalizedString(dvLocale, 'refreshDataset'))
+ .addClass('button secondary')
+ .click(async function() {
+ if (startUploadsHasBeenCalled) {
+ //Shouldn't happen - button should be disabled, but let's be conservative
+ alert("Can't refresh while upload is in progress");
+ } else {
+ try {
+ // Call retrieveDatasetInfo with isInitialLoad=false to indicate this is a refresh
+ await retrieveDatasetInfo(false);
+
+ // Clear any previous upload information
+ resetUploadState();
+
+ // Only manipulate files if the file list exists and has items
+ if ($('#filelist>.ui-fileupload-files').length > 0 &&
+ $('#filelist>.ui-fileupload-files .ui-fileupload-row').length > 0) {
+ removeCloseButton();
+
+ refreshListedFileStates();
+ // Clear progress bars from previous uploads
+ $('.ui-fileupload-progress progress').remove();
+ // Reset any error messages or states
+ $('.ui-fileupload-error').remove();
+ addUploadButton();
+ }
+ } catch (error) {
+ console.error('Error refreshing dataset:', error);
+ addMessage('error', 'msgErrorRefreshingDataset');
+ }
+ }
+ }));
+ } else {
+ $('#refreshDataset').removeClass('disabled').prop('disabled', false);
+ }
+}
+
+/**
+ * Removes the refresh button from the UI
+ */
+function removeRefreshButton() {
+ $('#refreshDataset').remove();
+}
+
+function sanitizeUploadPath(file, origPath) {
+ let path = origPath.substring(0, origPath.length - file.name.length);
+ path = path.replace(/[^\w\-\.\\\/ ]+/g, '_');
+ return path.concat(file.name.replace(/[:<>;#/"*|?\\]/g, '_'));
+}
+
+function refreshListedFileStates() {
+ $('#filelist>.ui-fileupload-files .ui-fileupload-row').each(function() {
+ let row = $(this);
+ let origPath = row.find('.ui-fileupload-filename').text();
+ let file = rawFileMap[origPath];
+
+ if (!file) {
+ row.find('input[type="checkbox"]').prop('checked', false);
+ return;
+ }
+
+ let path = sanitizeUploadPath(file, origPath);
+ let fileExists = (path in existingFiles) || (removeExtension(path) in convertedFileNameMap);
+
+ row.toggleClass('file-exists', fileExists);
+ row.find('input[type="checkbox"]').prop('checked', !fileExists);
+ });
+
+ selectMaxNewFiles();
+ updateFileSelectionMessage();
+}
+
+/**
+ * Adds a close button to the UI
+ */
+function addCloseButton() {
+ if ($('#closeWebloader').length === 0) {
+ // Add close button
+ $('#button-container .button-left').append($('')
+ .prop('id', 'closeWebloader')
+ .text(getLocalizedString(dvLocale, 'closeWindow'))
+ .addClass('button secondary')
+ .click(function() {
+ window.close();
+ }));
+ }
+}
+
+/**
+ * Removes the close button from the UI
+ */
+function removeCloseButton() {
+ $('#closeWebloader').remove();
+}
+
+/**
+ * Adds an upload button to the UI
+ */
+
+function addUploadButton() {
+ if ($('#upload').length === 0 && !startUploadsHasBeenCalled) {
+ $('#button-container .button-left').append($('')
+ .prop('id', 'upload')
+ .text(getLocalizedString(dvLocale, 'startUpload'))
+ .addClass('button')
+ .click(startUploads));
+ }
+
+ if (isRetrievingDatasetInfo) {
+ $('#upload').addClass('disabled').prop('disabled', true);
+ } else {
+ $('#upload').removeClass('disabled').prop('disabled', false);
}
}
+/**
+ * Removes the upload button from the UI
+ */
+function removeUploadButton() {
+ $('#upload').remove();
+}
+
function disableUploadFunctionality() {
$('#files').prop('disabled', true);
$('.file-selection-buttons').hide();
@@ -460,6 +642,70 @@ async function cancelDatasetEdit() {
var inDataverseCall = false;
+
+const uploadUrlRateWindowMs = 60 * 1000;
+const uploadUrlMaxRetries = 5;
+const uploadUrlBaseRetryDelayMs = 2000;
+const uploadUrlMaxRetryDelayMs = 60 * 1000;
+var uploadUrlRequestTimestamps = [];
+var uploadUrlCooldownUntil = 0;
+var uploadUrlInterRequestDelayMs = 0;
+var lastUploadUrlRequestTimestamp = 0;
+
+function pruneUploadUrlRequestTimestamps(now = Date.now()) {
+ uploadUrlRequestTimestamps = uploadUrlRequestTimestamps.filter(
+ timestamp => now - timestamp < uploadUrlRateWindowMs
+ );
+}
+
+function getUploadUrlAverageRatePerMinute() {
+ pruneUploadUrlRequestTimestamps();
+ console.log('Upload URL request average rate over last minute: ' + uploadUrlRequestTimestamps.length + '/minute');
+ return uploadUrlRequestTimestamps.length;
+}
+
+function recordUploadUrlRequest() {
+ let now = Date.now();
+ uploadUrlRequestTimestamps.push(now);
+ lastUploadUrlRequestTimestamp = now;
+}
+
+function getRetryAfterDelayMs(jqXHR) {
+ let retryAfter = jqXHR && jqXHR.getResponseHeader ? jqXHR.getResponseHeader('Retry-After') : null;
+
+ if (!retryAfter) {
+ return null;
+ }
+
+ let retryAfterSeconds = parseInt(retryAfter, 10);
+ if (!Number.isNaN(retryAfterSeconds)) {
+ return retryAfterSeconds * 1000;
+ }
+
+ let retryAfterDate = Date.parse(retryAfter);
+ if (!Number.isNaN(retryAfterDate)) {
+ return Math.max(0, retryAfterDate - Date.now());
+ }
+
+ return null;
+}
+
+async function waitForUploadUrlCooldown() {
+ let now = Date.now();
+ let waitMs = Math.max(
+ uploadUrlCooldownUntil - now,
+ (lastUploadUrlRequestTimestamp + uploadUrlInterRequestDelayMs) - now
+ );
+ if (waitMs > 0) {
+ console.log('Waiting ' + waitMs + 'ms before requesting another upload URL (cooldown or rate limiting)');
+ await sleep(waitMs);
+ }
+}
+
+function updateUploadUrlCooldown(delayMs) {
+ uploadUrlCooldownUntil = Math.max(uploadUrlCooldownUntil, Date.now() + delayMs);
+}
+
var fileUpload = class fileUploadClass {
constructor(file) {
this.file = file;
@@ -479,7 +725,10 @@ var fileUpload = class fileUploadClass {
this.requestDirectUploadUrls();
}
- async requestDirectUploadUrls() {
+ async requestDirectUploadUrls(retryCount = 0) {
+ await waitForUploadUrlCooldown();
+ recordUploadUrlRequest();
+
$.ajax({
url: siteUrl + '/api/datasets/:persistentId/uploadurls?persistentId=' + datasetPid + '&size=' + this.file.size,
headers: { "X-Dataverse-key": apiKey },
@@ -489,9 +738,9 @@ var fileUpload = class fileUploadClass {
dataType: "json",
processData: false,
success: function(body, statusText, jqXHR) {
- console.log(body);
+ //console.log(body);
let data = body.data;
- console.log(data);
+ //console.log(data);
this.storageId = data.storageIdentifier;
delete data.storageIdentifier;
this.urls = data;
@@ -499,18 +748,53 @@ var fileUpload = class fileUploadClass {
this.doUpload();
console.log(JSON.stringify(data));
},
- error: function(jqXHR, textStatus, errorThrown) {
+ error: async function(jqXHR, textStatus, errorThrown) {
console.log('Failure: ' + jqXHR.status);
console.log('Failure: ' + errorThrown);
- uploadFailure(jqXHR, this.file);
+
+ if (jqXHR.status === 429 && retryCount < uploadUrlMaxRetries && directUploadEnabled) {
+ let retryAfterDelayMs = getRetryAfterDelayMs(jqXHR);
+
+ // Recovery wait time for this specific 429 to clear
+ let recoveryDelayMs = Math.max(
+ retryAfterDelayMs || 0,
+ Math.min(uploadUrlBaseRetryDelayMs * Math.pow(2, retryCount), uploadUrlMaxRetryDelayMs)
+ );
+
+ // Increase the persistent inter-request delay to slow down future calls
+ // Adding 50ms to the delay each time a 429 is encountered
+ uploadUrlInterRequestDelayMs +=50;
+
+ updateUploadUrlCooldown(recoveryDelayMs);
+
+ console.log(
+ 'Received 429 while requesting upload URL for ' + this.file.name +
+ '. Recovery wait: ' + recoveryDelayMs + 'ms. Persistent delay increased to: ' +
+ uploadUrlInterRequestDelayMs + 'ms. Attempt ' +
+ (retryCount + 1) + ' of ' + uploadUrlMaxRetries
+ );
+
+ await sleep(recoveryDelayMs);
+ this.requestDirectUploadUrls(retryCount + 1);
+ return;
+ }
+
+ inDataverseCall = false;
+ // If it hasn't been moved to processing yet, do it now so the count is right
+ if (fileList.indexOf(this) !== -1) {
+ fileList.splice(fileList.indexOf(this), 1);
+ curFile = curFile + 1;
+ filesInProgress = filesInProgress - 1;
+ }
+ uploadFailure(jqXHR, this.id);
}
});
}
+
async doUpload() {
this.state = UploadState.UPLOADING;
- var thisFile = curFile;
-
+
//This appears to be the earliest point when the file table has been populated, and, since we don't know how many table entries have had ids added already, we check
var filerows = $('.ui-fileupload-files .ui-fileupload-row');
//Add an id attribute to each entry so we can later match progress and errors with the right entry
@@ -529,8 +813,17 @@ var fileUpload = class fileUploadClass {
//Decrement number queued for processing
console.log('Decrementing fip from :' + filesInProgress);
filesInProgress = filesInProgress - 1;
- fileList.splice(fileList.indexOf(this), 1);
+ if (fileList.indexOf(this) !== -1) {
+ fileList.splice(fileList.indexOf(this), 1);
+ }
+ this.thisFileIndex = curFile;
curFile = curFile + 1;
+
+ this.performActualUpload(fileNode);
+ }
+
+ async performActualUpload(fileNode) {
+ var thisFile = this.thisFileIndex;
const progBar = fileNode.find('.ui-fileupload-progress');
const cancelButton = fileNode.find('.ui-fileupload-cancel');
var cancelled = false;
@@ -540,9 +833,12 @@ var fileUpload = class fileUploadClass {
progBar.html('');
progBar.append($('').attr('class', 'ui-progressbar ui-widget ui-widget-content ui-corner-all'));
if (this.urls.hasOwnProperty("url")) {
+ const uploadHeaders = this.urls.url.toLowerCase().includes("x-amz-tagging")
+ ? { "x-amz-tagging": "dv-state=temp" }
+ : {};
$.ajax({
url: this.urls.url,
- headers: { "x-amz-tagging": "dv-state=temp" },
+ headers: uploadHeaders,
type: 'PUT',
data: this.file,
context: this,
@@ -552,6 +848,8 @@ var fileUpload = class fileUploadClass {
//ToDo - cancelling abandons the file. It is marked as temp so can be cleaned up later, but would be good to remove now (requires either sending a presigned delete URL or adding a callback to delete only a temp file
if (!cancelled) {
this.reportUpload();
+ } else {
+ directUploadFinished();
}
},
error: function(jqXHR, textStatus, errorThrown) {
@@ -674,17 +972,24 @@ var fileUpload = class fileUploadClass {
if (!allGood) {
if (this.alreadyRetried) {
console.log('Error after retrying ' + this.file.name);
- uploadFailure(null, this.file.name);
+ uploadFailure(null, this.id);
this.cancelMPUpload();
} else {
this.alreadyRetried = true;
- this.doUpload();
+ // Don't re-call doUpload() as it manages queue/counts which are already done for this file
+ this.retryMultipartUpload();
}
} else {
this.finishMPUpload();
}
}
+ retryMultipartUpload() {
+ var files = $('.ui-fileupload-files');
+ var fileNode = files.find("[upid='file_" + this.id + "']");
+ this.performActualUpload(fileNode);
+ }
+
reportUpload() {
this.state = UploadState.UPLOADED;
console.log('S3 Upload complete for ' + this.file.name + ' : ' + this.storageId);
@@ -772,7 +1077,7 @@ function queueFileForDirectUpload(file) {
var fUpload = new fileUpload(file);
let send = true;
let origPath = file.webkitRelativePath.substring(file.webkitRelativePath.indexOf('/') + 1);
-
+
//Remove filename part
let path =origPath.substring(0, origPath.length - file.name.length);
let badPath = (path.match(/^[\w\-\.\\\/ ]*$/)===null);
@@ -785,20 +1090,16 @@ function queueFileForDirectUpload(file) {
}
//Re-Add filename, munge filename if needed
path=path.concat(file.name.replace(/[:<>;#/"*|?\\]/g,'_'));
-
+
//Now check versus existing files
- if (path in existingFiles) {
- send = false;
- } else if (removeExtension(path) in convertedFileNameMap) {
+ if ((path in existingFiles) || (removeExtension(path) in convertedFileNameMap)) {
send = false;
}
rawFileMap[origPath] = file;
- let i = rawFileMap.length;
+ let i = Object.keys(rawFileMap).length;
//startUploads();
if (send) {
- if ($('#upload').length === 0) {
- $('').prop('id', 'upload').text(getLocalizedString(dvLocale, 'startUpload')).addClass('button').click(startUploads).appendTo($('#top'));
- }
+ addUploadButton();
}
let fileBlock = $('#filelist>.ui-fileupload-files');
if (fileBlock.length === 0) {
@@ -823,7 +1124,6 @@ function queueFileForDirectUpload(file) {
row.append(fnameElement)
.append($('').text(file.size)).append($('').addClass('ui-fileupload-progress'))
.append($('').addClass('ui-fileupload-cancel'));
- console.log('adding click handler for file_' + fUpload.id);
$('#file_' + fUpload.id).click(toggleUpload);
}
@@ -840,6 +1140,7 @@ function selectMaxNewFiles() {
}
});
toggleUpload();
+ updateFileSelectionMessage();
}
// Function to deselect all files
@@ -866,27 +1167,32 @@ function toggleUpload() {
addMessage('warn', 'msgMaxFilesExceeded');
$('#upload').addClass('disabled').prop('disabled', true);
} else {
- if ($('#upload').length === 0 && !startUploadsHasBeenCalled) {
- $('').prop('id', 'upload')
- .text(getLocalizedString(dvLocale, 'startUpload'))
- .addClass('button')
- .click(startUploads)
- .insertBefore($('#messages'));
- } else {
- $('#upload').removeClass('disabled').prop('disabled', false);
- }
+ addUploadButton();
addMessage('info', 'msgStartUpload');
}
} else {
- $('#upload').addClass('disabled').prop('disabled', true);
- addMessage('info', 'msgNoFile');
+ if($('.ui-fileupload-row').length > 0) {
+ $('#upload').addClass('disabled').prop('disabled', true);
+ addMessage('info', 'msgNoFile');
+ }
}
}
function startUploads() {
- startUploadsHasBeenCalled = true;
- $('#top button').remove();
let checked = $('#filelist>.ui-fileupload-files input:checked');
+ if (checked.length === 0) {
+ addMessage('info', 'msgNoFile');
+ return;
+ }
+ startUploadsHasBeenCalled = true;
+ resetUploadState();
+ $('#refreshDataset').addClass('disabled').prop('disabled', true);
+ $('#upload').remove();
+ // Also disable directory selection while uploading
+ $('#files').prop('disabled', true);
+ $('label[for="files"]').addClass('disabled');
+ // Add a message indicating uploads are in progress
+ addMessage('info', 'msgUploadsInProgress');
checked.each(function() {
console.log('Name ' + $(this).siblings('.ui-fileupload-filename').text());
let file = rawFileMap[$(this).siblings('.ui-fileupload-filename').text()];
@@ -991,6 +1297,13 @@ async function directUploadFinished() {
if (total === numDone) {
// $('button[id$="AllUploadsFinished"]').trigger('click');
console.log("All files in S3");
+ if (toRegisterFileList.length === 0) {
+ console.log("No files were successfully uploaded to S3.");
+ startUploadsHasBeenCalled = false;
+ addRefreshButton();
+ addMessage('info', 'msgNoFile');
+ return;
+ }
addMessage('info', 'msgUploadCompleteRegistering');
let body = [];
for (let i = 0; i < toRegisterFileList.length; i++) {
@@ -1001,7 +1314,7 @@ async function directUploadFinished() {
//Remove bad file name chars
entry.fileName = fup.file.name.replace(/[:<>;#/"*|?\\]/g,'_');
let path = fup.file.webkitRelativePath;
- console.log(path);
+ //console.log(path);
path = path.substring(path.indexOf('/'), path.lastIndexOf('/'));
//Remove bad path chars
path = path.replace(/[^\w\-\.\\\/ ]+/g,'_');
@@ -1020,6 +1333,13 @@ async function directUploadFinished() {
console.log(JSON.stringify(body));
let fd = new FormData();
fd.append('jsonData', JSON.stringify(body));
+
+ // Add a loading indicator before making the AJAX call
+ $('#messages').append($('').attr('id', 'loading-indicator').addClass('pending')
+ .html('' + formatMessage('msgRegisteringFiles')));
+ // Remove the refresh button when uploads start
+ removeRefreshButton();
+
$.ajax({
url: siteUrl + '/api/datasets/:persistentId/addFiles?persistentId=' + datasetPid,
headers: { "X-Dataverse-key": apiKey },
@@ -1031,6 +1351,9 @@ async function directUploadFinished() {
data: fd,
processData: false,
success: function(body, statusText, jqXHR) {
+ // Remove the loading indicator
+ $('#loading-indicator').remove();
+
var datasetUrl = siteUrl + '/dataset.xhtml?persistentId=' + datasetPid + '&version=DRAFT';
console.log("All files sent to " + datasetUrl);
if(draftExists) {
@@ -1038,12 +1361,25 @@ async function directUploadFinished() {
} else {
addMessage('success', 'msgUploadCompleteNewDraft', datasetUrl);
}
+ startUploadsHasBeenCalled = false;
+ resetUploadState();
+ addCloseButton();
+ addRefreshButton();
},
error: function(jqXHR, textStatus, errorThrown) {
+ // Remove the loading indicator
+ $('#loading-indicator').remove();
+
console.log('Failure: ' + jqXHR.status);
console.log('Failure: ' + errorThrown);
- addMessage("failure", "msgUploadToDataverseFailed", "Status: " + jqXHR.status + ", Error: " + errorThrown);
- //uploadFailure(jqXHR, thisFile);
+ if (jqXHR.status === 504 || textStatus === 'timeout') {
+ addMessage("error", "msgUploadToDataverseTimeout");
+ } else {
+ addMessage("error", "msgUploadToDataverseFailed", "Status: " + jqXHR.status + ", Error: " + errorThrown);
+ }
+ startUploadsHasBeenCalled = false;
+ addCloseButton();
+ addRefreshButton();
}
});
//stop observer when we're done
@@ -1063,6 +1399,12 @@ async function directUploadFinished() {
}
}
}
+ } else {
+ // Direct upload was disabled (e.g., cancelled)
+ if (total === numDone) {
+ startUploadsHasBeenCalled = false;
+ addRefreshButton();
+ }
}
await sleep(delay);
inDataverseCall = false;
@@ -1091,14 +1433,14 @@ async function uploadFailure(jqXHR, upid, filename) {
// only one element and that element includes a description of the row involved, including it's upid.
var name = null;
- var id = null;
+ var id = upid;
if (jqXHR === null) {
status = 1; //made up
statusText = 'Aborting';
+ name = filename;
} else if ((typeof jqXHR !== 'undefined')) {
status = jqXHR.status;
statusText = jqXHR.statusText;
- id = upid;
name = filename;
} else {
try {
@@ -1129,7 +1471,8 @@ async function uploadFailure(jqXHR, upid, filename) {
node.appendChild(textnode);
//Add the error message to the correct row
for (let i = 0; i < rows.length; i++) {
- if (rows[i].getAttribute('upid') === id) {
+ let rowUpid = rows[i].getAttribute('upid');
+ if (rowUpid === id || rowUpid === 'file_' + id) {
//Remove any existing error message/only show last error (have seen two error 0 from one network disconnect)
var err = rows[i].getElementsByClassName('ui-fileupload-error');
if (err.length !== 0) {
diff --git a/src/js/lang.js b/src/js/lang.js
index 2d52694..a342d60 100644
--- a/src/js/lang.js
+++ b/src/js/lang.js
@@ -7,17 +7,22 @@ const translations = {
sponsor: ", development sponsored by UiT/DataverseNO",
startUpload: "Start Uploads",
uploadingTo: "Uploading to ",
+ refreshDataset: "Refresh Dataset Info",
+ closeWindow: "Close Window",
msgGettingDatasetInfo: "Getting Dataset Information...",
msgFilesAlreadyExist: "All files already exist in dataset. There's nothing to upload.",
msgUploadOnlyCheckedFiles: "Some files already exist in dataset. Only checked files will be uploaded.",
msgReadyToStart: "Ready. Click Select a Directory. Review the selected files. Start Uploads. (Note - selection dialog will not show files, but they will be shown afterwards on the page.) ",
msgStartUpload: "Checked files will be uploaded.",
+ msgUploadsInProgress: "Uploads in progress...",
msgNoFile: "No files to upload. Check some files, or refresh to start over.",
msgUploadCompleteRegistering: "Uploads to S3 complete. Now registering all files with the dataset. This may take some time for large numbers of files.",
+ msgRegisteringFiles: "Waiting for response from Dataverse ...",
msgUploadComplete: "Upload complete, all files in dataset. Close this window and refresh your dataset page to see the uploaded files.",
msgUploadCompleteNewDraft: "Upload complete, all files in dataset. Close this window, refresh your dataset page, and navigate to the new DRAFT version to see the uploaded files, or click here to open the new draft version in this tab.",
msgRequiredPathOrFileNameChange: "The highlighted path/file(s) below contain one or more disallowed characters (paths can only contain a-Z, 0-9, '_', '-', '.', '\', '/' and ' ', and filenames cannot contain any of '/;:|?*#' ). Disallowed characters will be replaced by an underscore ('_') if the file(s) are uploaded.",
msgUploadToDataverseFailed: "Final submission of file metadata failed, files will not be added to the dataset. Please contact the repository for help and include the error information below.",
+ msgUploadToDataverseTimeout: "Dataverse took too long to respond. If you uploaded a large number of files, Dataverse may take significant time to process the upload. Please return to the dataset page and watch for changes before attempting a new upload. (For 1000+ files, this could take an hour or more).",
msgMaxFiles: "Max files to upload",
msgSelectAllNew: "Select up to max files not in dataset",
msgDeselectAll: "Deselect all",
@@ -34,17 +39,22 @@ const translations = {
sponsor: ", développement sponsorisé par UiT/DataverseNO",
startUpload: "Démarrer les envois",
uploadingTo: "Envoi vers ",
+ refreshDataset: "Rafraîchir",
+ closeWindow: "Fermer la fenêtre",
msgGettingDatasetInfo: "Récupération des informations du jeu de données...",
msgFilesAlreadyExist: "Tous les fichiers existent déjà dans le jeu de données. Il n'y a rien à envoyer.",
msgUploadOnlyCheckedFiles: "Certains fichiers existent déjà dans le jeu de données. Seuls les fichiers cochés seront envoyés.",
msgReadyToStart: "Prêt. Cliquez sur Sélectionner un répertoire. Passez en revue les fichiers sélectionnés. Démarrez les envois. (Remarque : la boîte de dialogue de sélection ne montrera pas les fichiers, mais ils seront affichés ensuite sur la page.) ",
msgStartUpload: "Les fichiers cochés seront envoyés.",
+ msgUploadsInProgress: "Transferts en cours...",
msgNoFile: "Aucun fichier à envoyer. Cochez certains fichiers ou rafraîchissez la page pour recommencer.",
msgUploadCompleteRegistering: "Envois vers S3 terminés. Enregistrement de tous les fichiers en cours dans le jeu de données. Cela peut prendre du temps pour un grand nombre de fichiers.",
+ msgRegisteringFiles: "En attente de réponse de Dataverse ...",
msgUploadComplete: "Envoi terminé, tous les fichiers sont dans le jeu de données. Fermez cette fenêtre et rafraîchissez la page de votre jeu de données pour voir les fichiers envoyés.",
msgUploadCompleteNewDraft: "Envoi terminé, tous les fichiers sont dans le jeu de données. Fermez cette fenêtre, rafraîchissez la page de votre jeu de données, et accédez à la nouvelle version DRAFT pour voir les fichiers envoyés, ou cliquez ici pour ouvrir la nouvelle version brouillon dans cet onglet.",
msgRequiredPathOrFileNameChange: "Le(s) chemin(s) en surbrillance ci-dessous contiennent un ou plusieurs caractères non autorisés (les chemins ne peuvent contenir que a-Z, 0-9, '_', '-', '.', '\', '/' et ' ', et les noms de fichiers ne peuvent contenir aucun des éléments '/;:|?*#' ). Les caractères non autorisés seront remplacés par un trait de soulignement (« _ ») si le(s) fichier(s) sont téléchargés.",
msgUploadToDataverseFailed: "La soumission finale des métadonnées du fichier a échoué ; les fichiers ne seront pas ajoutés au jeu de données. Veuillez contacter le dépôt pour obtenir de l'aide et inclure les informations d'erreur ci-dessous.",
+ msgUploadToDataverseTimeout: "Dataverse a mis trop de temps à répondre. Si vous avez téléchargé un grand nombre de fichiers, Dataverse peut prendre un temps considérable pour traiter le téléchargement. Veuillez retourner sur la page du jeu de données et surveiller les changements avant de tenter un nouveau téléchargement. (Pour 1000+ fichiers, cela pourrait prendre une heure ou plus).",
msgMaxFiles: "Nombre maximum de fichiers à télécharger",
msgSelectAllNew: "Sélectionner jusqu'au maximum de fichiers non présents dans l'ensemble de données",
msgDeselectAll: "Tout désélectionner",