package api import ( "encoding/json" "io" "log" "net/http" "os" "path/filepath" "strings" "docproc/processor" ) type Server struct { proc *processor.Processor } func NewServer(proc *processor.Processor) *Server { return &Server{proc: proc} } func (s *Server) Start(addr string) error { mux := http.NewServeMux() mux.HandleFunc("/health", s.handleHealth) mux.HandleFunc("/ingest", s.handleIngest) mux.HandleFunc("/search", s.handleSearch) mux.HandleFunc("/docs", s.handleList) mux.HandleFunc("/doc/", s.handleDoc) log.Printf("API server starting on %s", addr) return http.ListenAndServe(addr, s.corsMiddleware(mux)) } func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func (s *Server) handleIngest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse multipart form if err := r.ParseMultipartForm(50 << 20); err != nil { // 50MB max http.Error(w, "Failed to parse form: "+err.Error(), http.StatusBadRequest) return } file, header, err := r.FormFile("file") if err != nil { http.Error(w, "No file provided: "+err.Error(), http.StatusBadRequest) return } defer file.Close() // Save to inbox homeDir, _ := os.UserHomeDir() inboxPath := filepath.Join(homeDir, "documents", "inbox", header.Filename) out, err := os.Create(inboxPath) if err != nil { http.Error(w, "Failed to save file: "+err.Error(), http.StatusInternalServerError) return } defer out.Close() if _, err := io.Copy(out, file); err != nil { http.Error(w, "Failed to write file: "+err.Error(), http.StatusInternalServerError) return } // Process immediately if err := s.proc.ProcessFile(inboxPath); err != nil { http.Error(w, "Failed to process file: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "processed", "message": "Document processed successfully", }) } func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { http.Error(w, "Query parameter 'q' is required", http.StatusBadRequest) return } results := s.proc.Search(query) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "query": query, "count": len(results), "results": results, }) } func (s *Server) handleList(w http.ResponseWriter, r *http.Request) { docs := s.proc.ListDocuments() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "count": len(docs), "docs": docs, }) } func (s *Server) handleDoc(w http.ResponseWriter, r *http.Request) { // Extract ID from path: /doc/{id} id := strings.TrimPrefix(r.URL.Path, "/doc/") if id == "" { http.Error(w, "Document ID required", http.StatusBadRequest) return } doc := s.proc.GetDocument(id) if doc == nil { http.Error(w, "Document not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(doc) }