diff --git a/stacks/terminal/clipboard-upload/go.mod b/stacks/terminal/clipboard-upload/go.mod new file mode 100644 index 00000000..2c842e6e --- /dev/null +++ b/stacks/terminal/clipboard-upload/go.mod @@ -0,0 +1,3 @@ +module clipboard-upload + +go 1.25.0 diff --git a/stacks/terminal/clipboard-upload/main.go b/stacks/terminal/clipboard-upload/main.go new file mode 100644 index 00000000..99ba9367 --- /dev/null +++ b/stacks/terminal/clipboard-upload/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +const ( + uploadDir = "/tmp/clipboard-images" + maxUpload = 10 << 20 // 10MB + listenAddr = "0.0.0.0:7683" +) + +func main() { + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Fatalf("Failed to create upload dir: %v", err) + } + + http.HandleFunc("/upload", handleUpload) + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("ok")) + }) + + log.Printf("Clipboard upload service listening on %s, saving to %s", listenAddr, uploadDir) + log.Fatal(http.ListenAndServe(listenAddr, nil)) +} + +func handleUpload(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "POST only", http.StatusMethodNotAllowed) + return + } + + r.Body = http.MaxBytesReader(w, r.Body, maxUpload) + if err := r.ParseMultipartForm(maxUpload); err != nil { + http.Error(w, "File too large (max 10MB)", http.StatusRequestEntityTooLarge) + return + } + + file, header, err := r.FormFile("image") + if err != nil { + http.Error(w, "Missing 'image' field", http.StatusBadRequest) + return + } + defer file.Close() + + ct := header.Header.Get("Content-Type") + if !strings.HasPrefix(ct, "image/") { + http.Error(w, "Not an image", http.StatusBadRequest) + return + } + + ext := ".png" + switch ct { + case "image/jpeg": + ext = ".jpg" + case "image/gif": + ext = ".gif" + case "image/webp": + ext = ".webp" + } + + randBytes := make([]byte, 4) + rand.Read(randBytes) + filename := fmt.Sprintf("%s-%s%s", time.Now().Format("20060102-150405"), hex.EncodeToString(randBytes), ext) + destPath := filepath.Join(uploadDir, filename) + + dest, err := os.Create(destPath) + if err != nil { + http.Error(w, "Failed to save", http.StatusInternalServerError) + return + } + defer dest.Close() + + if _, err := io.Copy(dest, file); err != nil { + os.Remove(destPath) + http.Error(w, "Failed to save", http.StatusInternalServerError) + return + } + + log.Printf("Saved clipboard image: %s (%s, %d bytes)", destPath, ct, header.Size) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"path": destPath}) +} diff --git a/stacks/terminal/files/index.html b/stacks/terminal/files/index.html new file mode 100644 index 00000000..548e7552 --- /dev/null +++ b/stacks/terminal/files/index.html @@ -0,0 +1,231 @@ + + +
+ + +