Add file upload functionality to the agent. Implement API endpoint for uploading files, including validation and error handling. Update web interface to support file selection and display upload status. Enhance input handling with configurable key delay for text input.
This commit is contained in:
@@ -67,6 +67,7 @@ func (a *Agent) Serve() error {
|
||||
mux.HandleFunc("/api/v1/status", a.handleStatus)
|
||||
mux.HandleFunc("/api/v1/files", a.handleFiles)
|
||||
mux.HandleFunc("/api/v1/download", a.handleDownload)
|
||||
mux.HandleFunc("/api/v1/upload", a.handleUpload)
|
||||
mux.HandleFunc("/api/v1/screenshot", a.handleScreenshot)
|
||||
mux.HandleFunc("/api/v1/exec", a.handleExec)
|
||||
mux.HandleFunc("/api/v1/startup", a.handleStartup)
|
||||
|
||||
+70
-2
@@ -44,6 +44,7 @@ func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
|
||||
"/api/v1/status": map[string]any{"get": map[string]string{"summary": "Agent status"}},
|
||||
"/api/v1/files": map[string]any{"get": map[string]string{"summary": "List files"}},
|
||||
"/api/v1/download": map[string]any{"get": map[string]string{"summary": "Download file"}},
|
||||
"/api/v1/upload": map[string]any{"post": map[string]string{"summary": "Upload a file to a directory"}},
|
||||
"/api/v1/screenshot": map[string]any{"get": map[string]string{"summary": "Capture desktop"}},
|
||||
"/api/v1/exec": map[string]any{"post": map[string]string{"summary": "Run a command"}},
|
||||
"/api/v1/startup": map[string]any{"post": map[string]string{"summary": "Add to Windows startup"}, "delete": map[string]string{"summary": "Remove from Windows startup"}},
|
||||
@@ -170,6 +171,65 @@ func (a *Agent) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, name, info.ModTime(), f)
|
||||
}
|
||||
|
||||
func (a *Agent) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, config.MaxUploadSize)
|
||||
if err := r.ParseMultipartForm(config.MaxUploadSize); err != nil {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "upload body is too large or invalid")
|
||||
return
|
||||
}
|
||||
dir := strings.TrimSpace(r.FormValue("path"))
|
||||
if dir == "" {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "path is required")
|
||||
return
|
||||
}
|
||||
upload, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "file is required")
|
||||
return
|
||||
}
|
||||
defer upload.Close()
|
||||
|
||||
target, err := files.UploadTarget(a.root, dir, header.Filename)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, files.ErrBadUploadName):
|
||||
helpers.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
default:
|
||||
files.WritePathError(w, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
files.WritePathError(w, err)
|
||||
return
|
||||
}
|
||||
written, err := io.Copy(out, upload)
|
||||
closeErr := out.Close()
|
||||
if err != nil {
|
||||
helpers.Log.Printf("upload: %v", err)
|
||||
helpers.WriteError(w, http.StatusInternalServerError, "could not save file")
|
||||
return
|
||||
}
|
||||
if closeErr != nil {
|
||||
helpers.Log.Printf("upload close: %v", closeErr)
|
||||
helpers.WriteError(w, http.StatusInternalServerError, "could not save file")
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"path": target,
|
||||
"size": written,
|
||||
"name": filepath.Base(target),
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@@ -281,12 +341,20 @@ func (a *Agent) handleText(w http.ResponseWriter, r *http.Request) {
|
||||
helpers.WriteError(w, http.StatusBadRequest, "text is too long")
|
||||
return
|
||||
}
|
||||
if err := input.TypeText(request.Text); err != nil {
|
||||
delayMs := config.DefaultKeyDelayMs
|
||||
if request.DelayMs != nil {
|
||||
delayMs = input.ResolveKeyDelay(*request.DelayMs)
|
||||
}
|
||||
if err := input.TypeText(request.Text, delayMs); err != nil {
|
||||
helpers.Log.Printf("text: %v", err)
|
||||
helpers.WriteError(w, http.StatusInternalServerError, "could not type text")
|
||||
return
|
||||
}
|
||||
helpers.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "length": len(request.Text)})
|
||||
helpers.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"length": len(request.Text),
|
||||
"delay_ms": delayMs,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Agent) handleExec(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+38
-1
@@ -114,6 +114,22 @@
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadFile(file) {
|
||||
const dir = pathInput.value.trim();
|
||||
if (!dir) {
|
||||
throw new Error("open a folder first");
|
||||
}
|
||||
const form = new FormData();
|
||||
form.set("path", dir);
|
||||
form.set("file", file);
|
||||
showError("");
|
||||
const res = await fetch("/api/v1/upload", { method: "POST", body: form });
|
||||
if (!res.ok) {
|
||||
throw new Error(await readError(res));
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function listFiles(path) {
|
||||
const query = new URLSearchParams();
|
||||
if (path) query.set("path", path);
|
||||
@@ -284,10 +300,11 @@
|
||||
const field = document.getElementById("video-text");
|
||||
const text = field.value;
|
||||
if (!text) return;
|
||||
const delayMs = clamp(document.getElementById("video-key-delay").value, 0, 200);
|
||||
await api("/api/v1/input/text", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text }),
|
||||
body: JSON.stringify({ text, delay_ms: delayMs }),
|
||||
});
|
||||
field.value = "";
|
||||
}
|
||||
@@ -362,6 +379,21 @@
|
||||
document.getElementById("file-up").addEventListener("click", () => {
|
||||
listFiles(parentPath(pathInput.value.trim())).catch((err) => showError(err.message));
|
||||
});
|
||||
const filePicker = document.getElementById("file-picker");
|
||||
document.getElementById("file-upload").addEventListener("click", () => {
|
||||
filePicker.click();
|
||||
});
|
||||
filePicker.addEventListener("change", () => {
|
||||
const file = filePicker.files[0];
|
||||
filePicker.value = "";
|
||||
if (!file) return;
|
||||
uploadFile(file)
|
||||
.then((data) => {
|
||||
fileMeta.textContent = `uploaded ${data.name} (${formatBytes(data.size || 0)})`;
|
||||
return listFiles(pathInput.value.trim());
|
||||
})
|
||||
.catch((err) => showError(err.message));
|
||||
});
|
||||
document.getElementById("shot-capture").addEventListener("click", () => {
|
||||
captureScreen().catch((err) => showError(err.message));
|
||||
});
|
||||
@@ -379,6 +411,11 @@
|
||||
document.getElementById("video-send").addEventListener("click", () => {
|
||||
sendVideoText().catch((err) => showError(err.message));
|
||||
});
|
||||
const videoKeyDelay = document.getElementById("video-key-delay");
|
||||
const videoKeyDelayVal = document.getElementById("video-key-delay-val");
|
||||
videoKeyDelay.addEventListener("input", () => {
|
||||
videoKeyDelayVal.textContent = videoKeyDelay.value;
|
||||
});
|
||||
document.getElementById("video-text").addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -165,6 +165,8 @@
|
||||
<input id="file-path" type="text" placeholder="Directory path, e.g. C:\Users">
|
||||
<button id="file-up" type="button">Up</button>
|
||||
<button id="file-list" class="primary" type="button">List</button>
|
||||
<button id="file-upload" type="button">Upload</button>
|
||||
<input id="file-picker" type="file" hidden>
|
||||
</div>
|
||||
<p id="file-meta" class="meta"></p>
|
||||
<table>
|
||||
@@ -208,6 +210,10 @@
|
||||
<div class="row" style="margin-top:0.75rem">
|
||||
<input id="video-text" type="text" placeholder="Type text for the focused field">
|
||||
<button id="video-send" class="primary" type="button">Send</button>
|
||||
<label>Key delay
|
||||
<input id="video-key-delay" type="range" min="0" max="200" value="25" style="width:8rem; vertical-align:middle">
|
||||
<span id="video-key-delay-val">25</span> ms
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<section id="exec" class="panel">
|
||||
|
||||
Reference in New Issue
Block a user