1164 lines
42 KiB
JavaScript
1164 lines
42 KiB
JavaScript
"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);
|
|
|
|
var parentPath = sanitize(path + '/..');
|
|
|
|
// 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.changeDirectory(path);
|
|
} 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 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.cwd = parentPath;
|
|
$scope.cwdParts = parentPath.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
|
|
|
|
// call itself now that we know
|
|
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);
|
|
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) {
|
|
$scope.busy = false;
|
|
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.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 () {
|
|
$scope.changeDirectory($scope.cwd + '/..');
|
|
};
|
|
|
|
$scope.changeDirectory = function (path) {
|
|
path = sanitize(path);
|
|
|
|
if ($scope.cwd === path) return;
|
|
|
|
location.hash = path;
|
|
|
|
$scope.cwd = path;
|
|
$scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
|
|
|
|
$scope.refresh();
|
|
};
|
|
|
|
$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 () {
|
|
$('#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();
|
|
|
|
window.addEventListener('hashchange', function () {
|
|
$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('.'));
|
|
});
|
|
});
|
|
}]);
|