165 lines
3.7 KiB
Go
165 lines
3.7 KiB
Go
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"
|
|
}
|