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 36KB

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