docs.go
| 1 | package web |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "net/http" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | |
| 11 | "forge.gpl-nas.de/physiobase/internal/store" |
| 12 | ) |
| 13 | |
| 14 | const maxDocUpload = 300 << 20 // 300 MB pro Upload (mehrere Scans auf einmal) |
| 15 | |
| 16 | // Scans und Fotos. HEIC/TIFF werden gespeichert, aber vom Browser meist nicht angezeigt (→ Download). |
| 17 | var docExt = map[string]bool{ |
| 18 | ".pdf": true, ".jpg": true, ".jpeg": true, ".png": true, ".webp": true, ".gif": true, |
| 19 | ".heic": true, ".heif": true, ".tif": true, ".tiff": true, |
| 20 | } |
| 21 | |
| 22 | func isPreviewable(f string) bool { return isImage(f) } |
| 23 | |
| 24 | func (s *Server) uploadDocs(w http.ResponseWriter, r *http.Request) { |
| 25 | ctx := r.Context() |
| 26 | pid, ok := pathID(r, "id") |
| 27 | if !ok { |
| 28 | http.NotFound(w, r) |
| 29 | return |
| 30 | } |
| 31 | if _, err := s.st.Patient(ctx, pid); err != nil { |
| 32 | s.fail(w, r, err) |
| 33 | return |
| 34 | } |
| 35 | r.Body = http.MaxBytesReader(w, r.Body, maxDocUpload) |
| 36 | if err := r.ParseMultipartForm(32 << 20); err != nil { //nolint:gosec // G120: Größe oben per MaxBytesReader begrenzt |
| 37 | http.Error(w, "Upload fehlgeschlagen (max. 300 MB auf einmal)", http.StatusBadRequest) |
| 38 | return |
| 39 | } |
| 40 | defer func() { _ = r.MultipartForm.RemoveAll() }() |
| 41 | |
| 42 | files := r.MultipartForm.File["files"] |
| 43 | if len(files) == 0 { |
| 44 | http.Error(w, "Keine Datei ausgewählt", http.StatusBadRequest) |
| 45 | return |
| 46 | } |
| 47 | title, category, date := formText(r, "title"), formText(r, "category"), validDate(formText(r, "date")) |
| 48 | for i, fh := range files { |
| 49 | ext := extOf(fh.Filename) |
| 50 | if !docExt[ext] { |
| 51 | http.Error(w, "Dateityp nicht erlaubt: "+fh.Filename+" (PDF oder Foto)", http.StatusBadRequest) |
| 52 | return |
| 53 | } |
| 54 | name := fmt.Sprintf("%d-%s%s", pid, randomHex(10), ext) |
| 55 | if err := saveUpload(s.cfg.DocsDir, fh.Open, name); err != nil { |
| 56 | s.fail(w, r, err) |
| 57 | return |
| 58 | } |
| 59 | t := title |
| 60 | switch { |
| 61 | case t == "": |
| 62 | t = strings.TrimSuffix(filepath.Base(fh.Filename), filepath.Ext(fh.Filename)) |
| 63 | case len(files) > 1: |
| 64 | t = fmt.Sprintf("%s (%d/%d)", title, i+1, len(files)) |
| 65 | } |
| 66 | d := store.Doc{PatientID: pid, Filename: name, Original: filepath.Base(fh.Filename), Title: t, Category: category, Date: date, Size: fh.Size} |
| 67 | if err := s.st.AddDoc(ctx, d); err != nil { |
| 68 | os.Remove(filepath.Join(s.cfg.DocsDir, name)) //nolint:gosec // G703: Name vom Server erzeugt |
| 69 | s.fail(w, r, err) |
| 70 | return |
| 71 | } |
| 72 | } |
| 73 | redirect(w, r, "/patient/%d#dokumente", pid) |
| 74 | } |
| 75 | |
| 76 | func (s *Server) deleteDoc(w http.ResponseWriter, r *http.Request) { |
| 77 | id, ok := pathID(r, "id") |
| 78 | if !ok { |
| 79 | http.NotFound(w, r) |
| 80 | return |
| 81 | } |
| 82 | d, err := s.st.DeleteDoc(r.Context(), id) |
| 83 | if err != nil { |
| 84 | s.fail(w, r, err) |
| 85 | return |
| 86 | } |
| 87 | s.removeDocFile(d.Filename) |
| 88 | redirect(w, r, "/patient/%d#dokumente", d.PatientID) |
| 89 | } |
| 90 | |
| 91 | func (s *Server) removeDocFile(name string) { |
| 92 | err := os.Remove(filepath.Join(s.cfg.DocsDir, name)) //nolint:gosec // G703: Name stammt aus der Datenbank |
| 93 | if err != nil && !errors.Is(err, os.ErrNotExist) { |
| 94 | s.log.Warn("dokument nicht gelöscht", "datei", name, "err", err) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func (s *Server) serveDoc(w http.ResponseWriter, r *http.Request) { |
| 99 | name := r.PathValue("file") |
| 100 | if name != filepath.Base(name) || strings.HasPrefix(name, ".") { |
| 101 | http.NotFound(w, r) |
| 102 | return |
| 103 | } |
| 104 | d, err := s.st.DocByFile(r.Context(), name) |
| 105 | if err != nil { |
| 106 | s.fail(w, r, err) |
| 107 | return |
| 108 | } |
| 109 | // Gesundheitsdaten: nicht in geteilten Caches ablegen |
| 110 | w.Header().Set("Cache-Control", "private, no-store") |
| 111 | disposition := "inline" |
| 112 | if !isPreviewable(name) && extOf(name) != ".pdf" { |
| 113 | disposition = "attachment" // HEIC/TIFF kann der Browser meist nicht anzeigen |
| 114 | } |
| 115 | w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename*=UTF-8''%s`, disposition, urlPathEscape(d.Original))) |
| 116 | http.ServeFile(w, r, filepath.Join(s.cfg.DocsDir, name)) //nolint:gosec // G703: Name geprüft und in der DB bekannt |
| 117 | } |
| 118 |