Index: content-bundles/resources/types.properties =================================================================== --- content-bundles/resources/types.properties (revision 129637) +++ content-bundles/resources/types.properties (working copy) @@ -527,3 +527,7 @@ # SAK-23367 notes.expandfolder=Expand folder(s) of interest and select item(s) to copy to your current site above. + +label.dragDescription=Drag and drop files into the box to upload. If you upload a zip file the contents will be unzipped into a folder. It may take a few minutes to upload large files. +label.dragZoneMessage=Drop files here to upload. +alert.over-site-upload-quota=Your file upload attempt could not be completed because doing so would put you over your site quota. Please check the sum total file size of all items you wish to upload before your next attempt. Index: content-tool/tool/src/java/org/sakaiproject/content/tool/ResourcesHelperAction.java =================================================================== --- content-tool/tool/src/java/org/sakaiproject/content/tool/ResourcesHelperAction.java (revision 129637) +++ content-tool/tool/src/java/org/sakaiproject/content/tool/ResourcesHelperAction.java (working copy) @@ -36,9 +36,11 @@ import java.util.List; import java.util.Properties; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.fileupload.disk.DiskFileItem; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -49,21 +51,18 @@ import org.sakaiproject.cheftool.VelocityPortletPaneledAction; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.component.cover.ServerConfigurationService; -import org.sakaiproject.content.api.ContentCollection; -import org.sakaiproject.content.api.ContentEntity; -import org.sakaiproject.content.api.ContentTypeImageService; -import org.sakaiproject.content.api.MultiFileUploadPipe; -import org.sakaiproject.content.api.ResourceToolAction; -import org.sakaiproject.content.api.ResourceToolActionPipe; -import org.sakaiproject.content.api.ResourceType; -import org.sakaiproject.content.api.ResourceTypeRegistry; +import org.sakaiproject.content.api.*; import org.sakaiproject.content.api.GroupAwareEntity.AccessMode; import org.sakaiproject.content.cover.ContentHostingService; +import org.sakaiproject.content.util.ZipContentUtil; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.entity.api.ResourceProperties; +import org.sakaiproject.entity.api.ResourcePropertiesEdit; +import org.sakaiproject.entity.cover.EntityManager; import org.sakaiproject.event.api.SessionState; import org.sakaiproject.event.cover.NotificationService; import org.sakaiproject.exception.IdUnusedException; +import org.sakaiproject.exception.OverQuotaException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SitePage; @@ -692,6 +691,9 @@ String instr_uploads= rb.getFormattedMessage("instr.uploads", new String[]{ uploadMax}); context.put("instr_uploads", instr_uploads); + Boolean dragAndDrop = ServerConfigurationService.getBoolean("content.upload.dragndrop", false); + context.put("dragAndDrop", dragAndDrop); + // int max_bytes = 1024 * 1024; // try // { @@ -1862,4 +1864,101 @@ super.toolModeDispatch(methodBase, methodExt, req, res); } + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException { + String dragDropFile = request.getHeader("x-file-name"); + + if(dragDropFile != null){ + String uploadMax = ServerConfigurationService.getString("content.upload.max"); + String siteQuota = ServerConfigurationService.getString("siteQuota@org.sakaiproject.content.api.ContentHostingService"); + Long fileSize = Long.parseLong(request.getHeader("content-length")); + if((uploadMax != null && !"".equals(uploadMax)) && (fileSize /1024L / 1024L) > Long.parseLong(uploadMax)){ + addAlert(getState(request), rb.getFormattedMessage("alert.over-per-upload-quota", new Object[]{uploadMax})); + } else if ((siteQuota != null && !"".equals(siteQuota)) && (fileSize /1024L / 1024L) > Long.parseLong(siteQuota)) { + addAlert(getState(request), rb.getFormattedMessage("alert.over-site-upload-quota", new Object[]{siteQuota})); + } else { + doDragDropUpload(request, response); + } + } else{ + super.doPost(request, response); + } + } + + private void doDragDropUpload(HttpServletRequest request, HttpServletResponse response) { + String uploadFileName = request.getHeader("x-file-name"); + SessionState state = getState(request); + ToolSession toolSession = SessionManager.getCurrentToolSession(); + if (uploadFileName != null && !uploadFileName.isEmpty()) { + InputStream is = null; + ContentResourceEdit resource = null; + try { + String resourceGroup = toolSession.getAttribute("resources.request.create_wizard_collection_id").toString(); + String resourceName = getUniqueFileName(uploadFileName, resourceGroup); + resource = ContentHostingService.addResource(resourceGroup + resourceName); + if (resource != null) { + ResourcePropertiesEdit resourceProps = resource.getPropertiesEdit(); + resourceProps.addProperty(ResourcePropertiesEdit.PROP_DISPLAY_NAME, resourceName); + + DiskFileItem uploadFile = (DiskFileItem) request.getAttribute("file"); + if(uploadFile != null){ + resource.setContent(uploadFile.getInputStream()); + ContentHostingService.commitResource(resource, NotificationService.NOTI_NONE); + } + if (uploadFileName.contains(".zip")) { + ZipContentUtil zipContentUtil = new ZipContentUtil(); + zipContentUtil.extractArchive(EntityManager.newReference(resource.getReference())); + } + } + } catch (OverQuotaException e) { + addAlert(state, rb.getString("alert.over-site-upload-quota")); + logger.warn("Drag and drop upload failed: " + e, e); + } catch (Exception e) { + logger.warn("Drag and drop upload failed: " + e, e); + } finally { + try { + if (is != null) { + is.close(); + } + if (uploadFileName.contains(".zip")) { + //remove the zip file after unpacking it + try { + ContentHostingService.removeResource(resource.getId()); + } catch (Exception e) { + logger.warn("Unable to remove zip file: " + e, e); + } + } + } catch (IOException e) { + logger.warn("Caught exception: " + e, e); + } + } + } + } + + private String getUniqueFileName(String uploadFileName, String resourceGroup) throws org.sakaiproject.exception.PermissionException, org.sakaiproject.exception.TypeException { + String resourceId = ""; + boolean isNameUnique = false; + String fileName = uploadFileName; + int attempt = 0; + while (!isNameUnique) { + try { + resourceId = resourceGroup + fileName; + ContentResource tempEdit = ContentHostingService.getResource(resourceId); + if(tempEdit != null){ + attempt++; + StringBuffer fileNameBuffer = new StringBuffer(); + if(attempt > 1){ + fileNameBuffer.append(fileName.substring(0, fileName.lastIndexOf("-"))); + } else{ + fileNameBuffer.append(fileName.substring(0, fileName.lastIndexOf("."))); + } + fileNameBuffer.append("-"); + fileNameBuffer.append(attempt); + fileNameBuffer.append(fileName.substring(fileName.lastIndexOf("."), fileName.length())); + fileName = fileNameBuffer.toString(); + } + } catch (IdUnusedException e) { + isNameUnique = true; + } + } + return fileName; + } } Index: content-tool/tool/src/webapp/vm/resources/sakai_create_uploads.vm =================================================================== --- content-tool/tool/src/webapp/vm/resources/sakai_create_uploads.vm (revision 129637) +++ content-tool/tool/src/webapp/vm/resources/sakai_create_uploads.vm (working copy) @@ -1,4 +1,5 @@ +
$tlang.getString("label.dragDescription")
+" + this.options.dictFallbackText + "
"; + } + + fields = Dropzone.createElement(fieldsString); + if (this.element.tagName !== "FORM") { + form = Dropzone.createElement(""); + form.appendChild(fields); + } else { + this.element.setAttribute("enctype", "multipart/form-data"); + this.element.setAttribute("method", this.options.method); + } + return form != null ? form : fields; + }; + + Dropzone.prototype.getExistingFallback = function() { + var fallback, getFallback, tagName, _i, _len, _ref; + + getFallback = function(elements) { + var el, _i, _len; + + for (_i = 0, _len = elements.length; _i < _len; _i++) { + el = elements[_i]; + if (/(^| )fallback($| )/.test(el.className)) { + return el; + } + } + }; + _ref = ["div", "form"]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + tagName = _ref[_i]; + if (fallback = getFallback(this.element.getElementsByTagName(tagName))) { + return fallback; + } + } + }; + + Dropzone.prototype.setupEventListeners = function() { + var elementListeners, event, listener, _i, _len, _ref, _results; + + _ref = this.listeners; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + elementListeners = _ref[_i]; + _results.push((function() { + var _ref1, _results1; + + _ref1 = elementListeners.events; + _results1 = []; + for (event in _ref1) { + listener = _ref1[event]; + _results1.push(elementListeners.element.addEventListener(event, listener, false)); + } + return _results1; + })()); + } + return _results; + }; + + Dropzone.prototype.removeEventListeners = function() { + var elementListeners, event, listener, _i, _len, _ref, _results; + + _ref = this.listeners; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + elementListeners = _ref[_i]; + _results.push((function() { + var _ref1, _results1; + + _ref1 = elementListeners.events; + _results1 = []; + for (event in _ref1) { + listener = _ref1[event]; + _results1.push(elementListeners.element.removeEventListener(event, listener, false)); + } + return _results1; + })()); + } + return _results; + }; + + Dropzone.prototype.disable = function() { + if (this.clickableElement === this.element) { + this.element.classList.remove("dz-clickable"); + } + this.removeEventListeners(); + this.filesProcessing = []; + return this.filesQueue = []; + }; + + Dropzone.prototype.enable = function() { + if (this.clickableElement === this.element) { + this.element.classList.add("dz-clickable"); + } + return this.setupEventListeners(); + }; + + Dropzone.prototype.filesize = function(size) { + var string; + + if (size >= 100000000000) { + size = size / 100000000000; + string = "TB"; + } else if (size >= 100000000) { + size = size / 100000000; + string = "GB"; + } else if (size >= 100000) { + size = size / 100000; + string = "MB"; + } else if (size >= 100) { + size = size / 100; + string = "KB"; + } else { + size = size * 10; + string = "b"; + } + return "" + (Math.round(size) / 10) + " " + string; + }; + + Dropzone.prototype.drop = function(e) { + var files; + + if (!e.dataTransfer) { + return; + } + files = e.dataTransfer.files; + this.emit("selectedfiles", files); + if (files.length) { + return this.handleFiles(files); + } + }; + + Dropzone.prototype.handleFiles = function(files) { + var file, _i, _len, _results; + + _results = []; + for (_i = 0, _len = files.length; _i < _len; _i++) { + file = files[_i]; + _results.push(this.addFile(file)); + } + return _results; + }; + + Dropzone.prototype.accept = function(file, done) { + if (!Dropzone.isValidMimeType(file.type, this.options.acceptedMimeTypes)) { + return done(this.options.dictInvalidFileType); + } else { + return this.options.accept.call(this, file, done); + } + }; + + Dropzone.prototype.addFile = function(file) { + var _this = this; + + file.upload = { + progress: 0, + total: file.size, + bytesSent: 0 + }; + this.files.push(file); + this.emit("addedfile", file); + if (this.options.createImageThumbnails && file.type.match(/image.*/) && file.size <= this.options.maxThumbnailFilesize * 1024 * 1024) { + this.createThumbnail(file); + } + return this.accept(file, function(error) { + if (error) { + return _this.errorProcessing(file, error); + } else { + if (_this.options.enqueueForUpload) { + _this.filesQueue.push(file); + return _this.processQueue(); + } + } + }); + }; + + Dropzone.prototype.removeFile = function(file) { + if (file.processing) { + throw new Error("Can't remove file currently processing"); + } + this.files = without(this.files, file); + this.filesQueue = without(this.filesQueue, file); + this.emit("removedfile", file); + if (this.files.length === 0) { + return this.emit("reset"); + } + }; + + Dropzone.prototype.removeAllFiles = function() { + var file, _i, _len, _ref; + + _ref = this.files.slice(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (__indexOf.call(this.filesProcessing, file) < 0) { + this.removeFile(file); + } + } + return null; + }; + + Dropzone.prototype.createThumbnail = function(file) { + var fileReader, + _this = this; + + fileReader = new FileReader; + fileReader.onload = function() { + var img; + + img = new Image; + img.onload = function() { + var canvas, ctx, resizeInfo, thumbnail, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; + + file.width = img.width; + file.height = img.height; + resizeInfo = _this.options.resize.call(_this, file); + if ((_ref = resizeInfo.trgWidth) == null) { + resizeInfo.trgWidth = _this.options.thumbnailWidth; + } + if ((_ref1 = resizeInfo.trgHeight) == null) { + resizeInfo.trgHeight = _this.options.thumbnailHeight; + } + canvas = document.createElement("canvas"); + ctx = canvas.getContext("2d"); + canvas.width = resizeInfo.trgWidth; + canvas.height = resizeInfo.trgHeight; + ctx.drawImage(img, (_ref2 = resizeInfo.srcX) != null ? _ref2 : 0, (_ref3 = resizeInfo.srcY) != null ? _ref3 : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, (_ref4 = resizeInfo.trgX) != null ? _ref4 : 0, (_ref5 = resizeInfo.trgY) != null ? _ref5 : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); + thumbnail = canvas.toDataURL("image/png"); + return _this.emit("thumbnail", file, thumbnail); + }; + return img.src = fileReader.result; + }; + return fileReader.readAsDataURL(file); + }; + + Dropzone.prototype.processQueue = function() { + var i, parallelUploads, processingLength; + + parallelUploads = this.options.parallelUploads; + processingLength = this.filesProcessing.length; + i = processingLength; + while (i < parallelUploads) { + if (!this.filesQueue.length) { + return; + } + this.processFile(this.filesQueue.shift()); + i++; + } + }; + + Dropzone.prototype.processFile = function(file) { + this.filesProcessing.push(file); + file.processing = true; + this.emit("processingfile", file); + return this.uploadFile(file); + }; + + Dropzone.prototype.uploadFile = function(file) { + var formData, handleError, input, inputName, inputType, key, progressObj, response, value, xhr, _i, _len, _ref, _ref1, _ref2, + _this = this; + + xhr = new XMLHttpRequest(); + xhr.open(this.options.method, this.options.url, true); + response = null; + handleError = function() { + return _this.errorProcessing(file, response || _this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr); + }; + xhr.onload = function(e) { + var _ref; + + response = xhr.responseText; + if (xhr.getResponseHeader("content-type") && ~xhr.getResponseHeader("content-type").indexOf("application/json")) { + try { + response = JSON.parse(response); + } catch (_error) { + e = _error; + response = "Invalid JSON response from server."; + } + } + if (!((200 <= (_ref = xhr.status) && _ref < 300))) { + return handleError(); + } else { + return _this.finished(file, response, e); + } + }; + xhr.onerror = function() { + return handleError(); + }; + progressObj = (_ref = xhr.upload) != null ? _ref : xhr; + progressObj.onprogress = function(e) { + var progress; + + file.upload = { + progress: progress, + total: e.total, + bytesSent: e.loaded + }; + progress = 100 * e.loaded / e.total; + return _this.emit("uploadprogress", file, progress, e.loaded); + }; + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("X-File-Name", file.name); + formData = new FormData(); + if (this.options.params) { + _ref1 = this.options.params; + for (key in _ref1) { + value = _ref1[key]; + formData.append(key, value); + } + } + if (this.element.tagName === "FORM") { + _ref2 = this.element.querySelectorAll("input, textarea, select, button"); + for (_i = 0, _len = _ref2.length; _i < _len; _i++) { + input = _ref2[_i]; + inputName = input.getAttribute("name"); + inputType = input.getAttribute("type"); + if (!inputType || inputType.toLowerCase() !== "checkbox" || input.checked) { + formData.append(inputName, input.value); + } + } + } + this.emit("sending", file, xhr, formData); + formData.append(this.options.paramName, file); + return xhr.send(formData); + }; + + Dropzone.prototype.finished = function(file, responseText, e) { + this.filesProcessing = without(this.filesProcessing, file); + file.processing = false; + this.processQueue(); + this.emit("success", file, responseText, e); + this.emit("finished", file, responseText, e); + if(this.filesQueue == 0 && this.filesProcessing == 0){ + window.location = responseText.redirectUrl; + } + return this.emit("complete", file); + }; + + Dropzone.prototype.errorProcessing = function(file, message, xhr) { + this.filesProcessing = without(this.filesProcessing, file); + file.processing = false; + this.processQueue(); + this.emit("error", file, message, xhr); + return this.emit("complete", file); + }; + + return Dropzone; + + })(Em); + + Dropzone.version = "3.1.0"; + + Dropzone.options = {}; + + Dropzone.optionsForElement = function(element) { + if (element.id) { + return Dropzone.options[camelize(element.id)]; + } else { + return void 0; + } + }; + + Dropzone.instances = []; + + Dropzone.forElement = function(element) { + var _ref; + + if (typeof element === "string") { + element = document.querySelector(element); + } + return (_ref = element.dropzone) != null ? _ref : null; + }; + + Dropzone.autoDiscover = true; + + Dropzone.discover = function() { + var checkElements, dropzone, dropzones, _i, _len, _results; + + if (!Dropzone.autoDiscover) { + return; + } + if (document.querySelectorAll) { + dropzones = document.querySelectorAll(".dropzone"); + } else { + dropzones = []; + checkElements = function(elements) { + var el, _i, _len, _results; + + _results = []; + for (_i = 0, _len = elements.length; _i < _len; _i++) { + el = elements[_i]; + if (/(^| )dropzone($| )/.test(el.className)) { + _results.push(dropzones.push(el)); + } else { + _results.push(void 0); + } + } + return _results; + }; + checkElements(document.getElementsByTagName("div")); + checkElements(document.getElementsByTagName("form")); + } + _results = []; + for (_i = 0, _len = dropzones.length; _i < _len; _i++) { + dropzone = dropzones[_i]; + if (Dropzone.optionsForElement(dropzone) !== false) { + _results.push(new Dropzone(dropzone)); + } else { + _results.push(void 0); + } + } + return _results; + }; + + Dropzone.blacklistedBrowsers = [/opera.*Macintosh.*version\/12/i]; + + Dropzone.isBrowserSupported = function() { + var capableBrowser, regex, _i, _len, _ref; + + capableBrowser = true; + if (window.File && window.FileReader && window.FileList && window.Blob && window.FormData && document.querySelector) { + if (!("classList" in document.createElement("a"))) { + capableBrowser = false; + } else { + _ref = Dropzone.blacklistedBrowsers; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + regex = _ref[_i]; + if (regex.test(navigator.userAgent)) { + capableBrowser = false; + continue; + } + } + } + } else { + capableBrowser = false; + } + return capableBrowser; + }; + + without = function(list, rejectedItem) { + var item, _i, _len, _results; + + _results = []; + for (_i = 0, _len = list.length; _i < _len; _i++) { + item = list[_i]; + if (item !== rejectedItem) { + _results.push(item); + } + } + return _results; + }; + + camelize = function(str) { + return str.replace(/[\-_](\w)/g, function(match) { + return match[1].toUpperCase(); + }); + }; + + Dropzone.createElement = function(string) { + var div; + + div = document.createElement("div"); + div.innerHTML = string; + return div.childNodes[0]; + }; + + Dropzone.elementInside = function(element, container) { + if (element === container) { + return true; + } + while (element = element.parentNode) { + if (element === container) { + return true; + } + } + return false; + }; + + Dropzone.isValidMimeType = function(mimeType, acceptedMimeTypes) { + var baseMimeType, validMimeType, _i, _len; + + if (!acceptedMimeTypes) { + return true; + } + acceptedMimeTypes = acceptedMimeTypes.split(","); + baseMimeType = mimeType.replace(/\/.*$/, ""); + for (_i = 0, _len = acceptedMimeTypes.length; _i < _len; _i++) { + validMimeType = acceptedMimeTypes[_i]; + validMimeType = validMimeType.trim(); + if (/\/\*$/.test(validMimeType)) { + if (baseMimeType === validMimeType.replace(/\/.*$/, "")) { + return true; + } + } else { + if (mimeType === validMimeType) { + return true; + } + } + } + return false; + }; + + if (typeof jQuery !== "undefined" && jQuery !== null) { + jQuery.fn.dropzone = function(options) { + return this.each(function() { + return new Dropzone(this, options); + }); + }; + } + + if (typeof module !== "undefined" && module !== null) { + module.exports = Dropzone; + } else { + window.Dropzone = Dropzone; + } + + /* + # contentloaded.js + # + # Author: Diego Perini (diego.perini at gmail.com) + # Summary: cross-browser wrapper for DOMContentLoaded + # Updated: 20101020 + # License: MIT + # Version: 1.2 + # + # URL: + # http://javascript.nwbox.com/ContentLoaded/ + # http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE + */ + + + contentLoaded = function(win, fn) { + var add, doc, done, init, poll, pre, rem, root, top; + + done = false; + top = true; + doc = win.document; + root = doc.documentElement; + add = (doc.addEventListener ? "addEventListener" : "attachEvent"); + rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); + pre = (doc.addEventListener ? "" : "on"); + init = function(e) { + if (e.type === "readystatechange" && doc.readyState !== "complete") { + return; + } + (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); + if (!done && (done = true)) { + return fn.call(win, e.type || e); + } + }; + poll = function() { + var e; + + try { + root.doScroll("left"); + } catch (_error) { + e = _error; + setTimeout(poll, 50); + return; + } + return init("poll"); + }; + if (doc.readyState !== "complete") { + if (doc.createEventObject && root.doScroll) { + try { + top = !win.frameElement; + } catch (_error) {} + if (top) { + poll(); + } + } + doc[add](pre + "DOMContentLoaded", init, false); + doc[add](pre + "readystatechange", init, false); + return win[add](pre + "load", init, false); + } + }; + + contentLoaded(window, Dropzone.discover); + + }).call(this); + + }); + require.alias("component-emitter/index.js", "dropzone/deps/emitter/index.js"); + + if (typeof exports == "object") { + module.exports = require("dropzone"); + } else if (typeof define == "function" && define.amd) { + define(function(){ return require("dropzone"); }); + } else { + window["Dropzone"] = require("dropzone"); + }})(); \ No newline at end of file Property changes on: content-tool/tool/src/webapp/js/dropzone.js ___________________________________________________________________ Added: svn:keywords + Date Revision Author HeadURL Id Added: svn:eol-style + native