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.

file-upload.js 32KB

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