diff --git a/client/galaxy/scripts/apps/panels/tool-panel.js b/client/galaxy/scripts/apps/panels/tool-panel.js
index 0a2cead12b9..c7ba4027c95 100644
--- a/client/galaxy/scripts/apps/panels/tool-panel.js
+++ b/client/galaxy/scripts/apps/panels/tool-panel.js
@@ -2,6 +2,7 @@ import Tools from "mvc/tool/tools";
import Upload from "mvc/upload/upload-view";
import _l from "utils/localization";
import ToolForm from "mvc/tool/tool-form-composite";
+import _ from "libs/underscore";
var ToolPanel = Backbone.View.extend({
initialize: function(page, options) {
@@ -28,7 +29,8 @@ var ToolPanel = Backbone.View.extend({
// add upload modal
this.upload_button = new Upload({
- nginx_upload_path: config.nginx_upload_path,
+ upload_path: config.nginx_upload_path || `${Galaxy.root}api/tools`,
+ chunk_upload_size: config.chunk_upload_size,
ftp_upload_site: config.ftp_upload_site,
default_genome: config.default_genome,
default_extension: config.default_extension
@@ -74,70 +76,54 @@ var ToolPanel = Backbone.View.extend({
/** build a link to one tool */
_templateTool: function(tool) {
- return [
- '
"
- ].join("");
+ return ``;
},
/** build a link to 'All Workflows' */
_templateAllWorkflow: function(tool) {
- return [
- '"
- ].join("");
+ return ``;
},
/** build links to workflows in toolpanel */
_templateWorkflowLink: function(wf) {
- return [
- '"
- ].join("");
+ return ``;
},
/** override to include inital menu dom and workflow section */
_template: function() {
- return [
- '"
- ].join("");
+ return ``;
},
toString: function() {
diff --git a/client/galaxy/scripts/mvc/grid/grid-template.js b/client/galaxy/scripts/mvc/grid/grid-template.js
index 0bb8c2ba7d6..63e407e16ac 100644
--- a/client/galaxy/scripts/mvc/grid/grid-template.js
+++ b/client/galaxy/scripts/mvc/grid/grid-template.js
@@ -93,7 +93,9 @@ export default {
if (column.visible) {
tmpl += ``;
if (column.sortable) {
- tmpl += `${column.label}`;
+ tmpl += `${
+ column.label
+ }`;
} else {
tmpl += column.label;
}
diff --git a/client/galaxy/scripts/mvc/upload/collection/collection-view.js b/client/galaxy/scripts/mvc/upload/collection/collection-view.js
index 817bcaef646..b8744dcbe69 100644
--- a/client/galaxy/scripts/mvc/upload/collection/collection-view.js
+++ b/client/galaxy/scripts/mvc/upload/collection/collection-view.js
@@ -116,7 +116,7 @@ export default Backbone.View.extend({
// file upload
this.uploadbox = this.$(".upload-box").uploadbox({
- url: this.app.options.nginx_upload_path,
+ url: this.app.options.upload_path,
announce: function(index, file) {
self._eventAnnounce(index, file);
},
diff --git a/client/galaxy/scripts/mvc/upload/composite/composite-view.js b/client/galaxy/scripts/mvc/upload/composite/composite-view.js
index 928de5382ab..830dce6cb5f 100644
--- a/client/galaxy/scripts/mvc/upload/composite/composite-view.js
+++ b/client/galaxy/scripts/mvc/upload/composite/composite-view.js
@@ -133,7 +133,7 @@ export default Backbone.View.extend({
});
});
$.uploadpost({
- url: this.app.options.nginx_upload_path,
+ url: this.app.options.upload_path,
data: this.app.toData(this.collection.filter()),
success: function(message) {
self._eventSuccess(message);
diff --git a/client/galaxy/scripts/mvc/upload/default/default-row.js b/client/galaxy/scripts/mvc/upload/default/default-row.js
index 4f6d5c5fe3b..054c2cba775 100644
--- a/client/galaxy/scripts/mvc/upload/default/default-row.js
+++ b/client/galaxy/scripts/mvc/upload/default/default-row.js
@@ -11,6 +11,7 @@ export default Backbone.View.extend({
init: "upload-icon-button fa fa-trash-o",
queued: "upload-icon fa fa-spinner fa-spin",
running: "upload-icon fa fa-spinner fa-spin",
+ warning: "upload-icon fa fa-spinner fa-spin",
success: "upload-icon-button fa fa-check",
error: "upload-icon-button fa fa-exclamation-triangle"
},
@@ -183,7 +184,7 @@ export default Backbone.View.extend({
_refreshInfo: function() {
var info = this.model.get("info");
if (info) {
- this.$info_text.html(`Failed: ${info}`).show();
+ this.$info_text.html(`Warning: ${info}`).show();
} else {
this.$info_text.hide();
}
@@ -213,13 +214,17 @@ export default Backbone.View.extend({
this.select_genome.disable();
this.select_extension.disable();
}
+ this.$info_progress.show();
+ this.$el.removeClass().addClass("upload-row");
if (status == "success") {
this.$el.addClass("success");
this.$percentage.html("100%");
- }
- if (status == "error") {
+ } else if (status == "error") {
this.$el.addClass("danger");
this.$info_progress.hide();
+ } else if (status == "warning") {
+ this.$el.addClass("warning");
+ this.$info_progress.hide();
}
},
diff --git a/client/galaxy/scripts/mvc/upload/default/default-view.js b/client/galaxy/scripts/mvc/upload/default/default-view.js
index 741b44a55ab..ede97d8b757 100644
--- a/client/galaxy/scripts/mvc/upload/default/default-view.js
+++ b/client/galaxy/scripts/mvc/upload/default/default-view.js
@@ -104,7 +104,7 @@ export default Backbone.View.extend({
// file upload
this.uploadbox = this.$uploadbox.uploadbox({
- url: this.app.options.nginx_upload_path,
+ url: this.app.options.upload_path,
announce: function(index, file) {
self._eventAnnounce(index, file);
},
@@ -120,6 +120,9 @@ export default Backbone.View.extend({
error: function(index, message) {
self._eventError(index, message);
},
+ warning: function(index, message) {
+ self._eventWarning(index, message);
+ },
complete: function() {
self._eventComplete();
},
@@ -247,7 +250,7 @@ export default Backbone.View.extend({
/** Progress */
_eventProgress: function(index, percentage) {
var it = this.collection.get(index);
- it.set("percentage", percentage);
+ it.set({ percentage: percentage, status: "running", info: "" });
this.ui_button.model.set("percentage", this._uploadPercentage(percentage, it.get("file_size")));
},
@@ -263,6 +266,12 @@ export default Backbone.View.extend({
Galaxy.currHistoryPanel.refreshContents();
},
+ /** Warning */
+ _eventWarning: function(index, message) {
+ var it = this.collection.get(index);
+ it.set({ status: "warning", info: message });
+ },
+
/** Error */
_eventError: function(index, message) {
var it = this.collection.get(index);
@@ -363,7 +372,10 @@ export default Backbone.View.extend({
this._uploadFtp();
// queue remaining files
- this.uploadbox.start();
+ this.uploadbox.start({
+ id: Galaxy.user.id,
+ chunk_upload_size: this.app.options.chunk_upload_size
+ });
this.render();
}
},
@@ -430,7 +442,7 @@ export default Backbone.View.extend({
if (list.length > 0) {
$.uploadpost({
data: this.app.toData(list),
- url: this.app.options.nginx_upload_path,
+ url: this.app.options.upload_path,
success: function(message) {
_.each(list, model => {
self._eventSuccess(model.id);
@@ -452,37 +464,37 @@ export default Backbone.View.extend({
/** Template */
_template: function() {
- return (
- '' +
- ' ' +
- ' ' +
- "" +
- ' ' +
- ' Drop files here ' +
- ' ' +
- "" +
- "" +
- "| Name | " +
- "Size | " +
- "Type | " +
- "Genome | " +
- "Settings | " +
- "Status | " +
- " | " +
- " " +
- "" +
- "" +
- " " +
- " " +
- '" +
- ' ' +
- " "
- );
+ return `
+
+
+
+
+
+ Drop files here
+
+
+
+
+ | Name |
+ Size |
+ Type |
+ Genome |
+ Settings |
+ Status |
+ |
+
+
+
+
+
+
+
+ `;
}
});
diff --git a/client/galaxy/scripts/mvc/upload/upload-view.js b/client/galaxy/scripts/mvc/upload/upload-view.js
index 14272498fff..5f4684f53e7 100644
--- a/client/galaxy/scripts/mvc/upload/upload-view.js
+++ b/client/galaxy/scripts/mvc/upload/upload-view.js
@@ -9,7 +9,6 @@ import UploadViewComposite from "mvc/upload/composite/composite-view";
import UploadViewCollection from "mvc/upload/collection/collection-view";
export default Backbone.View.extend({
options: {
- nginx_upload_path: "",
ftp_upload_site: "n/a",
default_genome: "?",
default_extension: "auto",
diff --git a/client/galaxy/scripts/utils/uploadbox.js b/client/galaxy/scripts/utils/uploadbox.js
index bbd2a1c0590..44faa6e7025 100644
--- a/client/galaxy/scripts/utils/uploadbox.js
+++ b/client/galaxy/scripts/utils/uploadbox.js
@@ -6,81 +6,29 @@
jQuery.event.props.push("dataTransfer");
/**
- Posts file data to the API
+ xhr request helper
*/
- $.uploadpost = config => {
- // parse options
+ var _uploadrequest = config => {
var cnf = $.extend(
- {},
{
- data: {},
- success: function() {},
- error: function() {},
- progress: function() {},
- url: null,
- maxfilesize: 2048,
- error_filesize: "File exceeds 2GB. Please use a FTP client.",
error_default: "Please make sure the file is available.",
error_server: "Upload request failed.",
- error_login: "Uploads require you to log in."
+ error_login: "Uploads require you to log in.",
+ error_retry: "Waiting for server to resume..."
},
config
);
-
- // link data
- var data = cnf.data;
-
- // check errors
- if (data.error_message) {
- cnf.error(data.error_message);
- return;
- }
-
- // construct form data
- var form = new FormData();
- for (var key in data.payload) {
- form.append(key, data.payload[key]);
- }
-
- // add files to submission
- var sizes = 0;
- for (var key in data.files) {
- var d = data.files[key];
- form.append(d.name, d.file, d.file.name);
- sizes += d.file.size;
- }
-
- // check file size, unless it's an ftp file
- if (sizes > 1048576 * cnf.maxfilesize) {
- cnf.error(cnf.error_filesize);
- return;
- }
-
- // prepare request
+ console.debug(cnf);
var xhr = new XMLHttpRequest();
xhr.open("POST", cnf.url, true);
- xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Cache-Control", "no-cache");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
-
- // captures state changes
+ xhr.setRequestHeader("Accept", "application/json");
xhr.onreadystatechange = () => {
- // check for request completed, server connection closed
if (xhr.readyState == xhr.DONE) {
- // parse response
- var response = null;
- var extra_info = "";
- if (xhr.responseText) {
- try {
- response = jQuery.parseJSON(xhr.responseText);
- extra_info = response.err_msg;
- } catch (e) {
- response = xhr.responseText;
- extra_info = response;
- }
- }
- // pass any error to the error option
- if (xhr.status < 200 || xhr.status > 299) {
+ if ([502, 0].indexOf(xhr.status) !== -1 && cnf.warning) {
+ cnf.warning(cnf.error_retry);
+ } else if (xhr.status < 200 || xhr.status > 299) {
var text = xhr.statusText;
if (xhr.status == 403) {
text = cnf.error_login;
@@ -89,27 +37,191 @@
} else if (!text) {
text = cnf.error_default;
}
- cnf.error(`${text} (${xhr.status}). ${extra_info}`);
+ cnf.error(`${text} (${xhr.status})`);
} else {
+ var response = null;
+ if (xhr.responseText) {
+ try {
+ response = jQuery.parseJSON(xhr.responseText);
+ } catch (e) {
+ response = xhr.responseText;
+ }
+ }
cnf.success(response);
}
}
};
+ xhr.upload.addEventListener("progress", cnf.progress, false);
+ xhr.send(cnf.data);
+ };
- // prepare upload progress
- xhr.upload.addEventListener(
- "progress",
- e => {
+ /**
+ Posts chunked files to the API.
+ */
+ $.uploadchunk = function(config) {
+ // parse options
+ var cnf = $.extend(
+ {},
+ {
+ data: {},
+ success: () => {},
+ error: () => {},
+ warning: () => {},
+ progress: () => {},
+ attempts: 70000,
+ timeout: 5000,
+ url: null,
+ error_file: "File not provided.",
+ error_attempt: "Maximum number of attempts reached.",
+ error_tool: "Tool submission failed."
+ },
+ config
+ );
+
+ // initial validation
+ var data = cnf.data;
+ if (data.error_message) {
+ cnf.error(data.error_message);
+ return;
+ }
+ var file_data = data.files && data.files[0];
+ if (!file_data) {
+ cnf.error(cnf.error_file);
+ return;
+ }
+ var file = file_data.file;
+ var attempts = cnf.attempts;
+ var session_id = `${cnf.session.id}-${new Date().valueOf()}-${file.size}`;
+ var chunk_size = cnf.session.chunk_upload_size;
+ console.debug(`Starting chunked uploads [size=${chunk_size}].`);
+
+ // chunk processing helper
+ function process(start) {
+ start = start || 0;
+ var slicer = file.mozSlice || file.webkitSlice || file.slice;
+ if (!slicer) {
+ cnf.error("Browser does not support chunked uploads.");
+ return;
+ }
+ var end = Math.min(start + chunk_size, file.size);
+ var size = file.size;
+ console.debug(`Submitting chunk at ${start} bytes...`);
+ var form = new FormData();
+ form.append("session_id", session_id);
+ form.append("session_start", start);
+ form.append("session_chunk", slicer.bind(file)(start, end));
+ _uploadrequest({
+ url: `${Galaxy.root}api/uploads`,
+ data: form,
+ success: upload_response => {
+ var new_start = start + chunk_size;
+ if (new_start < size) {
+ attempts = cnf.attempts;
+ process(new_start);
+ } else {
+ console.debug("Upload completed.");
+ data.payload.inputs = JSON.parse(data.payload.inputs);
+ data.payload.inputs["files_0|file_data"] = {
+ session_id: session_id,
+ name: file.name
+ };
+ data.payload.inputs = JSON.stringify(data.payload.inputs);
+ $.ajax({
+ url: `${Galaxy.root}api/tools`,
+ method: "POST",
+ data: data.payload,
+ success: tool_response => {
+ cnf.success(tool_response);
+ },
+ error: tool_response => {
+ var err_msg =
+ tool_response && tool_response.responseJSON && tool_response.responseJSON.err_msg;
+ cnf.error(err_msg || cnf.error_tool);
+ }
+ });
+ }
+ },
+ warning: upload_response => {
+ if (--attempts > 0) {
+ console.debug("Retrying last chunk...");
+ cnf.warning(upload_response);
+ setTimeout(() => process(start), cnf.timeout);
+ } else {
+ console.debug(cnf.error_attempt);
+ cnf.error(cnf.error_attempt);
+ }
+ },
+ error: upload_response => {
+ console.debug(upload_response);
+ cnf.error(upload_response);
+ },
+ progress: e => {
+ if (e.lengthComputable) {
+ cnf.progress(Math.min(Math.round((start + e.loaded) * 100 / file.size), 100));
+ }
+ }
+ });
+ }
+
+ // initiate processing queue for chunks
+ process();
+ };
+
+ /**
+ Posts multiple files without chunking to the API.
+ */
+ $.uploadpost = function(config) {
+ var cnf = $.extend(
+ {},
+ {
+ data: {},
+ success: () => {},
+ error: () => {},
+ progress: () => {},
+ url: null,
+ maxfilesize: 1048576 * 2048,
+ error_filesize: "File exceeds 2GB. Please use a FTP client."
+ },
+ config
+ );
+ var data = cnf.data;
+ if (data.error_message) {
+ cnf.error(data.error_message);
+ return;
+ }
+
+ // construct form data
+ var form = new FormData();
+ for (let key in data.payload) {
+ form.append(key, data.payload[key]);
+ }
+
+ // add files to submission
+ var sizes = 0;
+ for (let key in data.files) {
+ var d = data.files[key];
+ form.append(d.name, d.file, d.file.name);
+ sizes += d.file.size;
+ }
+
+ // check file size, unless it's an ftp file
+ if (sizes > cnf.maxfilesize) {
+ cnf.error(cnf.error_filesize);
+ return;
+ }
+
+ // submit request
+ _uploadrequest({
+ url: cnf.url,
+ data: form,
+ success: cnf.success,
+ error: cnf.error,
+ progress: e => {
if (e.lengthComputable) {
cnf.progress(Math.round(e.loaded * 100 / e.total));
}
- },
- false
- );
-
- // send request
- Galaxy.emit.debug("uploadbox::uploadpost()", "Posting following data.", cnf);
- xhr.send(form);
+ }
+ });
};
/**
@@ -121,9 +233,9 @@
var opts = $.extend(
{},
{
- ondragover: function() {},
- ondragleave: function() {},
- onchange: function() {},
+ ondragover: () => {},
+ ondragleave: () => {},
+ onchange: () => {},
multiple: false
},
options
@@ -132,7 +244,7 @@
// append hidden upload field
var $input = $(``);
el.append(
- $input.change(function(e) {
+ $input.change(e => {
opts.onchange(e.target.files);
$(this).val("");
})
@@ -157,7 +269,7 @@
// exports
return {
- dialog: function() {
+ dialog: () => {
$input.trigger("click");
}
};
@@ -171,16 +283,17 @@
var opts = $.extend(
{},
{
- dragover: function() {},
- dragleave: function() {},
- announce: function(d) {},
- initialize: function(d) {},
- progress: function(d, m) {},
- success: function(d, m) {},
- error: function(d, m) {
+ dragover: () => {},
+ dragleave: () => {},
+ announce: d => {},
+ initialize: d => {},
+ progress: (d, m) => {},
+ success: (d, m) => {},
+ warning: (d, m) => {},
+ error: (d, m) => {
alert(m);
},
- complete: function() {}
+ complete: () => {}
},
options
);
@@ -188,6 +301,9 @@
// file queue
var queue = {};
+ // session options
+ var session = null;
+
// queue index/length counter
var queue_index = 0;
var queue_length = 0;
@@ -199,7 +315,10 @@
// element
var uploadinput = $(this).uploadinput({
multiple: true,
- onchange: function(files) {
+ onchange: files => {
+ _.each(files, file => {
+ file.chunk_mode = true;
+ });
add(files);
},
ondragover: options.ondragover,
@@ -252,7 +371,7 @@
// get an identifier from the queue
var index = -1;
- for (var key in queue) {
+ for (let key in queue) {
index = key;
break;
}
@@ -264,18 +383,32 @@
remove(index);
// create and submit data
- $.uploadpost({
+ var submitter = $.uploadpost;
+ if (
+ file.chunk_mode &&
+ session &&
+ session.id &&
+ session.chunk_upload_size &&
+ session.chunk_upload_size > 0
+ ) {
+ submitter = $.uploadchunk;
+ }
+ submitter({
url: opts.url,
data: opts.initialize(index),
- success: function(message) {
+ session: session,
+ success: message => {
opts.success(index, message);
process();
},
- error: function(message) {
+ warning: message => {
+ opts.warning(index, message);
+ },
+ error: message => {
opts.error(index, message);
process();
},
- progress: function(percentage) {
+ progress: percentage => {
opts.progress(index, percentage);
}
});
@@ -298,7 +431,8 @@
}
// initiate upload process
- function start() {
+ function start(_session) {
+ session = _session;
if (!queue_running) {
queue_running = true;
process();
diff --git a/config/galaxy.yml.sample b/config/galaxy.yml.sample
index 6112840f1d7..6951fc2762e 100644
--- a/config/galaxy.yml.sample
+++ b/config/galaxy.yml.sample
@@ -812,6 +812,11 @@ galaxy:
# documentation for the corresponding nginx configuration.
#nginx_upload_job_files_path: false
+ # Galaxy can upload user files in chunks without using nginx. Enable
+ # the chunk uploader by specifying a chunk size larger than 0. The
+ # chunk size is specified in bytes (default: 100MB).
+ #chunk_upload_size: 104857600
+
# Have Galaxy manage dynamic proxy component for routing requests to
# other services based on Galaxy's session cookie. It will attempt to
# do this by default though you do need to install node+npm and do an
diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst
index adb91a41fff..c77b764a514 100644
--- a/doc/source/admin/galaxy_options.rst
+++ b/doc/source/admin/galaxy_options.rst
@@ -1643,6 +1643,18 @@
:Type: bool
+~~~~~~~~~~~~~~~~~~~~~
+``chunk_upload_size``
+~~~~~~~~~~~~~~~~~~~~~
+
+:Description:
+ Galaxy can upload user files in chunks without using nginx. Enable
+ the chunk uploader by specifying a chunk size larger than 0. The
+ chunk size is specified in bytes (default: 100MB).
+:Default: ``104857600``
+:Type: int
+
+
~~~~~~~~~~~~~~~~~~~~~~~~
``dynamic_proxy_manage``
~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py
index bd0ba551975..26ea0bc56d9 100644
--- a/lib/galaxy/config.py
+++ b/lib/galaxy/config.py
@@ -436,6 +436,7 @@ class Configuration(object):
self.user_library_import_check_permissions = string_as_bool(kwargs.get('user_library_import_check_permissions', False))
self.user_library_import_dir_auto_creation = string_as_bool(kwargs.get('user_library_import_dir_auto_creation', False)) if self.user_library_import_dir else False
# Searching data libraries
+ self.chunk_upload_size = int(kwargs.get('chunk_upload_size', 104857600))
self.ftp_upload_dir = kwargs.get('ftp_upload_dir', None)
self.ftp_upload_dir_identifier = kwargs.get('ftp_upload_dir_identifier', 'email') # attribute on user - email, username, id, etc...
self.ftp_upload_dir_template = kwargs.get('ftp_upload_dir_template', '${ftp_upload_dir}%s${ftp_upload_dir_identifier}' % os.path.sep)
diff --git a/lib/galaxy/managers/configuration.py b/lib/galaxy/managers/configuration.py
index 57f1dc889f6..e0f313c443f 100644
--- a/lib/galaxy/managers/configuration.py
+++ b/lib/galaxy/managers/configuration.py
@@ -73,7 +73,8 @@ class ConfigSerializer(base.ModelSerializer):
# TODO: is there no 'correct' way to get an api url? controller='api', action='tools' is a hack
# at any rate: the following works with path_prefix but is still brittle
# TODO: change this to (more generic) upload_path and incorporate config.nginx_upload_path into building it
- 'nginx_upload_path' : lambda i, k, **c: getattr(i, k, False) or self.url_for('/api/tools'),
+ 'nginx_upload_path' : lambda i, k, **c: getattr(i, k, False),
+ 'chunk_upload_size' : _defaults_to(104857600),
'ftp_upload_dir' : _defaults_to(None),
'ftp_upload_site' : _defaults_to(None),
'version_major' : _defaults_to(None),
diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py
index 719174aba27..888b2206787 100755
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -918,8 +918,7 @@ class Tool(Dictifiable):
# If we have an nginx upload, save the action as a tuple instead of
# a string. The actual action needs to get url_for run to add any
# prefixes, and we want to avoid adding the prefix to the
- # nginx_upload_path. This logic is handled in the tool_form.mako
- # template.
+ # nginx_upload_path.
if self.nginx_upload and self.app.config.nginx_upload_path:
if '?' in unquote_plus(self.action):
raise Exception('URL parameters in a non-default tool action can not be used '
diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py
index e3655429751..7265d48a344 100644
--- a/lib/galaxy/tools/parameters/basic.py
+++ b/lib/galaxy/tools/parameters/basic.py
@@ -521,11 +521,19 @@ class FileToolParameter(ToolParameter):
# Middleware or proxies may encode files in special ways (TODO: this
# should be pluggable)
if type(value) == dict:
- upload_store = trans.app.config.nginx_upload_store
- assert upload_store, "Request appears to have been processed by nginx_upload_module but Galaxy is not configured to recognize it."
- # Check that the file is in the right location
- local_filename = os.path.abspath(value['path'])
- assert local_filename.startswith(upload_store), "Filename provided by nginx (%s) is not in correct directory (%s)." % (local_filename, upload_store)
+ if 'session_id' in value:
+ # handle api upload
+ session_id = value["session_id"]
+ upload_store = trans.app.config.new_file_path
+ if re.match('^[\w-]+$', session_id) is None:
+ raise ValueError("Invald session id format.")
+ local_filename = os.path.abspath(os.path.join(upload_store, session_id))
+ else:
+ # handle nginx upload
+ upload_store = trans.app.config.nginx_upload_store
+ assert upload_store, "Request appears to have been processed by nginx_upload_module but Galaxy is not configured to recognize it."
+ local_filename = os.path.abspath(value['path'])
+ assert local_filename.startswith(upload_store), "Filename provided by nginx (%s) is not in correct directory (%s)." % (local_filename, upload_store)
value = dict(filename=value["name"], local_filename=local_filename)
return value
diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py
index ef23ed0a127..9ce0ea099c2 100644
--- a/lib/galaxy/web/framework/decorators.py
+++ b/lib/galaxy/web/framework/decorators.py
@@ -134,7 +134,7 @@ def expose_api(func, to_json=True, user_required=True):
trans.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
# Perform api_run_as processing, possibly changing identity
- if 'payload' in kwargs and 'run_as' in kwargs['payload']:
+ if 'payload' in kwargs and isinstance(kwargs['payload'], dict) and 'run_as' in kwargs['payload']:
if not trans.user_can_do_run_as():
error_message = 'User does not have permissions to run jobs as another user'
return error
diff --git a/lib/galaxy/webapps/galaxy/api/uploads.py b/lib/galaxy/webapps/galaxy/api/uploads.py
new file mode 100644
index 00000000000..bedf6daf061
--- /dev/null
+++ b/lib/galaxy/webapps/galaxy/api/uploads.py
@@ -0,0 +1,53 @@
+"""
+API operations for uploaded files in storage.
+"""
+import logging
+import os
+import re
+
+from galaxy.exceptions import MessageException, NotImplemented
+from galaxy.web import expose_api_anonymous
+from galaxy.web.base.controller import BaseAPIController
+
+log = logging.getLogger(__name__)
+
+
+class UploadsAPIController(BaseAPIController):
+
+ READ_CHUNK_SIZE = 2 ** 16
+
+ @expose_api_anonymous
+ def index(self, trans, **kwd):
+ raise NotImplemented("Listing uploads is not implemented.")
+
+ @expose_api_anonymous
+ def create(self, trans, payload, **kwd):
+ """
+ POST /api/uploads/
+ """
+ session_id = payload.get("session_id")
+ session_start = payload.get("session_start")
+ session_chunk = payload.get("session_chunk")
+ if re.match('^[\w-]+$', session_id) is None:
+ raise MessageException("Requires a session id.")
+ if session_start is None:
+ raise MessageException("Requires a session start.")
+ if not hasattr(session_chunk, "file"):
+ raise MessageException("Requires a session chunk.")
+ target_file = os.path.join(trans.app.config.new_file_path, session_id)
+ target_size = 0
+ if os.path.exists(target_file):
+ target_size = os.path.getsize(target_file)
+ if session_start != target_size:
+ raise MessageException("Incorrect session start.")
+ chunk_size = os.fstat(session_chunk.file.fileno()).st_size
+ if chunk_size > trans.app.config.chunk_upload_size:
+ raise MessageException("Invalid chunk size.")
+ with open(target_file, "a") as f:
+ while True:
+ read_chunk = session_chunk.file.read(self.READ_CHUNK_SIZE)
+ if not read_chunk:
+ break
+ f.write(read_chunk)
+ session_chunk.file.close()
+ return {"message": "Successful."}
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index eda00a26ee4..f303fbfa92a 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -256,6 +256,7 @@ def populate_api_routes(webapp, app):
webapp.mapper.resource('dataset_collection', 'dataset_collections', path_prefix='/api/')
webapp.mapper.resource('form', 'forms', path_prefix='/api')
webapp.mapper.resource('role', 'roles', path_prefix='/api')
+ webapp.mapper.resource('upload', 'uploads', path_prefix='/api')
webapp.mapper.connect('/api/ftp_files', controller='remote_files')
webapp.mapper.resource('remote_file', 'remote_files', path_prefix='/api')
webapp.mapper.resource('group', 'groups', path_prefix='/api')
diff --git a/lib/galaxy/webapps/galaxy/config_schema.yml b/lib/galaxy/webapps/galaxy/config_schema.yml
index 97819d70d2b..5679ffb01f1 100644
--- a/lib/galaxy/webapps/galaxy/config_schema.yml
+++ b/lib/galaxy/webapps/galaxy/config_schema.yml
@@ -1239,6 +1239,15 @@ mapping:
operations on the remote end. See the Galaxy nginx documentation for the
corresponding nginx configuration.
+ chunk_upload_size:
+ type: int
+ default: 104857600
+ required: false
+ desc: |
+ Galaxy can upload user files in chunks without using nginx. Enable the chunk
+ uploader by specifying a chunk size larger than 0. The chunk size is specified
+ in bytes (default: 100MB).
+
dynamic_proxy_manage:
type: bool
default: true
diff --git a/lib/galaxy/webapps/galaxy/controllers/root.py b/lib/galaxy/webapps/galaxy/controllers/root.py
index ca727ea6889..ec732b7066b 100644
--- a/lib/galaxy/webapps/galaxy/controllers/root.py
+++ b/lib/galaxy/webapps/galaxy/controllers/root.py
@@ -108,7 +108,6 @@ class RootController(controller.JSAppLauncher, UsesAnnotations):
js_options = self._get_js_options(trans)
config = js_options['config']
config.update(self._get_extended_config(trans))
-
return self.template(trans, 'analysis', options=js_options)
@web.expose
diff --git a/test/base/driver_util.py b/test/base/driver_util.py
index 7c31e7df7ee..47e8ee1ae1c 100644
--- a/test/base/driver_util.py
+++ b/test/base/driver_util.py
@@ -191,6 +191,7 @@ def setup_galaxy_config(
api_allow_run_as='test@bx.psu.edu',
auto_configure_logging=logging_config_file is None,
check_migrate_tools=False,
+ chunk_upload_size=100,
conda_auto_init=conda_auto_init,
conda_auto_install=conda_auto_install,
cleanup_job='onsuccess',
|