file-upload.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  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 it's 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, humanFileSize, 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 path = '';
  35. if (this.uploader.fileList) {
  36. path = OC.joinPaths(this.uploader.fileList.getCurrentDirectory(), this.getFile().name);
  37. } else {
  38. path = this.getFile().name;
  39. }
  40. this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime();
  41. };
  42. OC.FileUpload.CONFLICT_MODE_DETECT = 0;
  43. OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1;
  44. OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2;
  45. OC.FileUpload.prototype = {
  46. /**
  47. * Unique upload id
  48. *
  49. * @type string
  50. */
  51. id: null,
  52. /**
  53. * Upload element
  54. *
  55. * @type Object
  56. */
  57. $uploadEl: null,
  58. /**
  59. * Target folder
  60. *
  61. * @type string
  62. */
  63. _targetFolder: '',
  64. /**
  65. * @type int
  66. */
  67. _conflictMode: OC.FileUpload.CONFLICT_MODE_DETECT,
  68. /**
  69. * New name from server after autorename
  70. *
  71. * @type String
  72. */
  73. _newName: null,
  74. /**
  75. * Returns the unique upload id
  76. *
  77. * @return string
  78. */
  79. getId: function() {
  80. return this.id;
  81. },
  82. /**
  83. * Returns the file to be uploaded
  84. *
  85. * @return {File} file
  86. */
  87. getFile: function() {
  88. return this.data.files[0];
  89. },
  90. /**
  91. * Return the final filename.
  92. *
  93. * @return {String} file name
  94. */
  95. getFileName: function() {
  96. // autorenamed name
  97. if (this._newName) {
  98. return this._newName;
  99. }
  100. return this.getFile().name;
  101. },
  102. setTargetFolder: function(targetFolder) {
  103. this._targetFolder = targetFolder;
  104. },
  105. getTargetFolder: function() {
  106. return this._targetFolder;
  107. },
  108. /**
  109. * Get full path for the target file, including relative path,
  110. * without the file name.
  111. *
  112. * @return {String} full path
  113. */
  114. getFullPath: function() {
  115. return OC.joinPaths(this._targetFolder, this.getFile().relativePath || '');
  116. },
  117. /**
  118. * Get full path for the target file,
  119. * including relative path and file name.
  120. *
  121. * @return {String} full path
  122. */
  123. getFullFilePath: function() {
  124. return OC.joinPaths(this.getFullPath(), this.getFile().name);
  125. },
  126. /**
  127. * Returns conflict resolution mode.
  128. *
  129. * @return {int} conflict mode
  130. */
  131. getConflictMode: function() {
  132. return this._conflictMode || OC.FileUpload.CONFLICT_MODE_DETECT;
  133. },
  134. /**
  135. * Set conflict resolution mode.
  136. * See CONFLICT_MODE_* constants.
  137. *
  138. * @param {int} mode conflict mode
  139. */
  140. setConflictMode: function(mode) {
  141. this._conflictMode = mode;
  142. },
  143. deleteUpload: function() {
  144. delete this.data.jqXHR;
  145. },
  146. /**
  147. * Trigger autorename and append "(2)".
  148. * Multiple calls will increment the appended number.
  149. */
  150. autoRename: function() {
  151. var name = this.getFile().name;
  152. if (!this._renameAttempt) {
  153. this._renameAttempt = 1;
  154. }
  155. var dotPos = name.lastIndexOf('.');
  156. var extPart = '';
  157. if (dotPos > 0) {
  158. this._newName = name.substr(0, dotPos);
  159. extPart = name.substr(dotPos);
  160. } else {
  161. this._newName = name;
  162. }
  163. // generate new name
  164. this._renameAttempt++;
  165. this._newName = this._newName + ' (' + this._renameAttempt + ')' + extPart;
  166. },
  167. /**
  168. * Submit the upload
  169. */
  170. submit: function() {
  171. var self = this;
  172. var data = this.data;
  173. var file = this.getFile();
  174. // it was a folder upload, so make sure the parent directory exists alrady
  175. var folderPromise;
  176. if (file.relativePath) {
  177. folderPromise = this.uploader.ensureFolderExists(this.getFullPath());
  178. } else {
  179. folderPromise = $.Deferred().resolve().promise();
  180. }
  181. if (this.uploader.fileList) {
  182. this.data.url = this.uploader.fileList.getUploadUrl(this.getFileName(), this.getFullPath());
  183. }
  184. if (!this.data.headers) {
  185. this.data.headers = {};
  186. }
  187. // webdav without multipart
  188. this.data.multipart = false;
  189. this.data.type = 'PUT';
  190. delete this.data.headers['If-None-Match'];
  191. if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT
  192. || this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
  193. this.data.headers['If-None-Match'] = '*';
  194. }
  195. if (file.lastModified) {
  196. // preserve timestamp
  197. this.data.headers['X-OC-Mtime'] = file.lastModified / 1000;
  198. }
  199. var userName = this.uploader.filesClient.getUserName();
  200. var password = this.uploader.filesClient.getPassword();
  201. if (userName) {
  202. // copy username/password from DAV client
  203. this.data.headers['Authorization'] =
  204. 'Basic ' + btoa(userName + ':' + (password || ''));
  205. }
  206. var chunkFolderPromise;
  207. if ($.support.blobSlice
  208. && this.uploader.fileUploadParam.maxChunkSize
  209. && this.getFile().size > this.uploader.fileUploadParam.maxChunkSize
  210. ) {
  211. data.isChunked = true;
  212. chunkFolderPromise = this.uploader.filesClient.createDirectory(
  213. 'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId())
  214. );
  215. // TODO: if fails, it means same id already existed, need to retry
  216. } else {
  217. chunkFolderPromise = $.Deferred().resolve().promise();
  218. }
  219. // wait for creation of the required directory before uploading
  220. $.when(folderPromise, chunkFolderPromise).then(function() {
  221. data.submit();
  222. }, function() {
  223. self.abort();
  224. });
  225. },
  226. /**
  227. * Process end of transfer
  228. */
  229. done: function() {
  230. if (!this.data.isChunked) {
  231. return $.Deferred().resolve().promise();
  232. }
  233. var uid = OC.getCurrentUser().uid;
  234. return this.uploader.filesClient.move(
  235. 'uploads/' + encodeURIComponent(uid) + '/' + encodeURIComponent(this.getId()) + '/.file',
  236. 'files/' + encodeURIComponent(uid) + '/' + OC.joinPaths(this.getFullPath(), this.getFileName())
  237. );
  238. },
  239. /**
  240. * Abort the upload
  241. */
  242. abort: function() {
  243. if (this.data.isChunked) {
  244. // delete transfer directory for this upload
  245. this.uploader.filesClient.remove(
  246. 'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId())
  247. );
  248. }
  249. this.data.abort();
  250. },
  251. /**
  252. * Returns the server response
  253. *
  254. * @return {Object} response
  255. */
  256. getResponse: function() {
  257. var response = this.data.response();
  258. if (typeof response.result !== 'string') {
  259. //fetch response from iframe
  260. response = $.parseJSON(response.result[0].body.innerText);
  261. if (!response) {
  262. // likely due to internal server error
  263. response = {status: 500};
  264. }
  265. } else {
  266. response = response.result;
  267. }
  268. return response;
  269. },
  270. /**
  271. * Returns the status code from the response
  272. *
  273. * @return {int} status code
  274. */
  275. getResponseStatus: function() {
  276. if (this.uploader.isXHRUpload()) {
  277. var xhr = this.data.response().jqXHR;
  278. if (xhr) {
  279. return xhr.status;
  280. }
  281. return null;
  282. }
  283. return this.getResponse().status;
  284. },
  285. /**
  286. * Returns the response header by name
  287. *
  288. * @param {String} headerName header name
  289. * @return {Array|String} response header value(s)
  290. */
  291. getResponseHeader: function(headerName) {
  292. headerName = headerName.toLowerCase();
  293. if (this.uploader.isXHRUpload()) {
  294. return this.data.response().jqXHR.getResponseHeader(headerName);
  295. }
  296. var headers = this.getResponse().headers;
  297. if (!headers) {
  298. return null;
  299. }
  300. var value = _.find(headers, function(value, key) {
  301. return key.toLowerCase() === headerName;
  302. });
  303. if (_.isArray(value) && value.length === 1) {
  304. return value[0];
  305. }
  306. return value;
  307. }
  308. };
  309. /**
  310. * keeps track of uploads in progress and implements callbacks for the conflicts dialog
  311. * @namespace
  312. */
  313. OC.Uploader = function() {
  314. this.init.apply(this, arguments);
  315. };
  316. OC.Uploader.prototype = _.extend({
  317. /**
  318. * @type Array<OC.FileUpload>
  319. */
  320. _uploads: {},
  321. /**
  322. * List of directories known to exist.
  323. *
  324. * Key is the fullpath and value is boolean, true meaning that the directory
  325. * was already created so no need to create it again.
  326. */
  327. _knownDirs: {},
  328. /**
  329. * @type OCA.Files.FileList
  330. */
  331. fileList: null,
  332. /**
  333. * @type OC.Files.Client
  334. */
  335. filesClient: null,
  336. /**
  337. * Function that will allow us to know if Ajax uploads are supported
  338. * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html
  339. * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata
  340. */
  341. _supportAjaxUploadWithProgress: function() {
  342. if (window.TESTING) {
  343. return true;
  344. }
  345. return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData();
  346. // Is the File API supported?
  347. function supportFileAPI() {
  348. var fi = document.createElement('INPUT');
  349. fi.type = 'file';
  350. return 'files' in fi;
  351. }
  352. // Are progress events supported?
  353. function supportAjaxUploadProgressEvents() {
  354. var xhr = new XMLHttpRequest();
  355. return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload));
  356. }
  357. // Is FormData supported?
  358. function supportFormData() {
  359. return !! window.FormData;
  360. }
  361. },
  362. /**
  363. * Returns whether an XHR upload will be used
  364. *
  365. * @return {bool} true if XHR upload will be used,
  366. * false for iframe upload
  367. */
  368. isXHRUpload: function () {
  369. return !this.fileUploadParam.forceIframeTransport &&
  370. ((!this.fileUploadParam.multipart && $.support.xhrFileUpload) ||
  371. $.support.xhrFormDataFileUpload);
  372. },
  373. /**
  374. * Makes sure that the upload folder and its parents exists
  375. *
  376. * @param {String} fullPath full path
  377. * @return {Promise} promise that resolves when all parent folders
  378. * were created
  379. */
  380. ensureFolderExists: function(fullPath) {
  381. if (!fullPath || fullPath === '/') {
  382. return $.Deferred().resolve().promise();
  383. }
  384. // remove trailing slash
  385. if (fullPath.charAt(fullPath.length - 1) === '/') {
  386. fullPath = fullPath.substr(0, fullPath.length - 1);
  387. }
  388. var self = this;
  389. var promise = this._knownDirs[fullPath];
  390. if (this.fileList) {
  391. // assume the current folder exists
  392. this._knownDirs[this.fileList.getCurrentDirectory()] = $.Deferred().resolve().promise();
  393. }
  394. if (!promise) {
  395. var deferred = new $.Deferred();
  396. promise = deferred.promise();
  397. this._knownDirs[fullPath] = promise;
  398. // make sure all parents already exist
  399. var parentPath = OC.dirname(fullPath);
  400. var parentPromise = this._knownDirs[parentPath];
  401. if (!parentPromise) {
  402. parentPromise = this.ensureFolderExists(parentPath);
  403. }
  404. parentPromise.then(function() {
  405. self.filesClient.createDirectory(fullPath).always(function(status) {
  406. // 405 is expected if the folder already exists
  407. if ((status >= 200 && status < 300) || status === 405) {
  408. self.trigger('createdfolder', fullPath);
  409. deferred.resolve();
  410. return;
  411. }
  412. OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: fullPath}));
  413. deferred.reject();
  414. });
  415. }, function() {
  416. deferred.reject();
  417. });
  418. }
  419. return promise;
  420. },
  421. /**
  422. * Submit the given uploads
  423. *
  424. * @param {Array} array of uploads to start
  425. */
  426. submitUploads: function(uploads) {
  427. var self = this;
  428. _.each(uploads, function(upload) {
  429. self._uploads[upload.data.uploadId] = upload;
  430. upload.submit();
  431. });
  432. },
  433. /**
  434. * Show conflict for the given file object
  435. *
  436. * @param {OC.FileUpload} file upload object
  437. */
  438. showConflict: function(fileUpload) {
  439. //show "file already exists" dialog
  440. var self = this;
  441. var file = fileUpload.getFile();
  442. // already attempted autorename but the server said the file exists ? (concurrently added)
  443. if (fileUpload.getConflictMode() === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
  444. // attempt another autorename, defer to let the current callback finish
  445. _.defer(function() {
  446. self.onAutorename(fileUpload);
  447. });
  448. return;
  449. }
  450. // retrieve more info about this file
  451. this.filesClient.getFileInfo(fileUpload.getFullFilePath()).then(function(status, fileInfo) {
  452. var original = fileInfo;
  453. var replacement = file;
  454. original.directory = original.path;
  455. OC.dialogs.fileexists(fileUpload, original, replacement, self);
  456. });
  457. },
  458. /**
  459. * cancels all uploads
  460. */
  461. cancelUploads:function() {
  462. this.log('canceling uploads');
  463. jQuery.each(this._uploads, function(i, upload) {
  464. upload.abort();
  465. });
  466. this.clear();
  467. },
  468. /**
  469. * Clear uploads
  470. */
  471. clear: function() {
  472. this._uploads = {};
  473. this._knownDirs = {};
  474. },
  475. /**
  476. * Returns an upload by id
  477. *
  478. * @param {int} data uploadId
  479. * @return {OC.FileUpload} file upload
  480. */
  481. getUpload: function(data) {
  482. if (_.isString(data)) {
  483. return this._uploads[data];
  484. } else if (data.uploadId) {
  485. return this._uploads[data.uploadId];
  486. }
  487. return null;
  488. },
  489. showUploadCancelMessage: _.debounce(function() {
  490. OC.Notification.showTemporary(t('files', 'Upload cancelled.'), {timeout: 10});
  491. }, 500),
  492. /**
  493. * callback for the conflicts dialog
  494. */
  495. onCancel:function() {
  496. this.cancelUploads();
  497. },
  498. /**
  499. * callback for the conflicts dialog
  500. * calls onSkip, onReplace or onAutorename for each conflict
  501. * @param {object} conflicts - list of conflict elements
  502. */
  503. onContinue:function(conflicts) {
  504. var self = this;
  505. //iterate over all conflicts
  506. jQuery.each(conflicts, function (i, conflict) {
  507. conflict = $(conflict);
  508. var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1;
  509. var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1;
  510. if (keepOriginal && keepReplacement) {
  511. // when both selected -> autorename
  512. self.onAutorename(conflict.data('data'));
  513. } else if (keepReplacement) {
  514. // when only replacement selected -> overwrite
  515. self.onReplace(conflict.data('data'));
  516. } else {
  517. // when only original seleted -> skip
  518. // when none selected -> skip
  519. self.onSkip(conflict.data('data'));
  520. }
  521. });
  522. },
  523. /**
  524. * handle skipping an upload
  525. * @param {OC.FileUpload} upload
  526. */
  527. onSkip:function(upload) {
  528. this.log('skip', null, upload);
  529. upload.deleteUpload();
  530. },
  531. /**
  532. * handle replacing a file on the server with an uploaded file
  533. * @param {FileUpload} data
  534. */
  535. onReplace:function(upload) {
  536. this.log('replace', null, upload);
  537. upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE);
  538. this.submitUploads([upload]);
  539. },
  540. /**
  541. * handle uploading a file and letting the server decide a new name
  542. * @param {object} upload
  543. */
  544. onAutorename:function(upload) {
  545. this.log('autorename', null, upload);
  546. upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME);
  547. do {
  548. upload.autoRename();
  549. // if file known to exist on the client side, retry
  550. } while (this.fileList && this.fileList.inList(upload.getFileName()));
  551. // resubmit upload
  552. this.submitUploads([upload]);
  553. },
  554. _trace:false, //TODO implement log handler for JS per class?
  555. log:function(caption, e, data) {
  556. if (this._trace) {
  557. console.log(caption);
  558. console.log(data);
  559. }
  560. },
  561. /**
  562. * checks the list of existing files prior to uploading and shows a simple dialog to choose
  563. * skip all, replace all or choose which files to keep
  564. *
  565. * @param {array} selection of files to upload
  566. * @param {object} callbacks - object with several callback methods
  567. * @param {function} callbacks.onNoConflicts
  568. * @param {function} callbacks.onSkipConflicts
  569. * @param {function} callbacks.onReplaceConflicts
  570. * @param {function} callbacks.onChooseConflicts
  571. * @param {function} callbacks.onCancel
  572. */
  573. checkExistingFiles: function (selection, callbacks) {
  574. var fileList = this.fileList;
  575. var conflicts = [];
  576. // only keep non-conflicting uploads
  577. selection.uploads = _.filter(selection.uploads, function(upload) {
  578. var file = upload.getFile();
  579. if (file.relativePath) {
  580. // can't check in subfolder contents
  581. return true;
  582. }
  583. if (!fileList) {
  584. // no list to check against
  585. return true;
  586. }
  587. var fileInfo = fileList.findFile(file.name);
  588. if (fileInfo) {
  589. conflicts.push([
  590. // original
  591. _.extend(fileInfo, {
  592. directory: fileInfo.directory || fileInfo.path || fileList.getCurrentDirectory()
  593. }),
  594. // replacement (File object)
  595. upload
  596. ]);
  597. return false;
  598. }
  599. return true;
  600. });
  601. if (conflicts.length) {
  602. // wait for template loading
  603. OC.dialogs.fileexists(null, null, null, this).done(function() {
  604. _.each(conflicts, function(conflictData) {
  605. OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this);
  606. });
  607. });
  608. }
  609. // upload non-conflicting files
  610. // note: when reaching the server they might still meet conflicts
  611. // if the folder was concurrently modified, these will get added
  612. // to the already visible dialog, if applicable
  613. callbacks.onNoConflicts(selection);
  614. },
  615. _hideProgressBar: function() {
  616. var self = this;
  617. $('#uploadprogresswrapper .stop').fadeOut();
  618. $('#uploadprogressbar').fadeOut(function() {
  619. self.$uploadEl.trigger(new $.Event('resized'));
  620. });
  621. },
  622. _showProgressBar: function() {
  623. $('#uploadprogressbar').fadeIn();
  624. this.$uploadEl.trigger(new $.Event('resized'));
  625. },
  626. /**
  627. * Returns whether the given file is known to be a received shared file
  628. *
  629. * @param {Object} file file
  630. * @return {bool} true if the file is a shared file
  631. */
  632. _isReceivedSharedFile: function(file) {
  633. if (!window.FileList) {
  634. return false;
  635. }
  636. var $tr = window.FileList.findFileEl(file.name);
  637. if (!$tr.length) {
  638. return false;
  639. }
  640. return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory');
  641. },
  642. /**
  643. * Initialize the upload object
  644. *
  645. * @param {Object} $uploadEl upload element
  646. * @param {Object} options
  647. * @param {OCA.Files.FileList} [options.fileList] file list object
  648. * @param {OC.Files.Client} [options.filesClient] files client object
  649. * @param {Object} [options.dropZone] drop zone for drag and drop upload
  650. */
  651. init: function($uploadEl, options) {
  652. var self = this;
  653. options = options || {};
  654. this.fileList = options.fileList;
  655. this.filesClient = options.filesClient || OC.Files.getClient();
  656. $uploadEl = $($uploadEl);
  657. this.$uploadEl = $uploadEl;
  658. if ($uploadEl.exists()) {
  659. $('#uploadprogresswrapper .stop').on('click', function() {
  660. self.cancelUploads();
  661. });
  662. this.fileUploadParam = {
  663. type: 'PUT',
  664. dropZone: options.dropZone, // restrict dropZone to content div
  665. autoUpload: false,
  666. sequentialUploads: true,
  667. //singleFileUploads is on by default, so the data.files array will always have length 1
  668. /**
  669. * on first add of every selection
  670. * - check all files of originalFiles array with files in dir
  671. * - on conflict show dialog
  672. * - skip all -> remember as single skip action for all conflicting files
  673. * - replace all -> remember as single replace action for all conflicting files
  674. * - choose -> show choose dialog
  675. * - mark files to keep
  676. * - when only existing -> remember as single skip action
  677. * - when only new -> remember as single replace action
  678. * - when both -> remember as single autorename action
  679. * - start uploading selection
  680. * @param {object} e
  681. * @param {object} data
  682. * @returns {boolean}
  683. */
  684. add: function(e, data) {
  685. self.log('add', e, data);
  686. var that = $(this), freeSpace;
  687. var upload = new OC.FileUpload(self, data);
  688. // can't link directly due to jQuery not liking cyclic deps on its ajax object
  689. data.uploadId = upload.getId();
  690. // we need to collect all data upload objects before
  691. // starting the upload so we can check their existence
  692. // and set individual conflict actions. Unfortunately,
  693. // there is only one variable that we can use to identify
  694. // the selection a data upload is part of, so we have to
  695. // collect them in data.originalFiles turning
  696. // singleFileUploads off is not an option because we want
  697. // to gracefully handle server errors like 'already exists'
  698. // create a container where we can store the data objects
  699. if ( ! data.originalFiles.selection ) {
  700. // initialize selection and remember number of files to upload
  701. data.originalFiles.selection = {
  702. uploads: [],
  703. filesToUpload: data.originalFiles.length,
  704. totalBytes: 0
  705. };
  706. }
  707. // TODO: move originalFiles to a separate container, maybe inside OC.Upload
  708. var selection = data.originalFiles.selection;
  709. // add uploads
  710. if ( selection.uploads.length < selection.filesToUpload ) {
  711. // remember upload
  712. selection.uploads.push(upload);
  713. }
  714. //examine file
  715. var file = upload.getFile();
  716. try {
  717. // FIXME: not so elegant... need to refactor that method to return a value
  718. Files.isFileNameValid(file.name);
  719. }
  720. catch (errorMessage) {
  721. data.textStatus = 'invalidcharacters';
  722. data.errorThrown = errorMessage;
  723. }
  724. if (data.targetDir) {
  725. upload.setTargetFolder(data.targetDir);
  726. delete data.targetDir;
  727. }
  728. // in case folder drag and drop is not supported file will point to a directory
  729. // http://stackoverflow.com/a/20448357
  730. if ( ! file.type && file.size % 4096 === 0 && file.size <= 102400) {
  731. var dirUploadFailure = false;
  732. try {
  733. var reader = new FileReader();
  734. reader.readAsBinaryString(file);
  735. } catch (NS_ERROR_FILE_ACCESS_DENIED) {
  736. //file is a directory
  737. dirUploadFailure = true;
  738. }
  739. if (dirUploadFailure) {
  740. data.textStatus = 'dirorzero';
  741. data.errorThrown = t('files',
  742. 'Unable to upload {filename} as it is a directory or has 0 bytes',
  743. {filename: file.name}
  744. );
  745. }
  746. }
  747. // only count if we're not overwriting an existing shared file
  748. if (self._isReceivedSharedFile(file)) {
  749. file.isReceivedShare = true;
  750. } else {
  751. // add size
  752. selection.totalBytes += file.size;
  753. }
  754. // check free space
  755. freeSpace = $('#free_space').val();
  756. if (freeSpace >= 0 && selection.totalBytes > freeSpace) {
  757. data.textStatus = 'notenoughspace';
  758. data.errorThrown = t('files',
  759. 'Not enough free space, you are uploading {size1} but only {size2} is left', {
  760. 'size1': humanFileSize(selection.totalBytes),
  761. 'size2': humanFileSize($('#free_space').val())
  762. });
  763. }
  764. // end upload for whole selection on error
  765. if (data.errorThrown) {
  766. // trigger fileupload fail handler
  767. var fu = that.data('blueimp-fileupload') || that.data('fileupload');
  768. fu._trigger('fail', e, data);
  769. return false; //don't upload anything
  770. }
  771. // check existing files when all is collected
  772. if ( selection.uploads.length >= selection.filesToUpload ) {
  773. //remove our selection hack:
  774. delete data.originalFiles.selection;
  775. var callbacks = {
  776. onNoConflicts: function (selection) {
  777. self.submitUploads(selection.uploads);
  778. },
  779. onSkipConflicts: function (selection) {
  780. //TODO mark conflicting files as toskip
  781. },
  782. onReplaceConflicts: function (selection) {
  783. //TODO mark conflicting files as toreplace
  784. },
  785. onChooseConflicts: function (selection) {
  786. //TODO mark conflicting files as chosen
  787. },
  788. onCancel: function (selection) {
  789. $.each(selection.uploads, function(i, upload) {
  790. upload.abort();
  791. });
  792. }
  793. };
  794. self.checkExistingFiles(selection, callbacks);
  795. }
  796. return true; // continue adding files
  797. },
  798. /**
  799. * called after the first add, does NOT have the data param
  800. * @param {object} e
  801. */
  802. start: function(e) {
  803. self.log('start', e, null);
  804. //hide the tooltip otherwise it covers the progress bar
  805. $('#upload').tooltip('hide');
  806. },
  807. fail: function(e, data) {
  808. var upload = self.getUpload(data);
  809. var status = null;
  810. if (upload) {
  811. status = upload.getResponseStatus();
  812. }
  813. self.log('fail', e, upload);
  814. if (data.textStatus === 'abort') {
  815. self.showUploadCancelMessage();
  816. } else if (status === 412) {
  817. // file already exists
  818. self.showConflict(upload);
  819. } else if (status === 404) {
  820. // target folder does not exist any more
  821. OC.Notification.showTemporary(
  822. t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()})
  823. );
  824. self.cancelUploads();
  825. } else if (status === 507) {
  826. // not enough space
  827. OC.Notification.showTemporary(
  828. t('files', 'Not enough free space')
  829. );
  830. self.cancelUploads();
  831. } else {
  832. // HTTP connection problem or other error
  833. OC.Notification.showTemporary(data.errorThrown, {timeout: 10});
  834. }
  835. if (upload) {
  836. upload.deleteUpload();
  837. }
  838. },
  839. /**
  840. * called for every successful upload
  841. * @param {object} e
  842. * @param {object} data
  843. */
  844. done:function(e, data) {
  845. var upload = self.getUpload(data);
  846. var that = $(this);
  847. self.log('done', e, upload);
  848. var status = upload.getResponseStatus();
  849. if (status < 200 || status >= 300) {
  850. // trigger fail handler
  851. var fu = that.data('blueimp-fileupload') || that.data('fileupload');
  852. fu._trigger('fail', e, data);
  853. return;
  854. }
  855. },
  856. /**
  857. * called after last upload
  858. * @param {object} e
  859. * @param {object} data
  860. */
  861. stop: function(e, data) {
  862. self.log('stop', e, data);
  863. }
  864. };
  865. // initialize jquery fileupload (blueimp)
  866. var fileupload = this.$uploadEl.fileupload(this.fileUploadParam);
  867. if (this._supportAjaxUploadWithProgress()) {
  868. //remaining time
  869. var lastUpdate = new Date().getMilliseconds();
  870. var lastSize = 0;
  871. var bufferSize = 20;
  872. var buffer = [];
  873. var bufferIndex = 0;
  874. var bufferTotal = 0;
  875. for(var i = 0; i < bufferSize;i++){
  876. buffer[i] = 0;
  877. }
  878. // add progress handlers
  879. fileupload.on('fileuploadadd', function(e, data) {
  880. self.log('progress handle fileuploadadd', e, data);
  881. self.trigger('add', e, data);
  882. });
  883. // add progress handlers
  884. fileupload.on('fileuploadstart', function(e, data) {
  885. self.log('progress handle fileuploadstart', e, data);
  886. $('#uploadprogresswrapper .stop').show();
  887. $('#uploadprogresswrapper .label').show();
  888. $('#uploadprogressbar').progressbar({value: 0});
  889. $('#uploadprogressbar .ui-progressbar-value').
  890. html('<em class="label inner"><span class="desktop">'
  891. + t('files', 'Uploading...')
  892. + '</span><span class="mobile">'
  893. + t('files', '...')
  894. + '</span></em>');
  895. $('#uploadprogressbar').tooltip({placement: 'bottom'});
  896. self._showProgressBar();
  897. self.trigger('start', e, data);
  898. });
  899. fileupload.on('fileuploadprogress', function(e, data) {
  900. self.log('progress handle fileuploadprogress', e, data);
  901. //TODO progressbar in row
  902. self.trigger('progress', e, data);
  903. });
  904. fileupload.on('fileuploadprogressall', function(e, data) {
  905. self.log('progress handle fileuploadprogressall', e, data);
  906. var progress = (data.loaded / data.total) * 100;
  907. var thisUpdate = new Date().getMilliseconds();
  908. var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s
  909. lastUpdate = thisUpdate;
  910. var diffSize = data.loaded - lastSize;
  911. lastSize = data.loaded;
  912. diffSize = diffSize / diffUpdate; // apply timing factor, eg. 1mb/2s = 0.5mb/s
  913. var remainingSeconds = ((data.total - data.loaded) / diffSize);
  914. if(remainingSeconds >= 0) {
  915. bufferTotal = bufferTotal - (buffer[bufferIndex]) + remainingSeconds;
  916. buffer[bufferIndex] = remainingSeconds; //buffer to make it smoother
  917. bufferIndex = (bufferIndex + 1) % bufferSize;
  918. }
  919. var smoothRemainingSeconds = (bufferTotal / bufferSize); //seconds
  920. var h = moment.duration(smoothRemainingSeconds, "seconds").humanize();
  921. $('#uploadprogressbar .label .mobile').text(h);
  922. $('#uploadprogressbar .label .desktop').text(h);
  923. $('#uploadprogressbar').attr('original-title',
  924. t('files', '{loadedSize} of {totalSize} ({bitrate})' , {
  925. loadedSize: humanFileSize(data.loaded),
  926. totalSize: humanFileSize(data.total),
  927. bitrate: humanFileSize(data.bitrate) + '/s'
  928. })
  929. );
  930. $('#uploadprogressbar').progressbar('value', progress);
  931. self.trigger('progressall', e, data);
  932. });
  933. fileupload.on('fileuploadstop', function(e, data) {
  934. self.log('progress handle fileuploadstop', e, data);
  935. self.clear();
  936. self._hideProgressBar();
  937. self.trigger('stop', e, data);
  938. });
  939. fileupload.on('fileuploadfail', function(e, data) {
  940. self.log('progress handle fileuploadfail', e, data);
  941. //if user pressed cancel hide upload progress bar and cancel button
  942. if (data.errorThrown === 'abort') {
  943. self._hideProgressBar();
  944. }
  945. self.trigger('fail', e, data);
  946. });
  947. var disableDropState = function() {
  948. $('#app-content').removeClass('file-drag');
  949. $('.dropping-to-dir').removeClass('dropping-to-dir');
  950. $('.dir-drop').removeClass('dir-drop');
  951. $('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
  952. };
  953. var disableClassOnFirefox = _.debounce(function() {
  954. disableDropState();
  955. }, 100);
  956. fileupload.on('fileuploaddragover', function(e){
  957. $('#app-content').addClass('file-drag');
  958. // dropping a folder in firefox doesn't cause a drop event
  959. // this is simulated by simply invoke disabling all classes
  960. // once no dragover event isn't noticed anymore
  961. if ($.browser['mozilla']) {
  962. disableClassOnFirefox();
  963. }
  964. $('#emptycontent .icon-folder').addClass('icon-filetype-folder-drag-accept');
  965. var filerow = $(e.delegatedEvent.target).closest('tr');
  966. if(!filerow.hasClass('dropping-to-dir')){
  967. $('.dropping-to-dir .icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
  968. $('.dropping-to-dir').removeClass('dropping-to-dir');
  969. $('.dir-drop').removeClass('dir-drop');
  970. }
  971. if(filerow.attr('data-type') === 'dir'){
  972. $('#app-content').addClass('dir-drop');
  973. filerow.addClass('dropping-to-dir');
  974. filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept');
  975. }
  976. });
  977. fileupload.on('fileuploaddragleave fileuploaddrop', function (){
  978. $('#app-content').removeClass('file-drag');
  979. $('.dropping-to-dir').removeClass('dropping-to-dir');
  980. $('.dir-drop').removeClass('dir-drop');
  981. $('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
  982. });
  983. fileupload.on('fileuploadchunksend', function(e, data) {
  984. // modify the request to adjust it to our own chunking
  985. var upload = self.getUpload(data);
  986. var range = data.contentRange.split(' ')[1];
  987. var chunkId = range.split('/')[0];
  988. data.url = OC.getRootPath() +
  989. '/remote.php/dav/uploads' +
  990. '/' + encodeURIComponent(OC.getCurrentUser().uid) +
  991. '/' + encodeURIComponent(upload.getId()) +
  992. '/' + encodeURIComponent(chunkId);
  993. delete data.contentRange;
  994. delete data.headers['Content-Range'];
  995. });
  996. fileupload.on('fileuploaddone', function(e, data) {
  997. var upload = self.getUpload(data);
  998. upload.done().then(function() {
  999. self.trigger('done', e, upload);
  1000. });
  1001. });
  1002. fileupload.on('fileuploaddrop', function(e, data) {
  1003. self.trigger('drop', e, data);
  1004. });
  1005. }
  1006. }
  1007. //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used)
  1008. if (navigator.userAgent.search(/konqueror/i) === -1) {
  1009. this.$uploadEl.attr('multiple', 'multiple');
  1010. }
  1011. return this.fileUploadParam;
  1012. }
  1013. }, OC.Backbone.Events);