package server import ( "encoding/csv" "fmt" "io" "net/http" "strings" ) // exportFormats maps ?format= values to file extension and content type. var exportFormats = map[string]struct{ ext, contentType string }{ "csv": {"csv", "text/csv; charset=utf-8"}, "txt": {"txt", "text/plain; charset=utf-8"}, } // handleExport downloads a list's titles in rank order, one per line, with // nothing else: no ranks, header row or metadata. func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { format := r.URL.Query().Get("format") if format == "" { format = "txt" } f, ok := exportFormats[format] if !ok { http.Error(w, `format must be "csv" or "txt"`, http.StatusBadRequest) return } l, ok := s.visibleList(w, r) if !ok { return } items, err := s.store.Items(r.Context(), l.ID, 0, 0) if err != nil { s.serverError(w, r, err) return } titles := make([]string, len(items)) for i, it := range items { titles[i] = it.Title } w.Header().Set("Content-Type", f.contentType) w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.%s"`, l.Slug, f.ext)) w.Header().Set("Cache-Control", "no-store") if err := writeTitles(w, format, titles); err != nil { s.log.Warn("export failed", "list", l.Slug, "err", err) } } // flatten keeps one title per line even if a title contains a newline. var flatten = strings.NewReplacer("\r\n", " ", "\n", " ", "\r", " ") func writeTitles(w io.Writer, format string, titles []string) error { if format == "csv" { // The BOM makes Excel read accented titles (Les Misérables) as UTF-8. if _, err := io.WriteString(w, "\xef\xbb\xbf"); err != nil { return err } cw := csv.NewWriter(w) cw.UseCRLF = true for _, t := range titles { if err := cw.Write([]string{flatten.Replace(t)}); err != nil { return err } } cw.Flush() return cw.Error() } for _, t := range titles { if _, err := io.WriteString(w, flatten.Replace(t)+"\n"); err != nil { return err } } return nil }