files.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  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. /* global getURLParameter */
  11. /**
  12. * Utility class for file related operations
  13. */
  14. (function() {
  15. var Files = {
  16. // file space size sync
  17. _updateStorageStatistics: function(currentDir) {
  18. var state = Files.updateStorageStatistics;
  19. if (state.dir){
  20. if (state.dir === currentDir) {
  21. return;
  22. }
  23. // cancel previous call, as it was for another dir
  24. state.call.abort();
  25. }
  26. state.dir = currentDir;
  27. state.call = $.getJSON(OC.generateUrl('apps/files/api/v1/stats?dir={dir}', {
  28. dir: currentDir,
  29. }), function(response) {
  30. state.dir = null;
  31. state.call = null;
  32. Files.updateMaxUploadFilesize(response);
  33. });
  34. },
  35. // update quota
  36. updateStorageQuotas: function() {
  37. Files._updateStorageQuotasThrottled();
  38. },
  39. _updateStorageQuotas: function() {
  40. var state = Files.updateStorageQuotas;
  41. state.call = $.getJSON(OC.generateUrl('apps/files/api/v1/stats'), function(response) {
  42. Files.updateQuota(response);
  43. });
  44. },
  45. /**
  46. * Update storage statistics such as free space, max upload,
  47. * etc based on the given directory.
  48. *
  49. * Note this function is debounced to avoid making too
  50. * many ajax calls in a row.
  51. *
  52. * @param dir directory
  53. * @param force whether to force retrieving
  54. */
  55. updateStorageStatistics: function(dir, force) {
  56. if (!OC.currentUser) {
  57. return;
  58. }
  59. if (force) {
  60. Files._updateStorageStatistics(dir);
  61. }
  62. else {
  63. Files._updateStorageStatisticsDebounced(dir);
  64. }
  65. },
  66. updateMaxUploadFilesize:function(response) {
  67. if (response === undefined) {
  68. return;
  69. }
  70. if (response.data !== undefined && response.data.free !== undefined) {
  71. $('#free_space').val(response.data.free);
  72. OCA.Files.App.fileList._updateDirectoryPermissions();
  73. }
  74. if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) {
  75. $('#upload.button').attr('title', response.data.maxHumanFilesize);
  76. $('#usedSpacePercent').val(response.data.usedSpacePercent);
  77. $('#usedSpacePercent').data('mount-type', response.data.mountType);
  78. $('#usedSpacePercent').data('mount-point', response.data.mountPoint);
  79. $('#owner').val(response.data.owner);
  80. $('#ownerDisplayName').val(response.data.ownerDisplayName);
  81. Files.displayStorageWarnings();
  82. OCA.Files.App.fileList._updateDirectoryPermissions();
  83. }
  84. if (response[0] === undefined) {
  85. return;
  86. }
  87. if (response[0].uploadMaxFilesize !== undefined) {
  88. $('#upload.button').attr('title', response[0].maxHumanFilesize);
  89. $('#usedSpacePercent').val(response[0].usedSpacePercent);
  90. Files.displayStorageWarnings();
  91. }
  92. },
  93. updateQuota:function(response) {
  94. if (response === undefined) {
  95. return;
  96. }
  97. if (response.data !== undefined
  98. && response.data.quota !== undefined
  99. && response.data.total !== undefined
  100. && response.data.used !== undefined
  101. && response.data.usedSpacePercent !== undefined) {
  102. var humanUsed = OC.Util.humanFileSize(response.data.used, true);
  103. var humanTotal = OC.Util.humanFileSize(response.data.total, true);
  104. if (response.data.quota > 0) {
  105. $('#quota').attr('title', t('files', '{used}%', {used: Math.round(response.data.usedSpacePercent)}));
  106. $('#quota progress').val(response.data.usedSpacePercent);
  107. $('#quotatext').html(t('files', '{used} of {quota} used', {used: humanUsed, quota: humanTotal}));
  108. } else {
  109. $('#quotatext').html(t('files', '{used} used', {used: humanUsed}));
  110. }
  111. if (response.data.usedSpacePercent > 80) {
  112. $('#quota progress').addClass('warn');
  113. } else {
  114. $('#quota progress').removeClass('warn');
  115. }
  116. }
  117. },
  118. /**
  119. * Fix path name by removing double slash at the beginning, if any
  120. */
  121. fixPath: function(fileName) {
  122. if (fileName.substr(0, 2) == '//') {
  123. return fileName.substr(1);
  124. }
  125. return fileName;
  126. },
  127. /**
  128. * Checks whether the given file name is valid.
  129. * @param name file name to check
  130. * @return true if the file name is valid.
  131. * Throws a string exception with an error message if
  132. * the file name is not valid
  133. *
  134. * NOTE: This function is duplicated in the filepicker inside core/src/OC/dialogs.js
  135. */
  136. isFileNameValid: function (name) {
  137. var trimmedName = name.trim();
  138. if (trimmedName === '.' || trimmedName === '..')
  139. {
  140. throw t('files', '"{name}" is an invalid file name.', {name: name});
  141. } else if (trimmedName.length === 0) {
  142. throw t('files', 'File name cannot be empty.');
  143. } else if (trimmedName.indexOf('/') !== -1) {
  144. throw t('files', '"/" is not allowed inside a file name.');
  145. } else if (!!(trimmedName.match(OC.config.blacklist_files_regex))) {
  146. throw t('files', '"{name}" is not an allowed filetype', {name: name});
  147. }
  148. return true;
  149. },
  150. displayStorageWarnings: function() {
  151. if (!OC.Notification.isHidden()) {
  152. return;
  153. }
  154. var usedSpacePercent = $('#usedSpacePercent').val(),
  155. owner = $('#owner').val(),
  156. ownerDisplayName = $('#ownerDisplayName').val(),
  157. mountType = $('#usedSpacePercent').data('mount-type'),
  158. mountPoint = $('#usedSpacePercent').data('mount-point');
  159. if (usedSpacePercent > 98) {
  160. if (owner !== OC.getCurrentUser().uid) {
  161. OC.Notification.show(t('files', 'Storage of {owner} is full, files cannot be updated or synced anymore!',
  162. {owner: ownerDisplayName}), {type: 'error'}
  163. );
  164. } else if (mountType === 'group') {
  165. OC.Notification.show(t('files',
  166. 'Group folder "{mountPoint}" is full, files cannot be updated or synced anymore!',
  167. {mountPoint: mountPoint}),
  168. {type: 'error'}
  169. );
  170. } else if (mountType === 'external') {
  171. OC.Notification.show(t('files',
  172. 'External storage "{mountPoint}" is full, files cannot be updated or synced anymore!',
  173. {mountPoint: mountPoint}),
  174. {type : 'error'}
  175. );
  176. } else {
  177. OC.Notification.show(t('files',
  178. 'Your storage is full, files cannot be updated or synced anymore!'),
  179. {type: 'error'}
  180. );
  181. }
  182. } else if (usedSpacePercent > 90) {
  183. if (owner !== OC.getCurrentUser().uid) {
  184. OC.Notification.show(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%).',
  185. {
  186. usedSpacePercent: usedSpacePercent,
  187. owner: ownerDisplayName
  188. }),
  189. {
  190. type: 'error'
  191. }
  192. );
  193. } else if (mountType === 'group') {
  194. OC.Notification.show(t('files',
  195. 'Group folder "{mountPoint}" is almost full ({usedSpacePercent}%).',
  196. {mountPoint: mountPoint, usedSpacePercent: usedSpacePercent}),
  197. {type : 'error'}
  198. );
  199. } else if (mountType === 'external') {
  200. OC.Notification.show(t('files',
  201. 'External storage "{mountPoint}" is almost full ({usedSpacePercent}%).',
  202. {mountPoint: mountPoint, usedSpacePercent: usedSpacePercent}),
  203. {type : 'error'}
  204. );
  205. } else {
  206. OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%).',
  207. {usedSpacePercent: usedSpacePercent}),
  208. {type : 'error'}
  209. );
  210. }
  211. }
  212. },
  213. /**
  214. * Returns the download URL of the given file(s)
  215. * @param {string} filename string or array of file names to download
  216. * @param {string} [dir] optional directory in which the file name is, defaults to the current directory
  217. * @param {boolean} [isDir=false] whether the given filename is a directory and might need a special URL
  218. */
  219. getDownloadUrl: function(filename, dir, isDir) {
  220. if (!_.isArray(filename) && !isDir) {
  221. var pathSections = dir.split('/');
  222. pathSections.push(filename);
  223. var encodedPath = '';
  224. _.each(pathSections, function(section) {
  225. if (section !== '') {
  226. encodedPath += '/' + encodeURIComponent(section);
  227. }
  228. });
  229. return OC.linkToRemoteBase('webdav') + encodedPath;
  230. }
  231. if (_.isArray(filename)) {
  232. filename = JSON.stringify(filename);
  233. }
  234. var params = {
  235. dir: dir,
  236. files: filename
  237. };
  238. return this.getAjaxUrl('download', params);
  239. },
  240. /**
  241. * Returns the ajax URL for a given action
  242. * @param action action string
  243. * @param params optional params map
  244. */
  245. getAjaxUrl: function(action, params) {
  246. var q = '';
  247. if (params) {
  248. q = '?' + OC.buildQueryString(params);
  249. }
  250. return OC.filePath('files', 'ajax', action + '.php') + q;
  251. },
  252. /**
  253. * Fetch the icon url for the mimetype
  254. * @param {string} mime The mimetype
  255. * @param {Files~mimeicon} ready Function to call when mimetype is retrieved
  256. * @deprecated use OC.MimeType.getIconUrl(mime)
  257. */
  258. getMimeIcon: function(mime, ready) {
  259. ready(OC.MimeType.getIconUrl(mime));
  260. },
  261. /**
  262. * Generates a preview URL based on the URL space.
  263. * @param urlSpec attributes for the URL
  264. * @param {number} urlSpec.x width
  265. * @param {number} urlSpec.y height
  266. * @param {String} urlSpec.file path to the file
  267. * @return preview URL
  268. * @deprecated used OCA.Files.FileList.generatePreviewUrl instead
  269. */
  270. generatePreviewUrl: function(urlSpec) {
  271. console.warn('DEPRECATED: please use generatePreviewUrl() from an OCA.Files.FileList instance');
  272. return OCA.Files.App.fileList.generatePreviewUrl(urlSpec);
  273. },
  274. /**
  275. * Lazy load preview
  276. * @deprecated used OCA.Files.FileList.lazyLoadPreview instead
  277. */
  278. lazyLoadPreview : function(path, mime, ready, width, height, etag) {
  279. console.warn('DEPRECATED: please use lazyLoadPreview() from an OCA.Files.FileList instance');
  280. return FileList.lazyLoadPreview({
  281. path: path,
  282. mime: mime,
  283. callback: ready,
  284. width: width,
  285. height: height,
  286. etag: etag
  287. });
  288. },
  289. /**
  290. * Initialize the files view
  291. */
  292. initialize: function() {
  293. Files.bindKeyboardShortcuts(document, $);
  294. // drag&drop support using jquery.fileupload
  295. // TODO use OC.dialogs
  296. $(document).bind('drop dragover', function (e) {
  297. e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone
  298. });
  299. // display storage warnings
  300. setTimeout(Files.displayStorageWarnings, 100);
  301. // only possible at the moment if user is logged in or the files app is loaded
  302. if (OC.currentUser && OCA.Files.App && OC.config.session_keepalive) {
  303. // start on load - we ask the server every 5 minutes
  304. var func = _.bind(OCA.Files.App.fileList.updateStorageStatistics, OCA.Files.App.fileList);
  305. var updateStorageStatisticsInterval = 5*60*1000;
  306. var updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval);
  307. // TODO: this should also stop when switching to another view
  308. // Use jquery-visibility to de-/re-activate file stats sync
  309. if ($.support.pageVisibility) {
  310. $(document).on({
  311. 'show': function() {
  312. if (!updateStorageStatisticsIntervalId) {
  313. updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval);
  314. }
  315. },
  316. 'hide': function() {
  317. clearInterval(updateStorageStatisticsIntervalId);
  318. updateStorageStatisticsIntervalId = 0;
  319. }
  320. });
  321. }
  322. }
  323. $('#webdavurl').on('click touchstart', function () {
  324. this.focus();
  325. this.setSelectionRange(0, this.value.length);
  326. });
  327. //FIXME scroll to and highlight preselected file
  328. /*
  329. if (getURLParameter('scrollto')) {
  330. FileList.scrollTo(getURLParameter('scrollto'));
  331. }
  332. */
  333. },
  334. /**
  335. * Handles the download and calls the callback function once the download has started
  336. * - browser sends download request and adds parameter with a token
  337. * - server notices this token and adds a set cookie to the download response
  338. * - browser now adds this cookie for the domain
  339. * - JS periodically checks for this cookie and then knows when the download has started to call the callback
  340. *
  341. * @param {string} url download URL
  342. * @param {Function} callback function to call once the download has started
  343. */
  344. handleDownload: function(url, callback) {
  345. var randomToken = Math.random().toString(36).substring(2),
  346. checkForDownloadCookie = function() {
  347. if (!OC.Util.isCookieSetToValue('ocDownloadStarted', randomToken)){
  348. return false;
  349. } else {
  350. callback();
  351. return true;
  352. }
  353. };
  354. if (url.indexOf('?') >= 0) {
  355. url += '&';
  356. } else {
  357. url += '?';
  358. }
  359. OC.redirect(url + 'downloadStartSecret=' + randomToken);
  360. OC.Util.waitFor(checkForDownloadCookie, 500);
  361. }
  362. };
  363. Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250);
  364. Files._updateStorageQuotasThrottled = _.throttle(Files._updateStorageQuotas, 30000);
  365. OCA.Files.Files = Files;
  366. })();
  367. // TODO: move to FileList
  368. var createDragShadow = function(event) {
  369. // FIXME: inject file list instance somehow
  370. /* global FileList, Files */
  371. //select dragged file
  372. var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked');
  373. if (!isDragSelected) {
  374. //select dragged file
  375. FileList._selectFileEl($(event.target).parents('tr:first'), true, false);
  376. }
  377. // do not show drag shadow for too many files
  378. var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize());
  379. selectedFiles = _.sortBy(selectedFiles, FileList._fileInfoCompare);
  380. if (!isDragSelected && selectedFiles.length === 1) {
  381. //revert the selection
  382. FileList._selectFileEl($(event.target).parents('tr:first'), false, false);
  383. }
  384. // build dragshadow
  385. var dragshadow = $('<table class="dragshadow"></table>');
  386. var tbody = $('<tbody></tbody>');
  387. dragshadow.append(tbody);
  388. var dir = FileList.getCurrentDirectory();
  389. $(selectedFiles).each(function(i,elem) {
  390. // TODO: refactor this with the table row creation code
  391. var newtr = $('<tr></tr>')
  392. .attr('data-dir', dir)
  393. .attr('data-file', elem.name)
  394. .attr('data-origin', elem.origin);
  395. newtr.append($('<td class="filename"></td>').text(elem.name).css('background-size', 32));
  396. newtr.append($('<td class="size"></td>').text(OC.Util.humanFileSize(elem.size)));
  397. tbody.append(newtr);
  398. if (elem.type === 'dir') {
  399. newtr.find('td.filename')
  400. .css('background-image', 'url(' + OC.MimeType.getIconUrl('folder') + ')');
  401. } else {
  402. var path = dir + '/' + elem.name;
  403. Files.lazyLoadPreview(path, elem.mimetype, function(previewpath) {
  404. newtr.find('td.filename')
  405. .css('background-image', 'url(' + previewpath + ')');
  406. }, null, null, elem.etag);
  407. }
  408. });
  409. return dragshadow;
  410. };
  411. //options for file drag/drop
  412. //start&stop handlers needs some cleaning up
  413. // TODO: move to FileList class
  414. var dragOptions={
  415. revert: 'invalid',
  416. revertDuration: 300,
  417. opacity: 0.7,
  418. cursorAt: { left: 24, top: 18 },
  419. helper: createDragShadow,
  420. cursor: 'move',
  421. start: function(event, ui){
  422. var $selectedFiles = $('td.filename input:checkbox:checked');
  423. if (!$selectedFiles.length) {
  424. $selectedFiles = $(this);
  425. }
  426. $selectedFiles.closest('tr').addClass('animate-opacity dragging');
  427. $selectedFiles.closest('tr').filter('.ui-droppable').droppable( 'disable' );
  428. // Show breadcrumbs menu
  429. $('.crumbmenu').addClass('canDropChildren');
  430. },
  431. stop: function(event, ui) {
  432. var $selectedFiles = $('td.filename input:checkbox:checked');
  433. if (!$selectedFiles.length) {
  434. $selectedFiles = $(this);
  435. }
  436. var $tr = $selectedFiles.closest('tr');
  437. $tr.removeClass('dragging');
  438. $tr.filter('.ui-droppable').droppable( 'enable' );
  439. setTimeout(function() {
  440. $tr.removeClass('animate-opacity');
  441. }, 300);
  442. // Hide breadcrumbs menu
  443. $('.crumbmenu').removeClass('canDropChildren');
  444. },
  445. drag: function(event, ui) {
  446. // Prevent scrolling when hovering .files-controls
  447. if ($(event.originalEvent.target).parents('.files-controls').length > 0) {
  448. return
  449. }
  450. /** @type {JQuery<HTMLDivElement>} */
  451. const scrollingArea = FileList.$container;
  452. // Get the top and bottom scroll trigger y positions
  453. const containerHeight = scrollingArea.innerHeight() ?? 0
  454. const scrollTriggerArea = Math.min(Math.floor(containerHeight / 2), 100);
  455. const bottomTriggerY = containerHeight - scrollTriggerArea;
  456. const topTriggerY = scrollTriggerArea;
  457. // Get the cursor position relative to the container
  458. const containerOffset = scrollingArea.offset() ?? {left: 0, top: 0}
  459. const cursorPositionY = event.pageY - containerOffset.top
  460. const currentScrollTop = scrollingArea.scrollTop() ?? 0
  461. if (cursorPositionY < topTriggerY) {
  462. scrollingArea.scrollTop(currentScrollTop - 10)
  463. } else if (cursorPositionY > bottomTriggerY) {
  464. scrollingArea.scrollTop(currentScrollTop + 10)
  465. }
  466. }
  467. };
  468. // sane browsers support using the distance option
  469. if ( $('html.ie').length === 0) {
  470. dragOptions['distance'] = 20;
  471. }
  472. // TODO: move to FileList class
  473. var folderDropOptions = {
  474. hoverClass: "canDrop",
  475. drop: function( event, ui ) {
  476. // don't allow moving a file into a selected folder
  477. /* global FileList */
  478. if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) {
  479. return false;
  480. }
  481. var $tr = $(this).closest('tr');
  482. if (($tr.data('permissions') & OC.PERMISSION_CREATE) === 0) {
  483. FileList._showPermissionDeniedNotification();
  484. return false;
  485. }
  486. var targetPath = FileList.getCurrentDirectory() + '/' + $tr.data('file');
  487. var files = FileList.getSelectedFiles();
  488. if (files.length === 0) {
  489. // single one selected without checkbox?
  490. files = _.map(ui.helper.find('tr'), function(el) {
  491. return FileList.elementToFile($(el));
  492. });
  493. }
  494. FileList.move(_.pluck(files, 'name'), targetPath);
  495. },
  496. tolerance: 'pointer'
  497. };
  498. // for backward compatibility
  499. window.Files = OCA.Files.Files;