"use strict"; require.config({ paths: { 'vs': '3rdparty/vs' }}); // create main application module var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ngDrag', 'ui.bootstrap', 'ui.bootstrap.contextMenu']); angular.module('Application').filter('prettyOwner', function () { return function (uid) { if (uid === 0) return 'root'; if (uid === 33) return 'www-data'; if (uid === 1000) return 'cloudron'; if (uid === 1001) return 'git'; return uid; }; }); // disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce app.config(function ($sceProvider) { $sceProvider.enabled(false); }); app.filter('trustUrl', ['$sce', function ($sce) { return function (recordingUrl) { return $sce.trustAsResourceUrl(recordingUrl); }; }]); // https://stackoverflow.com/questions/25621321/angularjs-ng-drag var ngDragEventDirectives = {}; angular.forEach( 'drag dragend dragenter dragexit dragleave dragover dragstart drop'.split(' '), function(eventName) { var directiveName = 'ng' + eventName.charAt(0).toUpperCase() + eventName.slice(1); ngDragEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse/*, $rootScope */) { return { restrict: 'A', compile: function($element, attr) { var fn = $parse(attr[directiveName], null, true); return function ngDragEventHandler(scope, element) { element.on(eventName, function(event) { var callback = function() { fn(scope, {$event: event}); }; scope.$apply(callback); }); }; } }; }]; } ); angular.module('ngDrag', []).directive(ngDragEventDirectives); app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Client', function ($scope, $translate, $timeout, Client) { var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); $scope.initialized = false; $scope.status = null; $scope.busy = true; $scope.client = Client; $scope.cwd = null; $scope.cwdParts = []; $scope.id = search.appId || search.volumeId; $scope.type = search.appId ? 'app' : 'volume'; $scope.rootDirLabel = ''; $scope.title = ''; $scope.entries = []; $scope.selected = []; // holds selected entries $scope.clipboard = []; // holds cut or copied entries $scope.clipboardCut = false; // if action is cut or copy $scope.dropToBody = false; $scope.sortAsc = true; $scope.sortProperty = 'fileName'; $scope.view = 'fileTree'; $scope.volumes = []; $scope.owners = [ { name: 'cloudron', value: 1000 }, { name: 'www-data', value: 33 }, { name: 'git', value: 1001 }, { name: 'root', value: 0 } ]; function isArchive(f) { return f.match(/\.tgz$/) || f.match(/\.tar$/) || f.match(/\.7z$/) || f.match(/\.zip$/) || f.match(/\.tar\.gz$/) || f.match(/\.tar\.xz$/) || f.match(/\.tar\.bz2$/); } $scope.menuOptions = []; // shown for entries $scope.menuOptionsBlank = []; // shown for empty space in folder var LANGUAGES = []; require(['vs/editor/editor.main'], function() { LANGUAGES = monaco.languages.getLanguages(); }); function getLanguage(filename) { var ext = '.' + filename.split('.').pop(); var language = LANGUAGES.find(function (l) { return !!l.extensions.find(function (e) { return e === ext; }); }) || ''; return language ? language.id : ''; } function sanitize(filePath) { filePath = filePath.split('/').filter(function (a) { return !!a; }).reduce(function (a, v) { if (v === '.'); // do nothing else if (v === '..') a.pop(); else a.push(v); return a; }, []).map(function (p) { // small detour to safely handle special characters and whitespace return encodeURIComponent(decodeURIComponent(p)); }).join('/'); return filePath; } function openPath(path) { path = sanitize(path); // we always show the parent path, even if overlayed by a mediaviewer var parentPath = sanitize(path + '/..'); $scope.busy = true; // nothing changes here, mostly triggered when editor is closed if ($scope.cwd === path) { $scope.busy = false; return; } if ($scope.cwd === parentPath) { var entry = $scope.entries.find(function (e) { return path === sanitize(parentPath + '/' + e.fileName); }); if (!entry) return Client.error('No such file or folder: ' + path); if (entry.isDirectory) { $scope.cwd = path; // refresh will set busy to false once done $scope.refresh(); } else if (entry.isFile) { var mimeType = Mimer().get(entry.fileName); var mimeGroup = mimeType.split('/')[0]; if (mimeGroup === 'video' || mimeGroup === 'image') { $scope.mediaViewer.show(entry); } else if (mimeType === 'application/pdf') { Client.filesGet($scope.id, $scope.type, path, 'open', function (error) { if (error) return Client.error(error); }); } else if (mimeGroup === 'text' || mimeGroup === 'application') { $scope.textEditor.show(entry); } else { Client.filesGet($scope.id, $scope.type, path, 'open', function (error) { if (error) return Client.error(error); }); } $scope.busy = false; } return; } Client.filesGet($scope.id, $scope.type, parentPath, 'data', function (error, result) { if (error) return setTimeout(function () { openPath(path); }, 2000); // try again in some time // amend icons result.entries.forEach(function (e) { e.icon = 'fa-file'; if (e.isDirectory) e.icon = 'fa-folder'; if (e.isSymbolicLink) e.icon = 'fa-link'; if (e.isFile) { var mimeType = Mimer().get(e.fileName); var mimeGroup = mimeType.split('/')[0]; if (mimeGroup === 'text') e.icon = 'fa-file-alt'; if (mimeGroup === 'image') e.icon = 'fa-file-image'; if (mimeGroup === 'video') e.icon = 'fa-file-video'; if (mimeGroup === 'audio') e.icon = 'fa-file-audio'; if (mimeType === 'text/csv') e.icon = 'fa-file-csv'; if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf'; } }); $scope.entries = result.entries; // call itself now that we know $scope.cwd = parentPath; openPath(path); }); } $scope.isSelected = function (entry) { return $scope.selected.indexOf(entry) !== -1; }; $scope.extractStatus = { error: null, busy: false, fileName: '' }; function extract(entry) { var filePath = sanitize($scope.cwd + '/' + entry.fileName); if (entry.isDirectory) return; // prevent it from getting closed $('#extractModal').modal({ backdrop: 'static', keyboard: false }); $scope.extractStatus.fileName = entry.fileName; $scope.extractStatus.error = null; $scope.extractStatus.busy = true; Client.filesExtract($scope.id, $scope.type, filePath, function (error) { $scope.extractStatus.busy = false; if (error) { console.error(error); $scope.extractStatus.error = $translate.instant('filemanager.extract.error', error.message); return; } $('#extractModal').modal('hide'); $scope.refresh(); }); } function download(entry) { var filePath = sanitize($scope.cwd + '/' + entry.fileName); Client.filesGet($scope.id, $scope.type, filePath, 'download', function (error) { if (error) return Client.error(error); }); } $scope.dragStart = function ($event, entry) { $event.originalEvent.dataTransfer.setData('text/plain', entry.fileName); $event.originalEvent.dataTransfer.setData('application/cloudron-filemanager', entry.fileName); }; $scope.dragEnter = function ($event, entry) { $event.originalEvent.stopPropagation(); $event.originalEvent.preventDefault(); // if entry is string, we come from breadcrumb if (entry && typeof entry === 'string') $event.currentTarget.classList.add('entry-hovered'); else if (entry && entry.isDirectory) entry.hovered = true; else $scope.dropToBody = true; $event.originalEvent.dataTransfer.dropEffect = 'move'; }; $scope.dragExit = function ($event, entry) { $event.originalEvent.stopPropagation(); $event.originalEvent.preventDefault(); // if entry is string, we come from breadcrumb if (entry && typeof entry === 'string') $event.currentTarget.classList.remove('entry-hovered'); else if (entry && entry.isDirectory) entry.hovered = false; $scope.dropToBody = false; $event.originalEvent.dataTransfer.dropEffect = 'move'; }; $scope.drop = function (event, entry) { event.originalEvent.stopPropagation(); event.originalEvent.preventDefault(); $scope.dropToBody = false; if (!event.originalEvent.dataTransfer.items[0]) return; var targetFolder; if (typeof entry === 'string') targetFolder = sanitize(entry); else targetFolder = sanitize($scope.cwd + '/' + (entry && entry.isDirectory ? entry.fileName : '')); var dataTransfer = event.originalEvent.dataTransfer; // check if we have internal drag'n'drop if (dataTransfer.getData('application/cloudron-filemanager')) { if ($scope.selected.length === 0) return; var moved = 0; // move files async.eachLimit($scope.selected, 5, function (entry, callback) { var oldFilePath = sanitize($scope.cwd + '/' + entry.fileName); var newFilePath = sanitize(targetFolder + '/' + entry.fileName); // if we drop the item on itself if (oldFilePath === targetFolder) return callback(); // if nothing changes if (newFilePath === oldFilePath) return callback(); moved++; // TODO this will overwrite files in destination! Client.filesRename($scope.id, $scope.type, oldFilePath, newFilePath, callback); }, function (error) { if (error) return Client.error(error); $scope.selected = []; // only refresh if anything has changed if (moved) $scope.refresh(); }); return; } // figure if a folder was dropped on a modern browser, in this case the first would have to be a directory var folderItem; try { folderItem = dataTransfer.items[0].webkitGetAsEntry(); if (folderItem.isFile) return uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false); } catch (e) { return uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false); } // if we got here we have a folder drop and a modern browser // now traverse the folder tree and create a file list $scope.uploadStatus.busy = true; $scope.uploadStatus.count = 0; var fileList = []; function traverseFileTree(item, path, callback) { if (item.isFile) { // Get file item.file(function (file) { fileList.push(file); ++$scope.uploadStatus.count; callback(); }); } else if (item.isDirectory) { // Get folder contents var dirReader = item.createReader(); dirReader.readEntries(function (entries) { async.each(entries, function (entry, callback) { traverseFileTree(entry, path + item.name + '/', callback); }, callback); }); } } traverseFileTree(folderItem, '', function (error) { $scope.uploadStatus.busy = false; $scope.uploadStatus.count = 0; if (error) return console.error(error); uploadFiles(fileList, targetFolder, false); }); }; $scope.refresh = function () { $scope.busy = true; Client.filesGet($scope.id, $scope.type, $scope.cwd, 'data', function (error, result) { if (error) return Client.error(error); // amend icons result.entries.forEach(function (e) { e.icon = 'fa-file'; if (e.isDirectory) e.icon = 'fa-folder'; if (e.isSymbolicLink) e.icon = 'fa-link'; if (e.isFile) { var mimeType = Mimer().get(e.fileName); var mimeGroup = mimeType.split('/')[0]; if (mimeGroup === 'text') e.icon = 'fa-file-alt'; if (mimeGroup === 'image') e.icon = 'fa-file-image'; if (mimeGroup === 'video') e.icon = 'fa-file-video'; if (mimeGroup === 'audio') e.icon = 'fa-file-audio'; if (mimeType === 'text/csv') e.icon = 'fa-file-csv'; if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf'; } }); $scope.entries = result.entries; $scope.cwdParts = $scope.cwd.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: $scope.cwd.split('/').slice(0, i+1).join('/') }; }); $scope.busy = false; }); }; $scope.open = function (entry) { location.hash = sanitize($scope.cwd + '/' + entry.fileName); }; $scope.select = function ($event, entry) { // we don't stop propagation for context menu closing, but if targets don't match we got the whole list click event if ($event.currentTarget !== $event.target) return; if (!entry) { $scope.selected = []; return; } var i = $scope.selected.indexOf(entry); var multi = ($event.ctrlKey || $event.metaKey); if ($event.button === 0) { // left click if (multi) { if (i === -1) $scope.selected.push(entry); else $scope.selected.splice(i, 1); } else { $scope.selected = [ entry ]; } } else if ($event.button === 2) { // right click if (i === -1) { if (multi) $scope.selected.push(entry); else $scope.selected = [ entry ]; } } }; $scope.onEntryContextMenu = function ($event, entry) { if ($scope.selected.indexOf(entry) !== -1) return; $scope.selected.push(entry); }; $scope.actionCut = function () { $scope.clipboard = $scope.selected.slice(); $scope.clipboard.forEach(function (entry) { entry.fullFilePath = sanitize($scope.cwd + '/' + entry.fileName); }); $scope.clipboardCut = true; }; $scope.actionCopy = function () { $scope.clipboard = $scope.selected.slice(); $scope.clipboard.forEach(function (entry) { entry.fullFilePath = sanitize($scope.cwd + '/' + entry.fileName); entry.pathFrom = $scope.cwd; // we stash the original path for pasting }); $scope.clipboardCut = false; }; function collectFiles(entry, callback) { var pathFrom = entry.pathFrom; if (entry.isDirectory) { Client.filesGet($scope.id, $scope.type, entry.fullFilePath, 'data', function (error, result) { if (error) return callback(error); if (!result.entries) return callback(new Error('not a folder')); // amend fullFilePath result.entries.forEach(function (e) { e.fullFilePath = sanitize(entry.fullFilePath + '/' + e.fileName); e.pathFrom = pathFrom; // we stash the original path for pasting }); var collectedFiles = []; async.eachLimit(result.entries, 5, function (entry, callback) { collectFiles(entry, function (error, result) { if (error) return callback(error); collectedFiles = collectedFiles.concat(result); callback(); }); }, function (error) { if (error) return callback(error); callback(null, collectedFiles); }); }); return; } callback(null, [ entry ]); } $scope.actionPaste = function (destinationEntry) { if ($scope.clipboardCut) { // move files async.eachLimit($scope.clipboard, 5, function (entry, callback) { var newFilePath = sanitize($scope.cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fileName); // TODO this will overwrite files in destination! Client.filesRename($scope.id, $scope.type, entry.fullFilePath, newFilePath, callback); }, function (error) { if (error) return Client.error(error); // clear clipboard $scope.clipboard = []; $scope.refresh(); }); } else { // copy files // first collect all files recursively var collectedFiles = []; async.eachLimit($scope.clipboard, 5, function (entry, callback) { collectFiles(entry, function (error, result) { if (error) return callback(error); collectedFiles = collectedFiles.concat(result); callback(); }); }, function (error) { if (error) return Client.error(error); async.eachLimit(collectedFiles, 5, function (entry, callback) { var newFilePath = sanitize($scope.cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fullFilePath.slice(entry.pathFrom.length)); // This will NOT overwrite but finds a unique new name to copy to // we prefix with a / to ensure we don't do relative target paths Client.filesCopy($scope.id, $scope.type, entry.fullFilePath, '/' + newFilePath, callback); }, function (error) { if (error) return Client.error(error); // clear clipboard $scope.clipboard = []; $scope.refresh(); }); }); } }; $scope.actionSelectAll = function () { $scope.selected = $scope.entries.slice(); }; $scope.goDirectoryUp = function () { openPath($scope.cwd + '/..'); }; $scope.changeDirectory = function (path) { path = sanitize(path); if ($scope.cwd === path) return; location.hash = path; }; $scope.uploadStatus = { error: null, busy: false, fileName: '', count: 0, countDone: 0, size: 0, done: 0, percentDone: 0, files: [], targetFolder: '' }; function uploadFiles(files, targetFolder, overwrite) { if (!files || !files.length) return; targetFolder = targetFolder || $scope.cwd; overwrite = !!overwrite; // prevent it from getting closed $('#uploadModal').modal({ backdrop: 'static', keyboard: false }); $scope.uploadStatus.files = files; $scope.uploadStatus.targetFolder = targetFolder; $scope.uploadStatus.error = null; $scope.uploadStatus.busy = true; $scope.uploadStatus.count = files.length; $scope.uploadStatus.countDone = 0; $scope.uploadStatus.size = 0; $scope.uploadStatus.sizeDone = 0; $scope.uploadStatus.done = 0; $scope.uploadStatus.percentDone = 0; for (var i = 0; i < files.length; ++i) { $scope.uploadStatus.size += files[i].size; } async.eachSeries(files, function (file, callback) { var filePath = sanitize(targetFolder + '/' + (file.webkitRelativePath || file.name)); $scope.uploadStatus.fileName = file.name; Client.filesUpload($scope.id, $scope.type, filePath, file, overwrite, function (loaded) { $scope.uploadStatus.percentDone = ($scope.uploadStatus.done+loaded) * 100 / $scope.uploadStatus.size; $scope.uploadStatus.sizeDone = loaded; }, function (error) { if (error) return callback(error); $scope.uploadStatus.done += file.size; $scope.uploadStatus.percentDone = $scope.uploadStatus.done * 100 / $scope.uploadStatus.size; $scope.uploadStatus.countDone++; callback(); }); }, function (error) { $scope.uploadStatus.busy = false; if (error && error.statusCode === 409) { $scope.uploadStatus.error = 'exists'; return; } else if (error) { console.error(error); $scope.uploadStatus.error = 'generic'; return; } $('#uploadModal').modal('hide'); $scope.uploadStatus.fileName = ''; $scope.uploadStatus.count = 0; $scope.uploadStatus.size = 0; $scope.uploadStatus.sizeDone = 0; $scope.uploadStatus.done = 0; $scope.uploadStatus.percentDone = 100; $scope.uploadStatus.files = []; $scope.uploadStatus.targetFolder = ''; $scope.refresh(); }); } $scope.retryUpload = function (overwrite) { uploadFiles($scope.uploadStatus.files, $scope.uploadStatus.targetFolder, !!overwrite); }; // file upload $('#uploadFileInput').on('change', function (e) { uploadFiles(e.target.files || [], $scope.cwd, false); }); $scope.onUploadFile = function () { $('#uploadFileInput').click(); }; // folder upload $('#uploadFolderInput').on('change', function (e ) { uploadFiles(e.target.files || [], $scope.cwd, false); }); $scope.onUploadFolder = function () { $('#uploadFolderInput').click(); }; $scope.restartAppBusy = false; $scope.onRestartApp = function () { $scope.restartAppBusy = true; function waitUntilRestarted(callback) { Client.getApp($scope.id, function (error, result) { if (error) return callback(error); if (result.installationState === ISTATES.INSTALLED) return callback(); setTimeout(waitUntilRestarted.bind(null, callback), 2000); }); } Client.restartApp($scope.id, function (error) { if (error) console.error('Failed to restart app.', error); waitUntilRestarted(function (error) { if (error) console.error('Failed wait for restart.', error); $scope.restartAppBusy = false; }); }); }; $scope.newDirectory = { busy: false, error: null, name: '', show: function () { $scope.newDirectory.error = null; $scope.newDirectory.name = ''; $scope.newDirectory.busy = false; $scope.newDirectoryForm.$setUntouched(); $scope.newDirectoryForm.$setPristine(); $('#newDirectoryModal').modal('show'); }, submit: function () { $scope.newDirectory.busy = true; $scope.newDirectory.error = null; var filePath = sanitize($scope.cwd + '/' + $scope.newDirectory.name); Client.filesCreateDirectory($scope.id, $scope.type, filePath, function (error) { $scope.newDirectory.busy = false; if (error && error.statusCode === 409) return $scope.newDirectory.error = 'exists'; if (error) return Client.error(error); $scope.refresh(); $('#newDirectoryModal').modal('hide'); }); } }; $scope.newFile = { busy: false, error: null, name: '', show: function () { $scope.newFile.error = null; $scope.newFile.name = ''; $scope.newFile.busy = false; $scope.newFileForm.$setUntouched(); $scope.newFileForm.$setPristine(); $('#newFileModal').modal('show'); }, submit: function () { $scope.newFile.busy = true; $scope.newFile.error = null; var filePath = sanitize($scope.cwd + '/' + $scope.newFile.name); Client.filesUpload($scope.id, $scope.type, filePath, new File([], $scope.newFile.name), false, function () {}, function (error) { $scope.newFile.busy = false; if (error && error.statusCode === 409) return $scope.newFile.error = 'exists'; if (error) return Client.error(error); $scope.refresh(); $('#newFileModal').modal('hide'); }); } }; $scope.renameEntry = { busy: false, error: null, entry: null, newName: '', show: function (entry) { $scope.renameEntry.error = null; $scope.renameEntry.entry = entry; $scope.renameEntry.newName = entry.fileName; $scope.renameEntry.busy = false; $('#renameEntryModal').modal('show'); }, submit: function () { $scope.renameEntry.busy = true; var oldFilePath = sanitize($scope.cwd + '/' + $scope.renameEntry.entry.fileName); var newFilePath = sanitize(($scope.renameEntry.newName[0] === '/' ? '' : ($scope.cwd + '/')) + $scope.renameEntry.newName); Client.filesRename($scope.id, $scope.type, oldFilePath, newFilePath, function (error) { $scope.renameEntry.busy = false; if (error) return Client.error(error); $scope.refresh(); $('#renameEntryModal').modal('hide'); }); } }; $scope.mediaViewer = { type: '', src: '', entry: null, show: function (entry) { var filePath = sanitize($scope.cwd + '/' + entry.fileName); $scope.mediaViewer.entry = entry; $scope.mediaViewer.type = Mimer().get(entry.fileName).split('/')[0]; $scope.mediaViewer.src = ''; Client.filesGet($scope.id, $scope.type, filePath, 'link', function (error, result) { if (error) return Client.error(error); $scope.mediaViewer.src = result; $('#mediaViewerModal').modal('show'); }); }, close: function () { // set an empty pixel image to bust the cached img to avoid flickering on slow load $scope.mediaViewer.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg=='; $('#mediaViewerModal').modal('hide'); } }; $scope.textEditor = { busy: false, entry: null, editor: null, unsaved: false, show: function (entry) { $scope.textEditor.entry = entry; $scope.textEditor.busy = false; $scope.textEditor.unsaved = false; // clear model if any if ($scope.textEditor.editor && $scope.textEditor.editor.getModel()) $scope.textEditor.editor.setModel(null); $scope.view = 'textEditor'; // document.getElementById('textEditorModal').style['display'] = 'flex'; var filePath = sanitize($scope.cwd + '/' + entry.fileName); var language = getLanguage(entry.fileName); Client.filesGet($scope.id, $scope.type, filePath, 'data', function (error, result) { if (error) return Client.error(error); if (!$scope.textEditor.editor) { $timeout(function () { $scope.textEditor.editor = monaco.editor.create(document.getElementById('textEditorContainer'), { value: result, language: language, theme: 'vs-dark' }); $scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; }); }, 200); } else { $scope.textEditor.editor.setModel(monaco.editor.createModel(result, language)); $scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; }); // have to re-attach whenever model changes } }); }, save: function (callback) { $scope.textEditor.busy = true; var newContent = $scope.textEditor.editor.getValue(); var filePath = sanitize($scope.cwd + '/' + $scope.textEditor.entry.fileName); var file = new File([newContent], 'file'); Client.filesUpload($scope.id, $scope.type, filePath, file, true, function () {}, function (error) { if (error) return Client.error(error); // update size immediately for the list view to avoid reloading the whole list $scope.textEditor.entry.size = file.size; $timeout(function () { $scope.textEditor.unsaved = false; $scope.textEditor.busy = false; if (typeof callback === 'function') return callback(); }, 1000); }); }, close: function () { $scope.view = 'fileTree'; $('#textEditorCloseModal').modal('hide'); }, onClose: function () { $scope.view = 'fileTree'; location.hash = $scope.cwd; $('#textEditorCloseModal').modal('hide'); }, saveAndClose: function () { $scope.textEditor.save(function () { $scope.textEditor.onClose(); }); }, maybeClose: function () { if (!$scope.textEditor.unsaved) return $scope.textEditor.onClose(); $('#textEditorCloseModal').modal('show'); }, }; $scope.chownEntry = { busy: false, error: null, newOwner: 0, recursive: false, showRecursiveOption: false, show: function () { $scope.chownEntry.error = null; // set default uid from first file $scope.chownEntry.newOwner = $scope.selected[0].uid; $scope.chownEntry.busy = false; // default for directories is recursive $scope.chownEntry.recursive = !!$scope.selected.find(function (entry) { return entry.isDirectory; }); $scope.chownEntry.showRecursiveOption = !!$scope.chownEntry.recursive; $('#chownEntryModal').modal('show'); }, submit: function () { $scope.chownEntry.busy = true; async.eachLimit($scope.selected, 5, function (entry, callback) { var filePath = sanitize($scope.cwd + '/' + entry.fileName); Client.filesChown($scope.id, $scope.type, filePath, $scope.chownEntry.newOwner, $scope.chownEntry.recursive, callback); }, function (error) { $scope.chownEntry.busy = false; if (error) return Client.error(error); $scope.refresh(); $('#chownEntryModal').modal('hide'); }); } }; $scope.entryRemove = { busy: false, error: null, show: function () { $scope.entryRemove.error = null; $('#entryRemoveModal').modal('show'); }, submit: function () { $scope.entryRemove.busy = true; async.eachLimit($scope.selected, 5, function (entry, callback) { var filePath = sanitize($scope.cwd + '/' + entry.fileName); Client.filesRemove($scope.id, $scope.type, filePath, callback); }, function (error) { $scope.entryRemove.busy = false; if (error) return Client.error(error); $scope.refresh(); $('#entryRemoveModal').modal('hide'); }); } }; function fetchVolumesInfo(mounts) { $scope.volumes = []; async.each(mounts, function (mount, callback) { Client.getVolume(mount.volumeId, function (error, result) { if (error) return callback(error); $scope.volumes.push(result); callback(); }); }, function (error) { if (error) console.error('Failed to fetch volumes info.', error); }); } function init() { Client.getStatus(function (error, status) { if (error) return Client.initError(error, init); if (!status.activated) { console.log('Not activated yet, redirecting', status); window.location.href = '/'; return; } // check version and force reload if needed if (!localStorage.version) { localStorage.version = status.version; } else if (localStorage.version !== status.version) { localStorage.version = status.version; window.location.reload(true); } $scope.status = status; console.log('Running filemanager version ', localStorage.version); // get user profile as the first thing. this populates the "scope" and affects subsequent API calls Client.refreshUserInfo(function (error) { if (error) return Client.initError(error, init); var getter = $scope.type === 'app' ? Client.getApp.bind(Client, $scope.id) : Client.getVolume.bind(Client, $scope.id); getter(function (error, result) { if (error) { $scope.initialized = true; return; } // fine to do async fetchVolumesInfo(result.mounts || []); $scope.title = $scope.type === 'app' ? (result.label || result.fqdn) : result.name; $scope.rootDirLabel = $scope.type === 'app' ? '/app/data/' : result.hostPath; window.document.title = $scope.title + ' - ' + $translate.instant('filemanager.title'); // now mark the Client to be ready Client.setReady(); openPath(window.location.hash.slice(1)); $scope.initialized = true; }); }); }); } init(); var busyHash = window.location.hash.slice(1); $scope.$watch('busy', function (prev, next) { if (!prev && next) { window.location.hash = busyHash; busyHash = window.location.hash.slice(1); } }); window.addEventListener('hashchange', function () { if ($scope.busy) return false; busyHash = window.location.hash.slice(1); $scope.$apply(function () { // first close all dialogs $scope.mediaViewer.close(); $scope.textEditor.close(); openPath(window.location.hash.slice(1)); }); }); function isModalVisible() { return !!document.getElementsByClassName('modal in').length; } // handle save shortcuts window.addEventListener('keydown', function (event) { if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') { if ($scope.view !== 'textEditor') return; event.preventDefault(); $scope.$apply($scope.textEditor.save); } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'c') { if ($scope.view === 'textEditor') return; if ($scope.selected.length === 0) return; if (isModalVisible()) return; event.preventDefault(); $scope.$apply($scope.actionCopy); } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'x') { if ($scope.view === 'textEditor') return; if ($scope.selected.length === 0) return; if (isModalVisible()) return; event.preventDefault(); $scope.$apply($scope.actionCut); } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'v') { if ($scope.view === 'textEditor') return; if ($scope.clipboard.length === 0) return; if (isModalVisible()) return; event.preventDefault(); $scope.$apply($scope.actionPaste); } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'a') { if ($scope.view === 'textEditor') return; if (isModalVisible()) return; event.preventDefault(); $scope.$apply($scope.actionSelectAll); } else if(event.key === 'Escape') { $scope.$apply(function () { $scope.selected = []; }); } }); $translate(['filemanager.list.menu.edit', 'filemanager.list.menu.cut', 'filemanager.list.menu.copy', 'filemanager.list.menu.paste', 'filemanager.list.menu.rename', 'filemanager.list.menu.chown', 'filemanager.list.menu.extract', 'filemanager.list.menu.download', 'filemanager.list.menu.delete' ]).then(function (tr) { $scope.menuOptions = [ { text: tr['filemanager.list.menu.edit'], displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && !entry.isSymbolicLink; }, enabled: function () { return $scope.selected.length === 1; }, hasBottomDivider: true, click: function ($itemScope, $event, entry) { $scope.open(entry); } }, { text: tr['filemanager.list.menu.cut'], click: function ($itemScope, $event, entry) { $scope.actionCut(); } }, { text: tr['filemanager.list.menu.copy'], click: function ($itemScope, $event, entry) { $scope.actionCopy(); } }, { text: tr['filemanager.list.menu.paste'], hasBottomDivider: true, enabled: function () { return $scope.clipboard.length; }, click: function ($itemScope, $event, entry) { $scope.actionPaste(entry); } }, { text: tr['filemanager.list.menu.rename'], enabled: function () { return $scope.selected.length === 1; }, click: function ($itemScope, $event, entry) { $scope.renameEntry.show(entry); } }, { text: tr['filemanager.list.menu.chown'], click: function ($itemScope, $event, entry) { $scope.chownEntry.show(); } }, { text: tr['filemanager.list.menu.extract'], displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && isArchive(entry.fileName); }, click: function ($itemScope, $event, entry) { extract(entry); } }, { text: tr['filemanager.list.menu.download'], enabled: function () { return $scope.selected.length === 1; }, click: function ($itemScope, $event, entry) { download(entry); } }, { text: tr['filemanager.list.menu.delete'], hasTopDivider: true, click: function ($itemScope, $event, entry) { $scope.entryRemove.show(); } } ]; }); $translate(['filemanager.toolbar.newFile', 'filemanager.toolbar.newFolder', 'filemanager.list.menu.paste', 'filemanager.list.menu.selectAll' ]).then(function (tr) { $scope.menuOptionsBlank = [ { text: tr['filemanager.toolbar.newFile'], click: function ($itemScope, $event) { $scope.newFile.show(); } }, { text: tr['filemanager.toolbar.newFolder'], click: function ($itemScope, $event) { $scope.newDirectory.show(); } }, { text: tr['filemanager.list.menu.paste'], hasTopDivider: true, hasBottomDivider: true, enabled: function () { return $scope.clipboard.length; }, click: function ($itemScope, $event) { $scope.actionPaste(); } }, { text: tr['filemanager.list.menu.selectAll'], click: function ($itemScope, $event) { $scope.actionSelectAll(); } } ]; }); $('.file-list').on('scroll', function (event) { if (event.target.scrollTop > 10) event.target.classList.add('top-scroll-indicator'); else event.target.classList.remove('top-scroll-indicator'); }); // setup all the dialog focus handling ['newFileModal', 'newDirectoryModal', 'renameEntryModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { $(this).find('[autofocus]:first').focus(); }); }); // this will update the hash $('#mediaViewerModal').on('hidden.bs.modal', function () { location.hash = $scope.cwd; }); // selects filename (without extension) ['renameEntryModal'].forEach(function (id) { $('#' + id).on('shown.bs.modal', function () { var elem = $(this).find('[autofocus]:first'); var text = elem.val(); elem[0].setSelectionRange(0, text.indexOf('.')); }); }); }]);