screenshot-server/main.go

173 lines
4.1 KiB
Go

// screenshot-server: Serves the latest screenshot from ~/Desktop, then deletes it
// Run: go build -o screenshot-server && ./screenshot-server
// Fetch: curl http://mac-ip:9123/screenshot -o screenshot.png
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
const (
port = ":9123"
desktopDir = "" // Will be set to ~/Desktop
)
func getDesktopPath() string {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal("Cannot find home directory:", err)
}
return filepath.Join(home, "Desktop")
}
type screenshotFile struct {
path string
modTime time.Time
}
func findLatestScreenshot(desktop string) (string, error) {
entries, err := os.ReadDir(desktop)
if err != nil {
return "", err
}
var screenshots []screenshotFile
for _, e := range entries {
name := e.Name()
if strings.HasPrefix(name, "Screenshot") && strings.HasSuffix(name, ".png") {
info, err := e.Info()
if err != nil {
continue
}
screenshots = append(screenshots, screenshotFile{
path: filepath.Join(desktop, name),
modTime: info.ModTime(),
})
}
}
if len(screenshots) == 0 {
return "", fmt.Errorf("no screenshots found")
}
// Sort by modification time (newest first)
sort.Slice(screenshots, func(i, j int) bool {
return screenshots[i].modTime.After(screenshots[j].modTime)
})
return screenshots[0].path, nil
}
func serveAndDelete(w http.ResponseWriter, path string) {
f, err := os.Open(path)
if err != nil {
http.Error(w, "Cannot open file", http.StatusInternalServerError)
return
}
defer f.Close()
filename := filepath.Base(path)
w.Header().Set("Content-Type", "image/png")
w.Header().Set("X-Screenshot-Name", filename)
_, err = io.Copy(w, f)
if err != nil {
log.Println("Error sending:", err)
return
}
f.Close()
if err := os.Remove(path); err != nil {
log.Println("Warning: could not delete:", err)
} else {
log.Println("Served and deleted:", filename)
}
}
func screenshotHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "GET only", http.StatusMethodNotAllowed)
return
}
desktop := getDesktopPath()
path, err := findLatestScreenshot(desktop)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
log.Println("No screenshot:", err)
return
}
serveAndDelete(w, path)
}
func captureHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
display := r.URL.Query().Get("display")
if display == "" {
display = "1"
}
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("capture-%d.png", time.Now().UnixNano()))
cmd := exec.Command("screencapture", "-x", "-D", display, tmpFile)
if out, err := cmd.CombinedOutput(); err != nil {
http.Error(w, fmt.Sprintf("capture failed: %v: %s", err, out), http.StatusInternalServerError)
log.Printf("Capture failed (display %s): %v: %s", display, err, out)
os.Remove(tmpFile)
return
}
// Resize to 50% unless full resolution requested
if r.URL.Query().Get("full") != "1" {
if out, err := exec.Command("sips", "-g", "pixelWidth", tmpFile).Output(); err == nil {
re := regexp.MustCompile(`pixelWidth:\s*(\d+)`)
if m := re.FindSubmatch(out); m != nil {
if w, _ := strconv.Atoi(string(m[1])); w > 0 {
exec.Command("sips", "--resampleWidth", strconv.Itoa(w/2), tmpFile).Run()
}
}
}
}
log.Printf("Captured display %s", display)
serveAndDelete(w, tmpFile)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}
func main() {
http.HandleFunc("/screenshot", screenshotHandler)
http.HandleFunc("/capture", captureHandler)
http.HandleFunc("/health", healthHandler)
log.Printf("Screenshot server starting on %s", port)
log.Printf("Desktop: %s", getDesktopPath())
log.Printf("Endpoints:")
log.Printf(" GET /screenshot - fetch & delete latest screenshot")
log.Printf(" POST /capture?display=N - take screenshot of display N")
log.Printf(" GET /health - health check")
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal(err)
}
}