package api import ( "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) // handleListDrops returns a JSON array of files in the drops directory. func (s *Server) handleListDrops(w http.ResponseWriter, r *http.Request) { if s.dropsDir == "" { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "drops directory not configured"}) return } entries, err := os.ReadDir(s.dropsDir) if err != nil { if os.IsNotExist(err) { writeJSON(w, http.StatusOK, []map[string]interface{}{}) return } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list drops"}) return } type fileEntry struct { Name string `json:"name"` Size int64 `json:"size"` Modified time.Time `json:"modified"` } files := []fileEntry{} for _, e := range entries { if e.IsDir() { continue } info, err := e.Info() if err != nil { continue } files = append(files, fileEntry{ Name: e.Name(), Size: info.Size(), Modified: info.ModTime().UTC(), }) } writeJSON(w, http.StatusOK, files) } // handleGetDrop serves a file from the drops directory as an attachment. func (s *Server) handleGetDrop(w http.ResponseWriter, r *http.Request) { if s.dropsDir == "" { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "drops directory not configured"}) return } filename := r.PathValue("filename") if strings.Contains(filename, "/") || strings.Contains(filename, "..") { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"}) return } path := filepath.Join(s.dropsDir, filepath.Clean(filename)) // Extra safety: ensure the resolved path is still inside dropsDir. if !strings.HasPrefix(path, s.dropsDir) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"}) return } f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"}) return } writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to open file"}) return } defer f.Close() w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) w.Header().Set("Content-Type", "application/octet-stream") io.Copy(w, f) //nolint:errcheck } // handlePostDrop accepts a file upload (multipart/form-data or raw body with ?filename=). func (s *Server) handlePostDrop(w http.ResponseWriter, r *http.Request) { if s.dropsDir == "" { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "drops directory not configured"}) return } if err := os.MkdirAll(s.dropsDir, 0700); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create drops directory"}) return } ct := r.Header.Get("Content-Type") if strings.Contains(ct, "multipart/form-data") { s.handleMultipartDrop(w, r) return } // Raw body with ?filename= query param. filename := r.URL.Query().Get("filename") if filename == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "filename query param required for raw upload"}) return } if strings.Contains(filename, "/") || strings.Contains(filename, "..") { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"}) return } path := filepath.Join(s.dropsDir, filename) data, err := io.ReadAll(r.Body) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read body"}) return } if err := os.WriteFile(path, data, 0600); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save file"}) return } writeJSON(w, http.StatusCreated, map[string]interface{}{"name": filename, "size": len(data)}) } func (s *Server) handleMultipartDrop(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MB limit writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to parse multipart form: " + err.Error()}) return } file, header, err := r.FormFile("file") if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'file' field: " + err.Error()}) return } defer file.Close() filename := filepath.Base(header.Filename) if filename == "" || filename == "." { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid filename"}) return } path := filepath.Join(s.dropsDir, filename) dst, err := os.Create(path) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create file"}) return } defer dst.Close() n, err := io.Copy(dst, file) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to write file"}) return } writeJSON(w, http.StatusCreated, map[string]interface{}{"name": filename, "size": n}) }