perf: several dragged items cross to another place as one tar stream; older agents get them one by one

This commit is contained in:
redtea committed 2026-10-15 22:28:46 +03:00
1 parent 6916af4236
commit 7d1996dd7d
8 files changed
+301 -72

No files matched your search

+11
View File
@@ -147,3 +147,14 @@ func TestAIChatNeedsUserOn(t *testing.T) {
t.Fatalf("on: %d %q", w.Code, b)
}
}
func TestVersionAtLeast(t *testing.T) {
for _, c := range []struct {
v string
want bool
}{{"0.2.0", true}, {"0.10.1", true}, {"0.1.9", false}, {"", false}, {"dev", false}, {"1.0", true}} {
if got := versionAtLeast(c.v, "0.2.0"); got != c.want {
t.Errorf("%q: %v", c.v, got)
}
}
}
+72 -12
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"net/http"
"path"
"strconv"
"strings"
"sync"
@@ -119,6 +120,8 @@ type shipReq struct {
// file pane (its connection id); To is still the agent holding ToConn.
FromConn string `json:"from_conn"`
ToConn string `json:"to_conn"`
// More: other things of Src's folder sent along, under their own names.
More []string `json:"more"`
}
// POST /api/panel/agents/{id}/fs/ship - start sending; the answer names the
@@ -214,7 +217,27 @@ func oldAgent(res proto.TaskResult) proto.TaskResult {
}
func (s *Server) ship1(ctx context.Context, j *shipJob, from string, q shipReq) proto.TaskResult {
args := proto.FSShipArgs{Src: q.Src, Name: q.Name, Dir: q.Dir, Conflict: q.Conflict, Job: j.id}
if len(q.More) > 0 && !s.shipsSeveral(from, q) {
// One at a time: an SSH address, or an agent from before 0.2.0.
var last proto.TaskResult
done, skipped := 0, 0
for i, p := range append([]string{q.Src}, q.More...) {
one := q
one.Src, one.More = p, nil
if i > 0 {
one.Name = path.Base(path.Clean(p))
}
if last = s.ship1(ctx, j, from, one); !last.OK {
return last
}
var d struct{ Done, Skipped int }
_ = json.Unmarshal(last.Data, &d)
done, skipped = done+d.Done, skipped+d.Skipped
}
data, _ := json.Marshal(map[string]int{"done": done, "skipped": skipped})
return proto.TaskResult{OK: true, Data: data}
}
args := proto.FSShipArgs{Src: q.Src, Name: q.Name, Dir: q.Dir, Conflict: q.Conflict, Job: j.id, More: q.More}
if q.SSH != nil {
args.SSH, args.Move = q.SSH, q.Move
res, err := s.waitTask(ctx, from, proto.TaskFSShipOut, args, xferTimeout)
@@ -267,17 +290,19 @@ func (s *Server) ship1(ctx context.Context, j *shipJob, from string, q shipReq)
return out
}
if q.Move {
task, del := proto.TaskFSDelTree, any(proto.FSPathArgs{Path: q.Src})
if q.FromConn != "" {
raw, _ := json.Marshal(proto.FSPathArgs{Path: q.Src})
task, del = proto.TaskSSHFSOp, proto.SSHFSArgs{Conn: q.FromConn, Op: "deltree", Args: raw}
}
res, err := s.waitTask(ctx, from, task, del, fsTaskTimeout)
if err == nil && !res.OK {
err = errText(res.Error)
}
if err != nil {
return proto.TaskResult{Error: "отправлено, но здесь не удалилось: " + err.Error()}
for _, p := range append([]string{q.Src}, q.More...) {
task, del := proto.TaskFSDelTree, any(proto.FSPathArgs{Path: p})
if q.FromConn != "" {
raw, _ := json.Marshal(proto.FSPathArgs{Path: p})
task, del = proto.TaskSSHFSOp, proto.SSHFSArgs{Conn: q.FromConn, Op: "deltree", Args: raw}
}
res, err := s.waitTask(ctx, from, task, del, fsTaskTimeout)
if err == nil && !res.OK {
err = errText(res.Error)
}
if err != nil {
return proto.TaskResult{Error: "отправлено, но здесь не удалилось: " + err.Error()}
}
}
}
// The receiver's count: what it put down and what it kept as it was.
@@ -434,3 +459,38 @@ func (s *Server) handleRelayGet(w http.ResponseWriter, r *http.Request) {
}
_, _ = w.Write(data)
}
// shipsSeveral: both agents take several things in one stream (0.2.0 on).
func (s *Server) shipsSeveral(from string, q shipReq) bool {
if q.SSH != nil {
return false
}
for _, id := range []string{from, q.To} {
a, _ := s.db.GetAgent(id)
if a == nil || !versionAtLeast(a.Version, "0.2.0") {
return false
}
}
return true
}
// versionAtLeast: version v (dotted numbers) is min or newer; anything else
// is taken for older.
func versionAtLeast(v, min string) bool {
a, b := strings.Split(v, "."), strings.Split(min, ".")
for i := range b {
var x, y int
if i < len(a) {
n, err := strconv.Atoi(a[i])
if err != nil {
return false
}
x = n
}
y, _ = strconv.Atoi(b[i])
if x != y {
return x > y
}
}
return true
}
+8 -4
View File
@@ -155,14 +155,18 @@
q('.dd-ptext').textContent = 'подготовка…';
q('.dd-cancel').textContent = 'Стоп';
}
for (const p of item.paths) {
// Between places, all of them go as one stream: the first, and the rest
// as "more" (the server sends one by one to older agents).
const batch = here ? item.paths.map(p => [p]) : [item.paths];
for (const group of batch) {
const p = group[0];
if (c.stop) break;
const name = p.split('/').pop();
let r;
if (here) {
r = await from.src.op('copy', {src: p, dst: join(t.dir, name), move: isMove, conflict});
} else {
const body = {src: p, name, dir: t.dir, conflict, move: isMove};
const body = {src: p, name, dir: t.dir, conflict, move: isMove, more: group.slice(1)};
if (t.source.kind === 'ssh') { body.to = t.pane.agent.id; body.to_conn = t.pane.src.conn; }
else body.to = t.pane.agent.id;
if (from.kind === 'sshfiles') body.from_conn = from.src.conn;
@@ -170,7 +174,7 @@
r = await new MachineSource(from.agent).op('ship', body);
if (r.ok) {
c.job = r.data.job;
const pre = item.paths.length > 1 ? name + ': ' : '';
const pre = '';
r = await FV.followJob(c.job, (pr, text) => {
if (pr.total > 0 || pr.done > 0) {
q('.dd-bar').parentNode.classList.remove('wait');
@@ -182,7 +186,7 @@
}
}
if (!r.ok) { err = r.error || 'ошибка'; break; }
done++;
done += group.length;
skipped += (r.data && r.data.skipped) || 0;
}
close();