返回 DeepSeek-Reasonix
remote.go
根目录 / internal / store / remote.go
1 package store
2
3 import (
4 "fmt"
5 "hash/fnv"
6 "strings"
7 "unicode/utf8"
8 )
9
10 // Remote-SSH module naming: the canonical file names for the state a
11 // bootstrapped remote serve leaves under the remote host's
12 // ~/.reasonix/remote/. Only name derivation lives here (this package is the
13 // path authority and does no I/O); reads and writes happen over SFTP in
14 // internal/remote. Local-side absolute paths (managed known_hosts) are
15 // derived in internal/config/paths.go, which owns REASONIX_HOME resolution.
16
17 // RemoteDirName is the directory under the remote ~/.reasonix that holds all
18 // remote-module state, and under the local Reasonix home that holds the
19 // managed known_hosts file.
20 const RemoteDirName = "remote"
21
22 // RemoteBinDirName holds an uploaded reasonix binary on the remote host:
23 // ~/.reasonix/remote/bin/reasonix.
24 const RemoteBinDirName = "bin"
25
26 // RemoteWorkspaceSlug flattens a remote (POSIX) workspace path into a
27 // filename component, mirroring config.WorkspaceSlug's shape. Remote targets
28 // are Linux/macOS only, so no case folding applies. A readable prefix derived
29 // from the path is always suffixed with an FNV-1a hash of the exact original
30 // path, so lossy separator replacement can never make two distinct workspaces
31 // share serve state: "/srv/a-b" and "/srv/a/b" both reduce to the readable
32 // stem "srv-a-b" but hash differently, yielding distinct slugs (and thus
33 // distinct pid/token/log/state files).
34 func RemoteWorkspaceSlug(remotePath string) string {
35 clean := strings.TrimSuffix(remotePath, "/")
36 stem := strings.NewReplacer("/", "-", ":", "-").Replace(clean)
37 stem = strings.Trim(stem, "-")
38 if stem == "" {
39 stem = "root"
40 }
41 h := fnv.New64a()
42 _, _ = h.Write([]byte(clean))
43 sum := fmt.Sprintf("%016x", h.Sum64())
44 // Cap the readable stem so the whole slug (stem + "-" + 16 hex) fits the
45 // per-workspace filename budget with room for the serve-<slug>.token wrapper.
46 stem = boundRemoteComponent(stem, 180)
47 return stem + "-" + sum
48 }
49
50 // RemoteServeStateName is the per-workspace serve state JSON: pid, addr,
51 // workspace, version, started_at.
52 func RemoteServeStateName(slug string) string { return "serve-" + slug + ".json" }
53
54 // RemoteServeTokenName holds the pre-shared auth token (0600), written over
55 // SFTP before launch and read by serve via --token-file.
56 func RemoteServeTokenName(slug string) string { return "serve-" + slug + ".token" }
57
58 // RemoteServeLogName captures the detached serve's stdout/stderr.
59 func RemoteServeLogName(slug string) string { return "serve-" + slug + ".log" }
60
61 // RemoteServePortName receives the real bound address via --port-file.
62 func RemoteServePortName(slug string) string { return "serve-" + slug + ".port" }
63
64 // RemoteServePidName receives the server pid via --pid-file.
65 func RemoteServePidName(slug string) string { return "serve-" + slug + ".pid" }
66
67 // RemoteServeLockName is the cross-client bootstrap lock directory. Directory
68 // creation is atomic on SFTP servers, including the Linux/macOS targets.
69 func RemoteServeLockName(slug string) string { return "serve-" + slug + ".lock" }
70
71 // boundRemoteComponent mirrors config.boundFilenameComponent (this package is
72 // a stdlib-only leaf and cannot import config): inputs at or under the budget
73 // pass through byte-identical; longer ones are truncated at a rune boundary
74 // with an FNV-1a hash of the full input appended.
75 func boundRemoteComponent(s string, maxLen int) string {
76 if maxLen <= 0 || len(s) <= maxLen {
77 return s
78 }
79 h := fnv.New64a()
80 _, _ = h.Write([]byte(s))
81 budget := maxLen - 17 // "-" + 16 hex digits
82 prefix := s[:budget]
83 for len(prefix) > 0 && !utf8.ValidString(prefix) {
84 prefix = prefix[:len(prefix)-1]
85 }
86 return fmt.Sprintf("%s-%016x", prefix, h.Sum64())
87 }
88
88 lines GO