Initial orientator
This commit is contained in:
164
internal/server/server.go
Normal file
164
internal/server/server.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"orientator/internal/pipeline"
|
||||
)
|
||||
|
||||
//go:embed all:web
|
||||
var webFS embed.FS
|
||||
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func New() *Server {
|
||||
s := &Server{mux: http.NewServeMux()}
|
||||
sub, _ := fs.Sub(webFS, "web")
|
||||
s.mux.Handle("/", http.FileServer(http.FS(sub)))
|
||||
s.mux.HandleFunc("/api/process", s.handleProcess)
|
||||
s.mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler { return s.mux }
|
||||
|
||||
const maxUploadBytes = 2 << 30 // 2 GiB
|
||||
|
||||
func (s *Server) handleProcess(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes)
|
||||
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
files := r.MultipartForm.File["files"]
|
||||
if len(files) == 0 {
|
||||
writeErr(w, http.StatusBadRequest, errors.New("no files"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(files) == 1 {
|
||||
fh := files[0]
|
||||
f, err := fh.Open()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
res, err := pipeline.Process(fh.Filename, data)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
writeFile(w, res.OutputName, res.OutputBytes, res.Reports)
|
||||
return
|
||||
}
|
||||
|
||||
// Multiple files: synthesize a tar/zip wrapping name and pack outputs.
|
||||
combined := &combinedZip{}
|
||||
for _, fh := range files {
|
||||
f, err := fh.Open()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
res, err := pipeline.Process(fh.Filename, data)
|
||||
if err != nil {
|
||||
combined.addError(fh.Filename, err)
|
||||
continue
|
||||
}
|
||||
combined.add(res)
|
||||
}
|
||||
out, reports, err := combined.finalize()
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
writeFile(w, "orientator-output.zip", out, reports)
|
||||
}
|
||||
|
||||
func writeErr(w http.ResponseWriter, code int, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
func writeFile(w http.ResponseWriter, name string, data []byte, reports []pipeline.PageReport) {
|
||||
rep, _ := json.Marshal(reports)
|
||||
w.Header().Set("Content-Type", contentTypeFor(name))
|
||||
w.Header().Set("Content-Disposition", contentDisposition(name))
|
||||
w.Header().Set("X-Orientator-Reports", url.QueryEscape(string(rep)))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func contentDisposition(name string) string {
|
||||
base := filepath.Base(name)
|
||||
return fmt.Sprintf(
|
||||
"attachment; filename=%q; filename*=UTF-8''%s",
|
||||
asciiFilename(base),
|
||||
url.PathEscape(base),
|
||||
)
|
||||
}
|
||||
|
||||
func asciiFilename(name string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(name))
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r < utf8.RuneSelf && (unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '-' || r == '_'):
|
||||
b.WriteRune(r)
|
||||
case unicode.IsSpace(r):
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
if b.Len() == 0 {
|
||||
return "output"
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func contentTypeFor(name string) string {
|
||||
switch strings.ToLower(filepath.Ext(name)) {
|
||||
case ".pdf":
|
||||
return "application/pdf"
|
||||
case ".zip":
|
||||
return "application/zip"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".tif", ".tiff":
|
||||
return "image/tiff"
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
Reference in New Issue
Block a user