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

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