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.

files.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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 OC, t, FileList */
  11. /* global getURLParameter */
  12. var Files = {
  13. // file space size sync
  14. _updateStorageStatistics: function() {
  15. Files._updateStorageStatisticsTimeout = null;
  16. var currentDir = FileList.getCurrentDirectory(),
  17. state = Files.updateStorageStatistics;
  18. if (state.dir){
  19. if (state.dir === currentDir) {
  20. return;
  21. }
  22. // cancel previous call, as it was for another dir
  23. state.call.abort();
  24. }
  25. state.dir = currentDir;
  26. state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php') + '?dir=' + encodeURIComponent(currentDir),function(response) {
  27. state.dir = null;
  28. state.call = null;
  29. Files.updateMaxUploadFilesize(response);
  30. });
  31. },
  32. updateStorageStatistics: function(force) {
  33. if (!OC.currentUser) {
  34. return;
  35. }
  36. // debounce to prevent calling too often
  37. if (Files._updateStorageStatisticsTimeout) {
  38. clearTimeout(Files._updateStorageStatisticsTimeout);
  39. }
  40. if (force) {
  41. Files._updateStorageStatistics();
  42. }
  43. else {
  44. Files._updateStorageStatisticsTimeout = setTimeout(Files._updateStorageStatistics, 250);
  45. }
  46. },
  47. updateMaxUploadFilesize:function(response) {
  48. if (response === undefined) {
  49. return;
  50. }
  51. if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) {
  52. $('#max_upload').val(response.data.uploadMaxFilesize);
  53. $('#free_space').val(response.data.freeSpace);
  54. $('#upload.button').attr('original-title', response.data.maxHumanFilesize);
  55. $('#usedSpacePercent').val(response.data.usedSpacePercent);
  56. Files.displayStorageWarnings();
  57. }
  58. if (response[0] === undefined) {
  59. return;
  60. }
  61. if (response[0].uploadMaxFilesize !== undefined) {
  62. $('#max_upload').val(response[0].uploadMaxFilesize);
  63. $('#upload.button').attr('original-title', response[0].maxHumanFilesize);
  64. $('#usedSpacePercent').val(response[0].usedSpacePercent);
  65. Files.displayStorageWarnings();
  66. }
  67. },
  68. /**
  69. * Fix path name by removing double slash at the beginning, if any
  70. */
  71. fixPath: function(fileName) {
  72. if (fileName.substr(0, 2) == '//') {
  73. return fileName.substr(1);
  74. }
  75. return fileName;
  76. },
  77. /**
  78. * Checks whether the given file name is valid.
  79. * @param name file name to check
  80. * @return true if the file name is valid.
  81. * Throws a string exception with an error message if
  82. * the file name is not valid
  83. */
  84. isFileNameValid: function (name) {
  85. var trimmedName = name.trim();
  86. if (trimmedName === '.' || trimmedName === '..')
  87. {
  88. throw t('files', '"{name}" is an invalid file name.', {name: name});
  89. } else if (trimmedName.length === 0) {
  90. throw t('files', 'File name cannot be empty.');
  91. }
  92. // check for invalid characters
  93. var invalidCharacters =
  94. ['\\', '/', '<', '>', ':', '"', '|', '?', '*', '\n'];
  95. for (var i = 0; i < invalidCharacters.length; i++) {
  96. if (trimmedName.indexOf(invalidCharacters[i]) !== -1) {
  97. throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed.");
  98. }
  99. }
  100. return true;
  101. },
  102. displayStorageWarnings: function() {
  103. if (!OC.Notification.isHidden()) {
  104. return;
  105. }
  106. var usedSpacePercent = $('#usedSpacePercent').val();
  107. if (usedSpacePercent > 98) {
  108. OC.Notification.show(t('files', 'Your storage is full, files can not be updated or synced anymore!'));
  109. return;
  110. }
  111. if (usedSpacePercent > 90) {
  112. OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)',
  113. {usedSpacePercent: usedSpacePercent}));
  114. }
  115. },
  116. displayEncryptionWarning: function() {
  117. if (!OC.Notification.isHidden()) {
  118. return;
  119. }
  120. var encryptedFiles = $('#encryptedFiles').val();
  121. var initStatus = $('#encryptionInitStatus').val();
  122. if (initStatus === '0') { // enc not initialized, but should be
  123. OC.Notification.show(t('files', 'Encryption App is enabled but your keys are not initialized, please log-out and log-in again'));
  124. return;
  125. }
  126. if (initStatus === '1') { // encryption tried to init but failed
  127. OC.Notification.show(t('files', 'Invalid private key for Encryption App. Please update your private key password in your personal settings to recover access to your encrypted files.'));
  128. return;
  129. }
  130. if (encryptedFiles === '1') {
  131. OC.Notification.show(t('files', 'Encryption was disabled but your files are still encrypted. Please go to your personal settings to decrypt your files.'));
  132. return;
  133. }
  134. },
  135. // TODO: move to FileList class
  136. setupDragAndDrop: function() {
  137. var $fileList = $('#fileList');
  138. //drag/drop of files
  139. $fileList.find('tr td.filename').each(function(i,e) {
  140. if ($(e).parent().data('permissions') & OC.PERMISSION_DELETE) {
  141. $(e).draggable(dragOptions);
  142. }
  143. });
  144. $fileList.find('tr[data-type="dir"] td.filename').each(function(i,e) {
  145. if ($(e).parent().data('permissions') & OC.PERMISSION_CREATE) {
  146. $(e).droppable(folderDropOptions);
  147. }
  148. });
  149. },
  150. /**
  151. * Returns the download URL of the given file(s)
  152. * @param filename string or array of file names to download
  153. * @param dir optional directory in which the file name is, defaults to the current directory
  154. */
  155. getDownloadUrl: function(filename, dir) {
  156. if ($.isArray(filename)) {
  157. filename = JSON.stringify(filename);
  158. }
  159. var params = {
  160. dir: dir || FileList.getCurrentDirectory(),
  161. files: filename
  162. };
  163. return this.getAjaxUrl('download', params);
  164. },
  165. /**
  166. * Returns the ajax URL for a given action
  167. * @param action action string
  168. * @param params optional params map
  169. */
  170. getAjaxUrl: function(action, params) {
  171. var q = '';
  172. if (params) {
  173. q = '?' + OC.buildQueryString(params);
  174. }
  175. return OC.filePath('files', 'ajax', action + '.php') + q;
  176. }
  177. };
  178. $(document).ready(function() {
  179. // FIXME: workaround for trashbin app
  180. if (window.trashBinApp) {
  181. return;
  182. }
  183. Files.displayEncryptionWarning();
  184. Files.bindKeyboardShortcuts(document, jQuery);
  185. Files.setupDragAndDrop();
  186. $('#file_action_panel').attr('activeAction', false);
  187. // Triggers invisible file input
  188. $('#upload a').on('click', function() {
  189. $(this).parent().children('#file_upload_start').trigger('click');
  190. return false;
  191. });
  192. // Trigger cancelling of file upload
  193. $('#uploadprogresswrapper .stop').on('click', function() {
  194. OC.Upload.cancelUploads();
  195. FileList.updateSelectionSummary();
  196. });
  197. // Show trash bin
  198. $('#trash').on('click', function() {
  199. window.location=OC.filePath('files_trashbin', '', 'index.php');
  200. });
  201. // drag&drop support using jquery.fileupload
  202. // TODO use OC.dialogs
  203. $(document).bind('drop dragover', function (e) {
  204. e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone
  205. });
  206. //do a background scan if needed
  207. scanFiles();
  208. // display storage warnings
  209. setTimeout(Files.displayStorageWarnings, 100);
  210. OC.Notification.setDefault(Files.displayStorageWarnings);
  211. // only possible at the moment if user is logged in
  212. if (OC.currentUser) {
  213. // start on load - we ask the server every 5 minutes
  214. var updateStorageStatisticsInterval = 5*60*1000;
  215. var updateStorageStatisticsIntervalId = setInterval(Files.updateStorageStatistics, updateStorageStatisticsInterval);
  216. // Use jquery-visibility to de-/re-activate file stats sync
  217. if ($.support.pageVisibility) {
  218. $(document).on({
  219. 'show.visibility': function() {
  220. if (!updateStorageStatisticsIntervalId) {
  221. updateStorageStatisticsIntervalId = setInterval(Files.updateStorageStatistics, updateStorageStatisticsInterval);
  222. }
  223. },
  224. 'hide.visibility': function() {
  225. clearInterval(updateStorageStatisticsIntervalId);
  226. updateStorageStatisticsIntervalId = 0;
  227. }
  228. });
  229. }
  230. }
  231. //scroll to and highlight preselected file
  232. if (getURLParameter('scrollto')) {
  233. FileList.scrollTo(getURLParameter('scrollto'));
  234. }
  235. });
  236. function scanFiles(force, dir, users) {
  237. if (!OC.currentUser) {
  238. return;
  239. }
  240. if (!dir) {
  241. dir = '';
  242. }
  243. force = !!force; //cast to bool
  244. scanFiles.scanning = true;
  245. var scannerEventSource;
  246. if (users) {
  247. var usersString;
  248. if (users === 'all') {
  249. usersString = users;
  250. } else {
  251. usersString = JSON.stringify(users);
  252. }
  253. scannerEventSource = new OC.EventSource(OC.filePath('files','ajax','scan.php'),{force: force,dir: dir, users: usersString});
  254. } else {
  255. scannerEventSource = new OC.EventSource(OC.filePath('files','ajax','scan.php'),{force: force,dir: dir});
  256. }
  257. scanFiles.cancel = scannerEventSource.close.bind(scannerEventSource);
  258. scannerEventSource.listen('count',function(count) {
  259. console.log(count + ' files scanned');
  260. });
  261. scannerEventSource.listen('folder',function(path) {
  262. console.log('now scanning ' + path);
  263. });
  264. scannerEventSource.listen('done',function(count) {
  265. scanFiles.scanning=false;
  266. console.log('done after ' + count + ' files');
  267. Files.updateStorageStatistics();
  268. });
  269. scannerEventSource.listen('user',function(user) {
  270. console.log('scanning files for ' + user);
  271. });
  272. }
  273. scanFiles.scanning=false;
  274. // TODO: move to FileList
  275. var createDragShadow = function(event) {
  276. //select dragged file
  277. var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked');
  278. if (!isDragSelected) {
  279. //select dragged file
  280. FileList._selectFileEl($(event.target).parents('tr:first'), true);
  281. }
  282. // do not show drag shadow for too many files
  283. var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize);
  284. selectedFiles.sort(FileList._fileInfoCompare);
  285. if (!isDragSelected && selectedFiles.length === 1) {
  286. //revert the selection
  287. FileList._selectFileEl($(event.target).parents('tr:first'), false);
  288. }
  289. // build dragshadow
  290. var dragshadow = $('<table class="dragshadow"></table>');
  291. var tbody = $('<tbody></tbody>');
  292. dragshadow.append(tbody);
  293. var dir=$('#dir').val();
  294. $(selectedFiles).each(function(i,elem) {
  295. var newtr = $('<tr/>')
  296. .attr('data-dir', dir)
  297. .attr('data-file', elem.name)
  298. .attr('data-origin', elem.origin);
  299. newtr.append($('<td/>').addClass('filename').text(elem.name));
  300. newtr.append($('<td/>').addClass('size').text(OC.Util.humanFileSize(elem.size)));
  301. tbody.append(newtr);
  302. if (elem.type === 'dir') {
  303. newtr.find('td.filename').attr('style','background-image:url('+OC.imagePath('core', 'filetypes/folder.png')+')');
  304. } else {
  305. var path = getPathForPreview(elem.name);
  306. Files.lazyLoadPreview(path, elem.mime, function(previewpath) {
  307. newtr.find('td.filename').attr('style','background-image:url('+previewpath+')');
  308. }, null, null, elem.etag);
  309. }
  310. });
  311. return dragshadow;
  312. };
  313. //options for file drag/drop
  314. //start&stop handlers needs some cleaning up
  315. // TODO: move to FileList class
  316. var dragOptions={
  317. revert: 'invalid', revertDuration: 300,
  318. opacity: 0.7, zIndex: 100, appendTo: 'body', cursorAt: { left: 24, top: 18 },
  319. helper: createDragShadow, cursor: 'move',
  320. start: function(event, ui){
  321. var $selectedFiles = $('td.filename input:checkbox:checked');
  322. if($selectedFiles.length > 1){
  323. $selectedFiles.parents('tr').fadeTo(250, 0.2);
  324. }
  325. else{
  326. $(this).fadeTo(250, 0.2);
  327. }
  328. },
  329. stop: function(event, ui) {
  330. var $selectedFiles = $('td.filename input:checkbox:checked');
  331. if($selectedFiles.length > 1){
  332. $selectedFiles.parents('tr').fadeTo(250, 1);
  333. }
  334. else{
  335. $(this).fadeTo(250, 1);
  336. }
  337. $('#fileList tr td.filename').addClass('ui-draggable');
  338. }
  339. };
  340. // sane browsers support using the distance option
  341. if ( $('html.ie').length === 0) {
  342. dragOptions['distance'] = 20;
  343. }
  344. // TODO: move to FileList class
  345. var folderDropOptions = {
  346. hoverClass: "canDrop",
  347. drop: function( event, ui ) {
  348. // don't allow moving a file into a selected folder
  349. if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) {
  350. return false;
  351. }
  352. var targetPath = FileList.getCurrentDirectory() + '/' + $(this).closest('tr').data('file');
  353. var files = FileList.getSelectedFiles();
  354. if (files.length === 0) {
  355. // single one selected without checkbox?
  356. files = _.map(ui.helper.find('tr'), FileList.elementToFile);
  357. }
  358. FileList.move(_.pluck(files, 'name'), targetPath);
  359. },
  360. tolerance: 'pointer'
  361. };
  362. Files.getMimeIcon = function(mime, ready) {
  363. if (Files.getMimeIcon.cache[mime]) {
  364. ready(Files.getMimeIcon.cache[mime]);
  365. } else {
  366. $.get( OC.filePath('files','ajax','mimeicon.php'), {mime: mime}, function(path) {
  367. if(OC.Util.hasSVGSupport()){
  368. path = path.substr(0, path.length-4) + '.svg';
  369. }
  370. Files.getMimeIcon.cache[mime]=path;
  371. ready(Files.getMimeIcon.cache[mime]);
  372. });
  373. }
  374. }
  375. Files.getMimeIcon.cache={};
  376. function getPathForPreview(name) {
  377. var path = $('#dir').val() + '/' + name;
  378. return path;
  379. }
  380. /**
  381. * Generates a preview URL based on the URL space.
  382. * @param urlSpec map with {x: width, y: height, file: file path}
  383. * @return preview URL
  384. */
  385. Files.generatePreviewUrl = function(urlSpec) {
  386. urlSpec = urlSpec || {};
  387. if (!urlSpec.x) {
  388. urlSpec.x = $('#filestable').data('preview-x');
  389. }
  390. if (!urlSpec.y) {
  391. urlSpec.y = $('#filestable').data('preview-y');
  392. }
  393. urlSpec.y *= window.devicePixelRatio;
  394. urlSpec.x *= window.devicePixelRatio;
  395. urlSpec.forceIcon = 0;
  396. return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
  397. };
  398. Files.lazyLoadPreview = function(path, mime, ready, width, height, etag) {
  399. // get mime icon url
  400. Files.getMimeIcon(mime, function(iconURL) {
  401. var previewURL,
  402. urlSpec = {};
  403. ready(iconURL); // set mimeicon URL
  404. urlSpec.file = Files.fixPath(path);
  405. if (etag){
  406. // use etag as cache buster
  407. urlSpec.c = etag;
  408. }
  409. else {
  410. console.warn('Files.lazyLoadPreview(): missing etag argument');
  411. }
  412. previewURL = Files.generatePreviewUrl(urlSpec);
  413. previewURL = previewURL.replace('(', '%28');
  414. previewURL = previewURL.replace(')', '%29');
  415. // preload image to prevent delay
  416. // this will make the browser cache the image
  417. var img = new Image();
  418. img.onload = function(){
  419. // if loading the preview image failed (no preview for the mimetype) then img.width will < 5
  420. if (img.width > 5) {
  421. ready(previewURL);
  422. }
  423. };
  424. img.src = previewURL;
  425. });
  426. };
  427. function getUniqueName(name) {
  428. if (FileList.findFileEl(name).exists()) {
  429. var numMatch;
  430. var parts=name.split('.');
  431. var extension = "";
  432. if (parts.length > 1) {
  433. extension=parts.pop();
  434. }
  435. var base=parts.join('.');
  436. numMatch=base.match(/\((\d+)\)/);
  437. var num=2;
  438. if (numMatch && numMatch.length>0) {
  439. num=parseInt(numMatch[numMatch.length-1])+1;
  440. base=base.split('(');
  441. base.pop();
  442. base=$.trim(base.join('('));
  443. }
  444. name=base+' ('+num+')';
  445. if (extension) {
  446. name = name+'.'+extension;
  447. }
  448. return getUniqueName(name);
  449. }
  450. return name;
  451. }
  452. function checkTrashStatus() {
  453. $.post(OC.filePath('files_trashbin', 'ajax', 'isEmpty.php'), function(result) {
  454. if (result.data.isEmpty === false) {
  455. $("input[type=button][id=trash]").removeAttr("disabled");
  456. }
  457. });
  458. }
  459. // override core's fileDownloadPath (legacy)
  460. function fileDownloadPath(dir, file) {
  461. return Files.getDownloadUrl(file, dir);
  462. }