files.js 17 KB

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