feat: file manager on SSH hosts through a machine - open from the plus menu
This commit is contained in:
1 parent
2859e69abe
commit
7e4daeda4c
8 files changed
+202
-11
No files matched your search
@@ -757,5 +757,6 @@ p.mv-a { margin:0; font-size:13px; }
|
||||
/* The pane: its bar and list scroll inside it. */
|
||||
.fpane-body { flex:1; min-height:0; overflow:auto; padding:10px 12px 12px; }
|
||||
.tabview:not(.split) .fpane-body { padding:12px 16px 16px; }
|
||||
/* A lone file pane: its first bar keeps clear of the corner "+". */
|
||||
.tabview:not(.split) .fpane .fm-bar:first-child, body.split1 .fpane .fm-bar:first-child { margin-right:40px; }
|
||||
/* The file pane under the corner "+" keeps its first bar clear of it,
|
||||
while the "+" is there (one tab; with the tab strip it is hidden). */
|
||||
body:not(.multi) .fpane.under-plus .fm-bar:first-child { margin-right:40px; }
|
||||
@@ -55,6 +55,21 @@
|
||||
close() {}
|
||||
}
|
||||
|
||||
// An SSH host's files, through the machine that holds the connection.
|
||||
class SSHSource {
|
||||
constructor(agent, conn, home) { this.agent = agent; this.conn = conn; this.homeDir = home || '/'; this.caps = {zip: false, sqlite: false}; }
|
||||
base() { return '/api/panel/agents/' + encodeURIComponent(this.agent.id) + '/sshfs/' + encodeURIComponent(this.conn) + '/'; }
|
||||
url(what, path) { return this.base() + what + '?path=' + encodeURIComponent(path); }
|
||||
op(name, body) {
|
||||
return fetch(this.base() + name, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body || {}), credentials: 'same-origin'
|
||||
}).then(r => r.status === 404 ? {ok: false, error: 'нет доступа к машине'} : r.json()).catch(() => ({ok: false, error: 'ошибка сети'}));
|
||||
}
|
||||
home() { return this.homeDir; }
|
||||
// The pane closing lets the machine drop the connection at once.
|
||||
close() { fetch(this.base() + 'close', {method: 'POST', keepalive: true, credentials: 'same-origin'}).catch(() => {}); }
|
||||
}
|
||||
|
||||
const UP_CAP = 512 * 1024 * 1024;
|
||||
const SORT_DEF = {by: 'name', dir: 1};
|
||||
const byName = (a, b) => a.name.localeCompare(b.name, undefined, {numeric: true, sensitivity: 'base'}) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
||||
@@ -366,4 +381,13 @@
|
||||
window.FilePane = FilePane;
|
||||
window.MachineSource = MachineSource;
|
||||
Workspace.paneTypes.files = agent => new FilePane({kind: 'agent', agent});
|
||||
Workspace.paneTypes.sshfiles = (agent, o) => new FilePane({kind: 'ssh', agent, ssh: o.ssh, src: new SSHSource(agent, o.conn, o.home)});
|
||||
// sshFiles connects agent to the SSH host t, then opens its files in a
|
||||
// pane; the password or key goes to the machine once and is not kept here.
|
||||
Workspace.sshFiles = (agent, t) => fetch('/api/panel/agents/' + encodeURIComponent(agent.id) + '/sshfs/open', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ssh: t}), credentials: 'same-origin'
|
||||
}).then(r => r.status === 404 ? {ok: false, error: 'нет доступа к машине'} : r.json()).then(r => {
|
||||
if (r.ok) Workspace.open('sshfiles', agent, {conn: r.data.id, home: r.data.home, ssh: {host: t.host, port: t.port, user: t.user}});
|
||||
return r;
|
||||
}).catch(() => ({ok: false, error: 'ошибка сети'}));
|
||||
})();
|
||||
@@ -269,7 +269,7 @@
|
||||
if (r.data.truncated) $('vmsg').textContent += ' · показаны первые ' + fmtSize(EDIT_LIMIT);
|
||||
return;
|
||||
}
|
||||
if (/^(db|sqlite3?|db3)$/.test(ext)) {
|
||||
if (/^(db|sqlite3?|db3)$/.test(ext) && cur.src.caps.sqlite) {
|
||||
// The agent opens the database and edits it; one that predates that
|
||||
// gets the file read here, for viewing only.
|
||||
box.innerHTML = '<p class="dv-note">загрузка…</p>';
|
||||
@@ -858,6 +858,9 @@
|
||||
$('mv-src').textContent = M.src + (e.is_dir ? '/' : '');
|
||||
setOp('move');
|
||||
if (!M.job) { busy(false); setRemote(false); }
|
||||
// From an SSH host the file goes elsewhere by dragging it to a pane;
|
||||
// this dialog sends a machine's files only.
|
||||
$('mv-net').hidden = cur.kind === 'sshfiles';
|
||||
$('mv-dir').value = cur.cwd === '/' ? '/' : cur.cwd + '/';
|
||||
$('mv-name').value = e.name;
|
||||
$('mv-conflict').value = 'error';
|
||||
|
||||
@@ -198,10 +198,15 @@
|
||||
if (!W || !H) return;
|
||||
const L = rects(t.layout, t.c, t.r, W, H);
|
||||
t.view.classList.toggle('instant', !animate);
|
||||
// The pane in the top right corner sits under the corner "+": it keeps
|
||||
// its first bar clear of it.
|
||||
let corner = 0;
|
||||
L.panes.forEach((p, i) => { const c = L.panes[corner]; if (p.y < c.y || (p.y === c.y && p.x + p.w > c.x + c.w)) corner = i; });
|
||||
t.panes.forEach((s, i) => {
|
||||
const p = L.panes[i];
|
||||
s.el.style.left = p.x + 'px'; s.el.style.top = p.y + 'px';
|
||||
s.el.style.width = p.w + 'px'; s.el.style.height = p.h + 'px';
|
||||
s.el.classList.toggle('under-plus', i === corner);
|
||||
});
|
||||
while (t.gutters.length < L.gutters.length) {
|
||||
const g = document.createElement('div');
|
||||
@@ -639,7 +644,11 @@
|
||||
window.visualViewport.addEventListener('scroll', refit);
|
||||
}
|
||||
window.addEventListener('pagehide', () => {
|
||||
tabs.forEach(t => t.panes.forEach(s => { if (s.sid) navigator.sendBeacon(base + s.sid + '/close'); }));
|
||||
tabs.forEach(t => t.panes.forEach(s => {
|
||||
if (s.sid) navigator.sendBeacon(base + s.sid + '/close');
|
||||
// A file pane on an SSH host lets the machine drop its connection.
|
||||
if (s.src && s.src.close) s.src.close();
|
||||
}));
|
||||
});
|
||||
|
||||
// The view coming into sight: fit the panes, show the status line.
|
||||
|
||||
Reference in new issue
Block a user