You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1399 lines
39 KiB

12 years ago
12 years ago
13 years ago
12 years ago
12 years ago
12 years ago
12 years ago
Fix dropping a folder on a folder row When the uploaded files have a relative path (that is, when a folder is uploaded) it is first ensured that all the parent folders exist, which is done by trying to create them. When a folder is created in the currently opened folder the file list is updated and a row for the new folder is added. However, this was done too when the folder already existed, which caused the previous row to be removed and a new one added to replace it. For security reasons, some special headers need to be set in requests; this is done automatically for jQuery by handling the "ajaxSend" event in the document. In the case of DAV requests, if the headers are not set the server rejects the request with "CSRF check not passed". When a file or folder is dropped on a folder row the jQuery upload events are chained from the initial drop event, which has the row as its target. In order to upload the file jQuery performs a request, which triggers the "ajaxSend" event in the row; this event then bubbles up to the document, which is then handled by adding the special headers to the request. However, when a folder was dropped on a folder row that folder row was removed when ensuring that the folder exists. The jQuery upload events were still triggered on the row, but as it had been removed it had no parent nodes, and thus the events did not bubble up. Due to this the "ajaxSend" event never reached the document when triggered on the removed row, the headers were not set, and the upload failed. All this is simply fixed by not removing the folder row when trying to create it if it existed already. Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
7 years ago
12 years ago
13 years ago
13 years ago
13 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
13 years ago
12 years ago
12 years ago
  1. /*
  2. * Copyright (c) 2014
  3. *
  4. * This file is licensed under the Affero General Public License version 3
  5. * or later.
  6. *
  7. * See the COPYING-README file.
  8. *
  9. */
  10. /**
  11. * The file upload code uses several hooks to interact with blueimps jQuery file upload library:
  12. * 1. the core upload handling hooks are added when initializing the plugin,
  13. * 2. if the browser supports progress events they are added in a separate set after the initialization
  14. * 3. every app can add its own triggers for fileupload
  15. * - files adds d'n'd handlers and also reacts to done events to add new rows to the filelist
  16. * - TODO pictures upload button
  17. * - TODO music upload button
  18. */
  19. /* global jQuery, md5 */
  20. /**
  21. * File upload object
  22. *
  23. * @class OC.FileUpload
  24. * @classdesc
  25. *
  26. * Represents a file upload
  27. *
  28. * @param {OC.Uploader} uploader uploader
  29. * @param {Object} data blueimp data
  30. */
  31. OC.FileUpload = function(uploader, data) {
  32. this.uploader = uploader;
  33. this.data = data;
  34. var basePath = '';
  35. if (this.uploader.fileList) {
  36. basePath = this.uploader.fileList.getCurrentDirectory();
  37. }
  38. var path = OC.joinPaths(basePath, this.getFile().relativePath || '', this.getFile().name);
  39. this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime();
  40. };
  41. OC.FileUpload.CONFLICT_MODE_DETECT = 0;
  42. OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1;
  43. OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2;
  44. // IE11 polyfill
  45. // TODO: nuke out of orbit as well as this legacy code
  46. if (!FileReader.prototype.readAsBinaryString) {
  47. FileReader.prototype.readAsBinaryString = function(fileData) {
  48. var binary = ''
  49. var pt = this
  50. var reader = new FileReader()
  51. reader.onload = function (e) {
  52. var bytes = new Uint8Array(reader.result)
  53. var length = bytes.byteLength
  54. for (var i = 0; i < length; i++) {
  55. binary += String.fromCharCode(bytes[i])
  56. }
  57. // pt.result - readonly so assign binary
  58. pt.content = binary
  59. $(pt).trigger('onload')
  60. }
  61. reader.readAsArrayBuffer(fileData)
  62. }
  63. }
  64. OC.FileUpload.prototype = {
  65. /**
  66. * Unique upload id
  67. *
  68. * @type string
  69. */
  70. id: null,
  71. /**
  72. * Upload data structure
  73. */
  74. data: null,
  75. /**
  76. * Upload element
  77. *
  78. * @type Object
  79. */
  80. $uploadEl: null,
  81. /**
  82. * Target folder
  83. *
  84. * @type string
  85. */
  86. _targetFolder: '',
  87. /**
  88. * @type int
  89. */
  90. _conflictMode: OC.FileUpload.CONFLICT_MODE_DETECT,
  91. /**
  92. * New name from server after autorename
  93. *
  94. * @type String
  95. */
  96. _newName: null,
  97. /**
  98. * Returns the unique upload id
  99. *
  100. * @return string
  101. */
  102. getId: function() {
  103. return this.id;
  104. },
  105. /**
  106. * Returns the file to be uploaded
  107. *
  108. * @return {File} file
  109. */
  110. getFile: function() {
  111. return this.data.files[0];
  112. },
  113. /**
  114. * Return the final filename.
  115. *
  116. * @return {String} file name
  117. */
  118. getFileName: function() {
  119. // autorenamed name
  120. if (this._newName) {
  121. return this._newName;
  122. }
  123. return this.getFile().name;
  124. },
  125. setTargetFolder: function(targetFolder) {
  126. this._targetFolder = targetFolder;
  127. },
  128. getTargetFolder: function() {
  129. return this._targetFolder;
  130. },
  131. /**
  132. * Get full path for the target file, including relative path,
  133. * without the file name.
  134. *
  135. * @return {String} full path
  136. */
  137. getFullPath: function() {
  138. return OC.joinPaths(this._targetFolder, this.getFile().relativePath || '');
  139. },
  140. /**
  141. * Get full path for the target file,
  142. * including relative path and file name.
  143. *
  144. * @return {String} full path
  145. */
  146. getFullFilePath: function() {
  147. return OC.joinPaths(this.getFullPath(), this.getFile().name);
  148. },
  149. /**
  150. * Returns conflict resolution mode.
  151. *
  152. * @return {number} conflict mode
  153. */
  154. getConflictMode: function() {
  155. return this._conflictMode || OC.FileUpload.CONFLICT_MODE_DETECT;
  156. },
  157. /**
  158. * Set conflict resolution mode.
  159. * See CONFLICT_MODE_* constants.
  160. *
  161. * @param {number} mode conflict mode
  162. */
  163. setConflictMode: function(mode) {
  164. this._conflictMode = mode;
  165. },
  166. deleteUpload: function() {
  167. delete this.data.jqXHR;
  168. },
  169. /**
  170. * Trigger autorename and append "(2)".
  171. * Multiple calls will increment the appended number.
  172. */
  173. autoRename: function() {
  174. var name = this.getFile().name;
  175. if (!this._renameAttempt) {
  176. this._renameAttempt = 1;
  177. }
  178. var dotPos = name.lastIndexOf('.');
  179. var extPart = '';
  180. if (dotPos > 0) {
  181. this._newName = name.substr(0, dotPos);
  182. extPart = name.substr(dotPos);
  183. } else {
  184. this._newName = name;
  185. }
  186. // generate new name
  187. this._renameAttempt++;
  188. this._newName = this._newName + ' (' + this._renameAttempt + ')' + extPart;
  189. },
  190. /**
  191. * Submit the upload
  192. */
  193. submit: function() {
  194. var self = this;
  195. var data = this.data;
  196. var file = this.getFile();
  197. // if file is a directory, just create it
  198. // files are handled separately
  199. if (file.isDirectory) {
  200. return this.uploader.ensureFolderExists(OC.joinPaths(this._targetFolder, file.fullPath));
  201. }
  202. if (self.aborted === true) {
  203. return $.Deferred().resolve().promise();
  204. }
  205. // it was a folder upload, so make sure the parent directory exists already
  206. var folderPromise;
  207. if (file.relativePath) {
  208. folderPromise = this.uploader.ensureFolderExists(this.getFullPath());
  209. } else {
  210. folderPromise = $.Deferred().resolve().promise();
  211. }
  212. if (this.uploader.fileList) {
  213. this.data.url = this.uploader.fileList.getUploadUrl(this.getFileName(), this.getFullPath());
  214. }
  215. if (!this.data.headers) {
  216. this.data.headers = {};
  217. }
  218. // webdav without multipart
  219. this.data.multipart = false;
  220. this.data.type = 'PUT';
  221. delete this.data.headers['If-None-Match'];
  222. if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT
  223. || this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
  224. this.data.headers['If-None-Match'] = '*';
  225. }
  226. var userName = this.uploader.davClient.getUserName();
  227. var password = this.uploader.davClient.getPassword();
  228. if (userName) {
  229. // copy username/password from DAV client
  230. this.data.headers['Authorization'] =
  231. 'Basic ' + btoa(userName + ':' + (password || ''));
  232. }
  233. var chunkFolderPromise;
  234. if ($.support.blobSlice
  235. && this.uploader.fileUploadParam.maxChunkSize
  236. && this.getFile().size > this.uploader.fileUploadParam.maxChunkSize
  237. ) {
  238. data.isChunked = true;
  239. chunkFolderPromise = this.uploader.davClient.createDirectory(
  240. 'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
  241. );
  242. // TODO: if fails, it means same id already existed, need to retry
  243. } else {
  244. chunkFolderPromise = $.Deferred().resolve().promise();
  245. var mtime = this.getFile().lastModified;
  246. if (mtime) {
  247. data.headers['X-OC-Mtime'] = mtime / 1000;
  248. }
  249. }
  250. // wait for creation of the required directory before uploading
  251. return Promise.all([folderPromise, chunkFolderPromise]).then(function() {
  252. if (self.aborted !== true) {
  253. data.submit();
  254. }
  255. }, function() {
  256. self.abort();
  257. });
  258. },
  259. /**
  260. * Process end of transfer
  261. */
  262. done: function() {
  263. if (!this.data.isChunked) {
  264. return $.Deferred().resolve().promise();
  265. }
  266. var uid = OC.getCurrentUser().uid;
  267. var mtime = this.getFile().lastModified;
  268. var size = this.getFile().size;
  269. var headers = {};
  270. if (mtime) {
  271. headers['X-OC-Mtime'] = mtime / 1000;
  272. }
  273. if (size) {
  274. headers['OC-Total-Length'] = size;
  275. }
  276. return this.uploader.davClient.move(
  277. 'uploads/' + uid + '/' + this.getId() + '/.file',
  278. 'files/' + uid + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()),
  279. true,
  280. headers
  281. );
  282. },
  283. _deleteChunkFolder: function() {
  284. // delete transfer directory for this upload
  285. this.uploader.davClient.remove(
  286. 'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
  287. );
  288. },
  289. _delete: function() {
  290. if (this.data.isChunked) {
  291. this._deleteChunkFolder()
  292. }
  293. this.deleteUpload();
  294. },
  295. /**
  296. * Abort the upload
  297. */
  298. abort: function() {
  299. if (this.aborted) {
  300. return
  301. }
  302. this.aborted = true;
  303. if (this.data) {
  304. // abort running XHR
  305. this.data.abort();
  306. }
  307. this._delete();
  308. },
  309. /**
  310. * Fail the upload
  311. */
  312. fail: function() {
  313. if (this.aborted) {
  314. return
  315. }
  316. this._delete();
  317. },
  318. /**
  319. * Returns the server response
  320. *
  321. * @return {Object} response
  322. */
  323. getResponse: function() {
  324. var response = this.data.response();
  325. if (response.errorThrown || response.textStatus === 'error') {
  326. // attempt parsing Sabre exception is available
  327. var xml = response.jqXHR.responseXML;
  328. if (xml && xml.documentElement.localName === 'error' && xml.documentElement.namespaceURI === 'DAV:') {
  329. var messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message');
  330. var exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception');
  331. if (messages.length) {
  332. response.message = messages[0].textContent;
  333. }
  334. if (exceptions.length) {
  335. response.exception = exceptions[0].textContent;
  336. }
  337. return response;
  338. }
  339. }
  340. if (typeof response.result !== 'string' && response.result) {
  341. //fetch response from iframe
  342. response = $.parseJSON(response.result[0].body.innerText);
  343. if (!response) {
  344. // likely due to internal server error
  345. response = {status: 500};
  346. }
  347. } else {
  348. response = response.result;
  349. }
  350. return response;
  351. },
  352. /**
  353. * Returns the status code from the response
  354. *
  355. * @return {number} status code
  356. */
  357. getResponseStatus: function() {
  358. if (this.uploader.isXHRUpload()) {
  359. var xhr = this.data.response().jqXHR;
  360. if (xhr) {
  361. return xhr.status;
  362. }
  363. return null;
  364. }
  365. return this.getResponse().status;
  366. },
  367. /**
  368. * Returns the response header by name
  369. *
  370. * @param {String} headerName header name
  371. * @return {Array|String} response header value(s)
  372. */
  373. getResponseHeader: function(headerName) {
  374. headerName = headerName.toLowerCase();
  375. if (this.uploader.isXHRUpload()) {
  376. return this.data.response().jqXHR.getResponseHeader(headerName);
  377. }
  378. var headers = this.getResponse().headers;
  379. if (!headers) {
  380. return null;
  381. }
  382. var value = _.find(headers, function(value, key) {
  383. return key.toLowerCase() === headerName;
  384. });
  385. if (_.isArray(value) && value.length === 1) {
  386. return value[0];
  387. }
  388. return value;
  389. }
  390. };
  391. /**
  392. * keeps track of uploads in progress and implements callbacks for the conflicts dialog
  393. * @namespace
  394. */
  395. OC.Uploader = function() {
  396. this.init.apply(this, arguments);
  397. };
  398. OC.Uploader.prototype = _.extend({
  399. /**
  400. * @type Array<OC.FileUpload>
  401. */
  402. _uploads: {},
  403. /**
  404. * Count of upload done promises that have not finished yet.
  405. *
  406. * @type int
  407. */
  408. _pendingUploadDoneCount: 0,
  409. /**
  410. * Is it currently uploading?
  411. *
  412. * @type boolean
  413. */
  414. _uploading: false,
  415. /**
  416. * List of directories known to exist.
  417. *
  418. * Key is the fullpath and value is boolean, true meaning that the directory
  419. * was already created so no need to create it again.
  420. */
  421. _knownDirs: {},
  422. /**
  423. * @type OCA.Files.FileList
  424. */
  425. fileList: null,
  426. /**
  427. * @type OCA.Files.OperationProgressBar
  428. */
  429. progressBar: null,
  430. /**
  431. * @type OC.Files.Client
  432. */
  433. filesClient: null,
  434. /**
  435. * Webdav client pointing at the root "dav" endpoint
  436. *
  437. * @type OC.Files.Client
  438. */
  439. davClient: null,
  440. /**
  441. * Function that will allow us to know if Ajax uploads are supported
  442. * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html
  443. * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata
  444. */
  445. _supportAjaxUploadWithProgress: function() {
  446. if (window.TESTING) {
  447. return true;
  448. }
  449. return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData();
  450. // Is the File API supported?
  451. function supportFileAPI() {
  452. var fi = document.createElement('INPUT');
  453. fi.type = 'file';
  454. return 'files' in fi;
  455. }
  456. // Are progress events supported?
  457. function supportAjaxUploadProgressEvents() {
  458. var xhr = new XMLHttpRequest();
  459. return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload));
  460. }
  461. // Is FormData supported?
  462. function supportFormData() {
  463. return !! window.FormData;
  464. }
  465. },
  466. /**
  467. * Returns whether an XHR upload will be used
  468. *
  469. * @return {boolean} true if XHR upload will be used,
  470. * false for iframe upload
  471. */
  472. isXHRUpload: function () {
  473. return !this.fileUploadParam.forceIframeTransport &&
  474. ((!this.fileUploadParam.multipart && $.support.xhrFileUpload) ||
  475. $.support.xhrFormDataFileUpload);
  476. },
  477. /**
  478. * Makes sure that the upload folder and its parents exists
  479. *
  480. * @param {String} fullPath full path
  481. * @return {Promise} promise that resolves when all parent folders
  482. * were created
  483. */
  484. ensureFolderExists: function(fullPath) {
  485. if (!fullPath || fullPath === '/') {
  486. return $.Deferred().resolve().promise();
  487. }
  488. // remove trailing slash
  489. if (fullPath.charAt(fullPath.length - 1) === '/') {
  490. fullPath = fullPath.substr(0, fullPath.length - 1);
  491. }
  492. var self = this;
  493. var promise = this._knownDirs[fullPath];
  494. if (this.fileList) {
  495. // assume the current folder exists
  496. this._knownDirs[this.fileList.getCurrentDirectory()] = $.Deferred().resolve().promise();
  497. }
  498. if (!promise) {
  499. var deferred = new $.Deferred();
  500. promise = deferred.promise();
  501. this._knownDirs[fullPath] = promise;
  502. // make sure all parents already exist
  503. var parentPath = OC.dirname(fullPath);
  504. var parentPromise = this._knownDirs[parentPath];
  505. if (!parentPromise) {
  506. parentPromise = this.ensureFolderExists(parentPath);
  507. }
  508. parentPromise.then(function() {
  509. self.filesClient.createDirectory(fullPath).always(function(status) {
  510. // 405 is expected if the folder already exists
  511. if ((status >= 200 && status < 300) || status === 405) {
  512. if (status !== 405) {
  513. self.trigger('createdfolder', fullPath);
  514. }
  515. deferred.resolve();
  516. return;
  517. }
  518. OC.Notification.show(t('files', 'Could not create folder "{dir}"', {dir: fullPath}), {type: 'error'});
  519. deferred.reject();
  520. });
  521. }, function() {
  522. deferred.reject();
  523. });
  524. }
  525. return promise;
  526. },
  527. /**
  528. * Submit the given uploads
  529. *
  530. * @param {Array} array of uploads to start
  531. */
  532. submitUploads: function(uploads) {
  533. var self = this;
  534. _.each(uploads, function(upload) {
  535. self._uploads[upload.data.uploadId] = upload;
  536. });
  537. if (!self._uploading) {
  538. self.totalToUpload = 0;
  539. }
  540. self.totalToUpload += _.reduce(uploads, function(memo, upload) { return memo+upload.getFile().size; }, 0);
  541. var semaphore = new OCA.Files.Semaphore(5);
  542. var promises = _.map(uploads, function(upload) {
  543. return semaphore.acquire().then(function(){
  544. return upload.submit().then(function(){
  545. semaphore.release();
  546. });
  547. });
  548. });
  549. },
  550. confirmBeforeUnload: function() {
  551. if (this._uploading) {
  552. return t('files', 'This will stop your current uploads.')
  553. }
  554. },
  555. /**
  556. * Show conflict for the given file object
  557. *
  558. * @param {OC.FileUpload} file upload object
  559. */
  560. showConflict: function(fileUpload) {
  561. //show "file already exists" dialog
  562. var self = this;
  563. var file = fileUpload.getFile();
  564. // already attempted autorename but the server said the file exists ? (concurrently added)
  565. if (fileUpload.getConflictMode() === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
  566. // attempt another autorename, defer to let the current callback finish
  567. _.defer(function() {
  568. self.onAutorename(fileUpload);
  569. });
  570. return;
  571. }
  572. // retrieve more info about this file
  573. this.filesClient.getFileInfo(fileUpload.getFullFilePath()).then(function(status, fileInfo) {
  574. var original = fileInfo;
  575. var replacement = file;
  576. original.directory = original.path;
  577. OC.dialogs.fileexists(fileUpload, original, replacement, self);
  578. });
  579. },
  580. /**
  581. * cancels all uploads
  582. */
  583. cancelUploads:function() {
  584. this.log('canceling uploads');
  585. jQuery.each(this._uploads, function(i, upload) {
  586. upload.abort();
  587. });
  588. this.clear();
  589. },
  590. /**
  591. * Clear uploads
  592. */
  593. clear: function() {
  594. this._knownDirs = {};
  595. },
  596. /**
  597. * Returns an upload by id
  598. *
  599. * @param {number} data uploadId
  600. * @return {OC.FileUpload} file upload
  601. */
  602. getUpload: function(data) {
  603. if (_.isString(data)) {
  604. return this._uploads[data];
  605. } else if (data.uploadId && this._uploads[data.uploadId]) {
  606. this._uploads[data.uploadId].data = data;
  607. return this._uploads[data.uploadId];
  608. }
  609. return null;
  610. },
  611. /**
  612. * Removes an upload from the list of known uploads.
  613. *
  614. * @param {OC.FileUpload} upload the upload to remove.
  615. */
  616. removeUpload: function(upload) {
  617. if (!upload || !upload.data || !upload.data.uploadId) {
  618. return;
  619. }
  620. // defer as some calls/chunks might still be busy failing, so we need
  621. // the upload info there still
  622. var self = this;
  623. var uploadId = upload.data.uploadId;
  624. // mark as deleted for the progress bar
  625. this._uploads[uploadId].deleted = true;
  626. window.setTimeout(function() {
  627. delete self._uploads[uploadId];
  628. }, 5000)
  629. },
  630. _activeUploadCount: function() {
  631. var count = 0;
  632. for (var key in this._uploads) {
  633. if (!this._uploads[key].deleted) {
  634. count++;
  635. }
  636. }
  637. return count;
  638. },
  639. showUploadCancelMessage: _.debounce(function() {
  640. OC.Notification.show(t('files', 'Upload cancelled.'), {timeout : 7, type: 'error'});
  641. }, 500),
  642. /**
  643. * callback for the conflicts dialog
  644. */
  645. onCancel:function() {
  646. this.cancelUploads();
  647. },
  648. /**
  649. * callback for the conflicts dialog
  650. * calls onSkip, onReplace or onAutorename for each conflict
  651. * @param {object} conflicts - list of conflict elements
  652. */
  653. onContinue:function(conflicts) {
  654. var self = this;
  655. //iterate over all conflicts
  656. jQuery.each(conflicts, function (i, conflict) {
  657. conflict = $(conflict);
  658. var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1;
  659. var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1;
  660. if (keepOriginal && keepReplacement) {
  661. // when both selected -> autorename
  662. self.onAutorename(conflict.data('data'));
  663. } else if (keepReplacement) {
  664. // when only replacement selected -> overwrite
  665. self.onReplace(conflict.data('data'));
  666. } else {
  667. // when only original selected -> skip
  668. // when none selected -> skip
  669. self.onSkip(conflict.data('data'));
  670. }
  671. });
  672. },
  673. /**
  674. * handle skipping an upload
  675. * @param {OC.FileUpload} upload
  676. */
  677. onSkip:function(upload) {
  678. this.log('skip', null, upload);
  679. upload.deleteUpload();
  680. },
  681. /**
  682. * handle replacing a file on the server with an uploaded file
  683. * @param {FileUpload} data
  684. */
  685. onReplace:function(upload) {
  686. this.log('replace', null, upload);
  687. upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE);
  688. this.submitUploads([upload]);
  689. },
  690. /**
  691. * handle uploading a file and letting the server decide a new name
  692. * @param {object} upload
  693. */
  694. onAutorename:function(upload) {
  695. this.log('autorename', null, upload);
  696. upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME);
  697. do {
  698. upload.autoRename();
  699. // if file known to exist on the client side, retry
  700. } while (this.fileList && this.fileList.inList(upload.getFileName()));
  701. // resubmit upload
  702. this.submitUploads([upload]);
  703. },
  704. _trace: false, //TODO implement log handler for JS per class?
  705. log: function(caption, e, data) {
  706. if (this._trace) {
  707. console.log(caption);
  708. console.log(data);
  709. }
  710. },
  711. /**
  712. * checks the list of existing files prior to uploading and shows a simple dialog to choose
  713. * skip all, replace all or choose which files to keep
  714. *
  715. * @param {array} selection of files to upload
  716. * @param {object} callbacks - object with several callback methods
  717. * @param {Function} callbacks.onNoConflicts
  718. * @param {Function} callbacks.onSkipConflicts
  719. * @param {Function} callbacks.onReplaceConflicts
  720. * @param {Function} callbacks.onChooseConflicts
  721. * @param {Function} callbacks.onCancel
  722. */
  723. checkExistingFiles: function (selection, callbacks) {
  724. var fileList = this.fileList;
  725. var conflicts = [];
  726. // only keep non-conflicting uploads
  727. selection.uploads = _.filter(selection.uploads, function(upload) {
  728. var file = upload.getFile();
  729. if (file.relativePath) {
  730. // can't check in subfolder contents
  731. return true;
  732. }
  733. if (!fileList) {
  734. // no list to check against
  735. return true;
  736. }
  737. if (upload.getTargetFolder() !== fileList.getCurrentDirectory()) {
  738. // not uploading to the current folder
  739. return true;
  740. }
  741. var fileInfo = fileList.findFile(file.name);
  742. if (fileInfo) {
  743. conflicts.push([
  744. // original
  745. _.extend(fileInfo, {
  746. directory: fileInfo.directory || fileInfo.path || fileList.getCurrentDirectory()
  747. }),
  748. // replacement (File object)
  749. upload
  750. ]);
  751. return false;
  752. }
  753. return true;
  754. });
  755. if (conflicts.length) {
  756. // wait for template loading
  757. OC.dialogs.fileexists(null, null, null, this).done(function() {
  758. _.each(conflicts, function(conflictData) {
  759. OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this);
  760. });
  761. });
  762. }
  763. // upload non-conflicting files
  764. // note: when reaching the server they might still meet conflicts
  765. // if the folder was concurrently modified, these will get added
  766. // to the already visible dialog, if applicable
  767. callbacks.onNoConflicts(selection);
  768. },
  769. _updateProgressBarOnUploadStop: function() {
  770. if (this._pendingUploadDoneCount === 0) {
  771. // All the uploads ended and there is no pending operation, so hide
  772. // the progress bar.
  773. // Note that this happens here only with non-chunked uploads; if the
  774. // upload was chunked then this will have been executed after all
  775. // the uploads ended but before the upload done handler that reduces
  776. // the pending operation count was executed.
  777. this._hideProgressBar();
  778. return;
  779. }
  780. this._setProgressBarText(t('files', 'Processing files …'), t('files', '…'));
  781. // Nothing is being uploaded at this point, and the pending operations
  782. // can not be cancelled, so the cancel button should be hidden.
  783. this._hideCancelButton();
  784. },
  785. _hideProgressBar: function() {
  786. this.progressBar.hideProgressBar();
  787. },
  788. _hideCancelButton: function() {
  789. this.progressBar.hideCancelButton();
  790. },
  791. _showProgressBar: function() {
  792. this.progressBar.showProgressBar();
  793. },
  794. _setProgressBarValue: function(value) {
  795. this.progressBar.setProgressBarValue(value);
  796. },
  797. _setProgressBarText: function(textDesktop, textMobile, title) {
  798. this.progressBar.setProgressBarText(textDesktop, textMobile, title);
  799. },
  800. /**
  801. * Returns whether the given file is known to be a received shared file
  802. *
  803. * @param {Object} file file
  804. * @return {boolean} true if the file is a shared file
  805. */
  806. _isReceivedSharedFile: function(file) {
  807. if (!window.FileList) {
  808. return false;
  809. }
  810. var $tr = window.FileList.findFileEl(file.name);
  811. if (!$tr.length) {
  812. return false;
  813. }
  814. return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory');
  815. },
  816. /**
  817. * Initialize the upload object
  818. *
  819. * @param {Object} $uploadEl upload element
  820. * @param {Object} options
  821. * @param {OCA.Files.FileList} [options.fileList] file list object
  822. * @param {OC.Files.Client} [options.filesClient] files client object
  823. * @param {Object} [options.dropZone] drop zone for drag and drop upload
  824. */
  825. init: function($uploadEl, options) {
  826. var self = this;
  827. options = options || {};
  828. this.fileList = options.fileList;
  829. this.progressBar = options.progressBar;
  830. this.filesClient = options.filesClient || OC.Files.getClient();
  831. this.davClient = new OC.Files.Client({
  832. host: this.filesClient.getHost(),
  833. root: OC.linkToRemoteBase('dav'),
  834. useHTTPS: OC.getProtocol() === 'https',
  835. userName: this.filesClient.getUserName(),
  836. password: this.filesClient.getPassword()
  837. });
  838. $uploadEl = $($uploadEl);
  839. this.$uploadEl = $uploadEl;
  840. if ($uploadEl.exists()) {
  841. this.progressBar.on('cancel', function() {
  842. self.cancelUploads();
  843. self.showUploadCancelMessage();
  844. });
  845. this.fileUploadParam = {
  846. type: 'PUT',
  847. dropZone: options.dropZone, // restrict dropZone to content div
  848. autoUpload: false,
  849. sequentialUploads: false,
  850. limitConcurrentUploads: 4,
  851. /**
  852. * on first add of every selection
  853. * - check all files of originalFiles array with files in dir
  854. * - on conflict show dialog
  855. * - skip all -> remember as single skip action for all conflicting files
  856. * - replace all -> remember as single replace action for all conflicting files
  857. * - choose -> show choose dialog
  858. * - mark files to keep
  859. * - when only existing -> remember as single skip action
  860. * - when only new -> remember as single replace action
  861. * - when both -> remember as single autorename action
  862. * - start uploading selection
  863. * @param {object} e
  864. * @param {object} data
  865. * @returns {boolean}
  866. */
  867. add: function(e, data) {
  868. self.log('add', e, data);
  869. var that = $(this), freeSpace = 0;
  870. var upload = new OC.FileUpload(self, data);
  871. // can't link directly due to jQuery not liking cyclic deps on its ajax object
  872. data.uploadId = upload.getId();
  873. // create a container where we can store the data objects
  874. if ( ! data.originalFiles.selection ) {
  875. // initialize selection and remember number of files to upload
  876. data.originalFiles.selection = {
  877. uploads: [],
  878. filesToUpload: data.originalFiles.length,
  879. totalBytes: 0
  880. };
  881. }
  882. // TODO: move originalFiles to a separate container, maybe inside OC.Upload
  883. var selection = data.originalFiles.selection;
  884. // add uploads
  885. if ( selection.uploads.length < selection.filesToUpload ) {
  886. // remember upload
  887. selection.uploads.push(upload);
  888. }
  889. //examine file
  890. var file = upload.getFile();
  891. try {
  892. // FIXME: not so elegant... need to refactor that method to return a value
  893. Files.isFileNameValid(file.name);
  894. }
  895. catch (errorMessage) {
  896. data.textStatus = 'invalidcharacters';
  897. data.errorThrown = errorMessage;
  898. }
  899. if (data.targetDir) {
  900. upload.setTargetFolder(data.targetDir);
  901. delete data.targetDir;
  902. }
  903. // in case folder drag and drop is not supported file will point to a directory
  904. // http://stackoverflow.com/a/20448357
  905. if ( !file.type && file.size % 4096 === 0 && file.size <= 102400) {
  906. var dirUploadFailure = false;
  907. try {
  908. var reader = new FileReader();
  909. reader.readAsBinaryString(file);
  910. } catch (error) {
  911. console.log(reader, error)
  912. //file is a directory
  913. dirUploadFailure = true;
  914. }
  915. if (dirUploadFailure) {
  916. data.textStatus = 'dirorzero';
  917. data.errorThrown = t('files',
  918. 'Unable to upload {filename} as it is a directory or has 0 bytes',
  919. {filename: file.name}
  920. );
  921. }
  922. }
  923. // only count if we're not overwriting an existing shared file
  924. if (self._isReceivedSharedFile(file)) {
  925. file.isReceivedShare = true;
  926. } else {
  927. // add size
  928. selection.totalBytes += file.size;
  929. }
  930. // check free space
  931. if (!self.fileList || upload.getTargetFolder() === self.fileList.getCurrentDirectory()) {
  932. // Use global free space if there is no file list to check or the current directory is the target
  933. freeSpace = $('input[name=free_space]').val()
  934. } else if (upload.getTargetFolder().indexOf(self.fileList.getCurrentDirectory()) === 0) {
  935. // Check subdirectory free space if file is uploaded there
  936. // Retrieve the folder destination name
  937. var targetSubdir = upload._targetFolder.split('/').pop()
  938. freeSpace = parseInt(upload.uploader.fileList.getModelForFile(targetSubdir).get('quotaAvailableBytes'))
  939. }
  940. if (freeSpace >= 0 && selection.totalBytes > freeSpace) {
  941. data.textStatus = 'notenoughspace';
  942. data.errorThrown = t('files',
  943. 'Not enough free space, you are uploading {size1} but only {size2} is left', {
  944. 'size1': OC.Util.humanFileSize(selection.totalBytes),
  945. 'size2': OC.Util.humanFileSize(freeSpace)
  946. });
  947. }
  948. // end upload for whole selection on error
  949. if (data.errorThrown) {
  950. // trigger fileupload fail handler
  951. var fu = that.data('blueimp-fileupload') || that.data('fileupload');
  952. fu._trigger('fail', e, data);
  953. return false; //don't upload anything
  954. }
  955. // check existing files when all is collected
  956. if ( selection.uploads.length >= selection.filesToUpload ) {
  957. //remove our selection hack:
  958. delete data.originalFiles.selection;
  959. var callbacks = {
  960. onNoConflicts: function (selection) {
  961. self.submitUploads(selection.uploads);
  962. },
  963. onSkipConflicts: function (selection) {
  964. //TODO mark conflicting files as toskip
  965. },
  966. onReplaceConflicts: function (selection) {
  967. //TODO mark conflicting files as toreplace
  968. },
  969. onChooseConflicts: function (selection) {
  970. //TODO mark conflicting files as chosen
  971. },
  972. onCancel: function (selection) {
  973. $.each(selection.uploads, function(i, upload) {
  974. upload.abort();
  975. });
  976. }
  977. };
  978. self.checkExistingFiles(selection, callbacks);
  979. }
  980. return true; // continue adding files
  981. },
  982. /**
  983. * called after the first add, does NOT have the data param
  984. * @param {object} e
  985. */
  986. start: function(e) {
  987. self.log('start', e, null);
  988. self._uploading = true;
  989. },
  990. fail: function(e, data) {
  991. var upload = self.getUpload(data);
  992. var status = null;
  993. if (upload) {
  994. if (upload.aborted) {
  995. // uploads might fail with errors from the server when aborted
  996. return
  997. }
  998. status = upload.getResponseStatus();
  999. }
  1000. self.log('fail', e, upload);
  1001. self.removeUpload(upload);
  1002. if (data.textStatus === 'abort' || data.errorThrown === 'abort') {
  1003. return
  1004. } else if (status === 412) {
  1005. // file already exists
  1006. self.showConflict(upload);
  1007. } else if (status === 404) {
  1008. // target folder does not exist any more
  1009. OC.Notification.show(t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()} ), {type: 'error'});
  1010. self.cancelUploads();
  1011. } else if (data.textStatus === 'notenoughspace') {
  1012. // not enough space
  1013. OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'});
  1014. self.cancelUploads();
  1015. } else {
  1016. // HTTP connection problem or other error
  1017. var message = t('files', 'An unknown error has occurred');
  1018. if (upload) {
  1019. var response = upload.getResponse();
  1020. if (response) {
  1021. message = response.message;
  1022. }
  1023. }
  1024. console.error(e, data, response)
  1025. OC.Notification.show(message || data.errorThrown || t('files', 'File could not be uploaded'), {type: 'error'});
  1026. }
  1027. if (upload) {
  1028. upload.fail();
  1029. }
  1030. },
  1031. /**
  1032. * called for every successful upload
  1033. * @param {object} e
  1034. * @param {object} data
  1035. */
  1036. done:function(e, data) {
  1037. var upload = self.getUpload(data);
  1038. var that = $(this);
  1039. self.log('done', e, upload);
  1040. self.removeUpload(upload);
  1041. var status = upload.getResponseStatus();
  1042. if (status < 200 || status >= 300) {
  1043. // trigger fail handler
  1044. var fu = that.data('blueimp-fileupload') || that.data('fileupload');
  1045. fu._trigger('fail', e, data);
  1046. return;
  1047. }
  1048. },
  1049. /**
  1050. * called after last upload
  1051. * @param {object} e
  1052. * @param {object} data
  1053. */
  1054. stop: function(e, data) {
  1055. self.log('stop', e, data);
  1056. self._uploading = false;
  1057. }
  1058. };
  1059. if (options.maxChunkSize) {
  1060. this.fileUploadParam.maxChunkSize = options.maxChunkSize;
  1061. }
  1062. // initialize jquery fileupload (blueimp)
  1063. var fileupload = this.$uploadEl.fileupload(this.fileUploadParam);
  1064. if (this._supportAjaxUploadWithProgress()) {
  1065. //remaining time
  1066. var lastUpdate, lastSize, bufferSize, buffer, bufferIndex, bufferIndex2, bufferTotal;
  1067. var dragging = false;
  1068. // add progress handlers
  1069. fileupload.on('fileuploadadd', function(e, data) {
  1070. self.log('progress handle fileuploadadd', e, data);
  1071. self.trigger('add', e, data);
  1072. });
  1073. // add progress handlers
  1074. fileupload.on('fileuploadstart', function(e, data) {
  1075. self.log('progress handle fileuploadstart', e, data);
  1076. self._setProgressBarText(t('files', 'Uploading …'), t('files', '…'));
  1077. self._setProgressBarValue(0);
  1078. self._showProgressBar();
  1079. // initial remaining time variables
  1080. lastUpdate = new Date().getTime();
  1081. lastSize = 0;
  1082. bufferSize = 20;
  1083. buffer = [];
  1084. bufferIndex = 0;
  1085. bufferIndex2 = 0;
  1086. bufferTotal = 0;
  1087. for(var i = 0; i < bufferSize; i++){
  1088. buffer[i] = 0;
  1089. }
  1090. self.trigger('start', e, data);
  1091. });
  1092. fileupload.on('fileuploadprogress', function(e, data) {
  1093. self.log('progress handle fileuploadprogress', e, data);
  1094. //TODO progressbar in row
  1095. self.trigger('progress', e, data);
  1096. });
  1097. fileupload.on('fileuploadprogressall', function(e, data) {
  1098. self.log('progress handle fileuploadprogressall', e, data);
  1099. var total = self.totalToUpload;
  1100. var progress = (data.loaded / total) * 100;
  1101. var thisUpdate = new Date().getTime();
  1102. var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s
  1103. lastUpdate = thisUpdate;
  1104. var diffSize = data.loaded - lastSize;
  1105. lastSize = data.loaded;
  1106. diffSize = diffSize / diffUpdate; // apply timing factor, eg. 1MiB/2s = 0.5MiB/s, unit is byte per second
  1107. var remainingSeconds = ((total - data.loaded) / diffSize);
  1108. if(remainingSeconds >= 0) {
  1109. bufferTotal = bufferTotal - (buffer[bufferIndex]) + remainingSeconds;
  1110. buffer[bufferIndex] = remainingSeconds; //buffer to make it smoother
  1111. bufferIndex = (bufferIndex + 1) % bufferSize;
  1112. bufferIndex2++;
  1113. }
  1114. var smoothRemainingSeconds;
  1115. if (bufferIndex2 > 0 && bufferIndex2 < 20) {
  1116. smoothRemainingSeconds = bufferTotal / bufferIndex2;
  1117. } else if (bufferSize > 0) {
  1118. smoothRemainingSeconds = bufferTotal / bufferSize;
  1119. } else {
  1120. smoothRemainingSeconds = 1;
  1121. }
  1122. var h = moment.duration(smoothRemainingSeconds, "seconds").humanize();
  1123. if (!(smoothRemainingSeconds >= 0 && smoothRemainingSeconds < 14400)) {
  1124. // show "Uploading ..." for durations longer than 4 hours
  1125. h = t('files', 'Uploading …');
  1126. }
  1127. self._setProgressBarText(h, h, t('files', '{loadedSize} of {totalSize} ({bitrate})' , {
  1128. loadedSize: OC.Util.humanFileSize(data.loaded),
  1129. totalSize: OC.Util.humanFileSize(total),
  1130. bitrate: OC.Util.humanFileSize(data.bitrate / 8) + '/s'
  1131. }));
  1132. self._setProgressBarValue(progress);
  1133. self.trigger('progressall', e, data);
  1134. });
  1135. fileupload.on('fileuploadstop', function(e, data) {
  1136. self.log('progress handle fileuploadstop', e, data);
  1137. self.clear();
  1138. self._updateProgressBarOnUploadStop();
  1139. self.trigger('stop', e, data);
  1140. });
  1141. fileupload.on('fileuploadfail', function(e, data) {
  1142. self.log('progress handle fileuploadfail', e, data);
  1143. self.trigger('fail', e, data);
  1144. });
  1145. fileupload.on('fileuploaddragover', function(e){
  1146. $('#app-content').addClass('file-drag');
  1147. $('.emptyfilelist.emptycontent .icon-folder').addClass('icon-filetype-folder-drag-accept');
  1148. var filerow = $(e.delegatedEvent.target).closest('tr');
  1149. if(!filerow.hasClass('dropping-to-dir')){
  1150. $('.dropping-to-dir .icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
  1151. $('.dropping-to-dir').removeClass('dropping-to-dir');
  1152. $('.dir-drop').removeClass('dir-drop');
  1153. }
  1154. if(filerow.attr('data-type') === 'dir'){
  1155. $('#app-content').addClass('dir-drop');
  1156. filerow.addClass('dropping-to-dir');
  1157. filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept');
  1158. }
  1159. dragging = true;
  1160. });
  1161. var disableDropState = function() {
  1162. $('#app-content').removeClass('file-drag');
  1163. $('.dropping-to-dir').removeClass('dropping-to-dir');
  1164. $('.dir-drop').removeClass('dir-drop');
  1165. $('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
  1166. dragging = false;
  1167. };
  1168. fileupload.on('fileuploaddragleave fileuploaddrop', disableDropState);
  1169. // In some browsers the "drop" event can be triggered with no
  1170. // files even if the "dragover" event seemed to suggest that a
  1171. // file was being dragged (and thus caused "fileuploaddragover"
  1172. // to be triggered).
  1173. fileupload.on('fileuploaddropnofiles', function() {
  1174. if (!dragging) {
  1175. return;
  1176. }
  1177. disableDropState();
  1178. OC.Notification.show(t('files', 'Uploading that item is not supported'), {type: 'error'});
  1179. });
  1180. fileupload.on('fileuploadchunksend', function(e, data) {
  1181. // modify the request to adjust it to our own chunking
  1182. var upload = self.getUpload(data);
  1183. if (!upload) {
  1184. // likely cancelled
  1185. return
  1186. }
  1187. var range = data.contentRange.split(' ')[1];
  1188. var chunkId = range.split('/')[0].split('-')[0];
  1189. data.url = OC.getRootPath() +
  1190. '/remote.php/dav/uploads' +
  1191. '/' + OC.getCurrentUser().uid +
  1192. '/' + upload.getId() +
  1193. '/' + chunkId;
  1194. delete data.contentRange;
  1195. delete data.headers['Content-Range'];
  1196. });
  1197. fileupload.on('fileuploaddone', function(e, data) {
  1198. var upload = self.getUpload(data);
  1199. self._pendingUploadDoneCount++;
  1200. upload.done().always(function() {
  1201. self._pendingUploadDoneCount--;
  1202. if (self._activeUploadCount() === 0 && self._pendingUploadDoneCount === 0) {
  1203. // All the uploads ended and there is no pending
  1204. // operation, so hide the progress bar.
  1205. // Note that this happens here only with chunked
  1206. // uploads; if the upload was non-chunked then this
  1207. // handler is immediately executed, before the
  1208. // jQuery upload done handler that removes the
  1209. // upload from the list, and thus at this point
  1210. // there is still at least one upload that has not
  1211. // ended (although the upload stop handler is always
  1212. // executed after all the uploads have ended, which
  1213. // hides the progress bar in that case).
  1214. self._hideProgressBar();
  1215. }
  1216. }).done(function() {
  1217. self.trigger('done', e, upload);
  1218. }).fail(function(status, response) {
  1219. if (upload.aborted) {
  1220. return
  1221. }
  1222. var message = response.message;
  1223. if (status === 507) {
  1224. // not enough space
  1225. OC.Notification.show(message || t('files', 'Not enough free space'), {type: 'error'});
  1226. self.cancelUploads();
  1227. } else if (status === 409) {
  1228. OC.Notification.show(message || t('files', 'Target folder does not exist any more'), {type: 'error'});
  1229. } else if (status === 403) {
  1230. OC.Notification.show(message || t('files', 'Operation is blocked by access control'), {type: 'error'});
  1231. } else {
  1232. OC.Notification.show(message || t('files', 'Error when assembling chunks, status code {status}', {status: status}), {type: 'error'});
  1233. }
  1234. self.trigger('fail', e, data);
  1235. });
  1236. });
  1237. fileupload.on('fileuploaddrop', function(e, data) {
  1238. self.trigger('drop', e, data);
  1239. if (e.isPropagationStopped()) {
  1240. return false;
  1241. }
  1242. });
  1243. }
  1244. window.onbeforeunload = function() {
  1245. return self.confirmBeforeUnload();
  1246. }
  1247. }
  1248. //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used)
  1249. if (navigator.userAgent.search(/konqueror/i) === -1) {
  1250. this.$uploadEl.attr('multiple', 'multiple');
  1251. }
  1252. return this.fileUploadParam;
  1253. }
  1254. }, OC.Backbone.Events);