Documentation
¶
Overview ¶
Package export streams CSV, JSON and NDJSON downloads row by row, so a large export never sits in memory:
return export.CSV(c.Writer(), c.Request(), "users.csv", []string{"id", "email"},
orm.QueryOf[User](db).Each(c.Context()),
func(u User) []string { return []string{strconv.FormatInt(u.ID, 10), u.Email} })
Responses are attachments with a sanitized file name (RFC 6266, with an RFC 5987 filename* for non-ASCII names), Cache-Control: no-store and nosniff. CSV cells that a spreadsheet would run as a formula are written as text. Nothing is sent before the first row, so a query that fails up front still reaches the error handler; a failure after that aborts the connection so the client sees a failed download, not a truncated file.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#exports.
Index ¶
- Constants
- Variables
- func CSV[T any](w http.ResponseWriter, r *http.Request, filename string, header []string, ...) error
- func ContentDisposition(filename string) string
- func JSON[T any](w http.ResponseWriter, r *http.Request, filename string, ...) error
- func NDJSON[T any](w http.ResponseWriter, r *http.Request, filename string, ...) error
- func SafeCell(s string) string
- func WriteCSV(w http.ResponseWriter, r *http.Request, filename string, ...) error
- type CSVWriter
- type Option
Examples ¶
Constants ¶
const ( // DefaultFlushEvery is the number of rows between flushes. DefaultFlushEvery = 500 // DefaultWriteTimeout is how long each flush window may take before // the write deadline cuts a stalled client. DefaultWriteTimeout = time.Minute )
Variables ¶
var ErrIncomplete = errors.New("export: response incomplete")
ErrIncomplete wraps a failure after the response started: the client got part of the export and the connection was dropped.
Functions ¶
func CSV ¶
func CSV[T any]( w http.ResponseWriter, r *http.Request, filename string, header []string, rows iter.Seq2[T, error], record func(T) []string, opts ...Option, ) error
CSV streams rows as a CSV attachment: header first, then record(row) for each row. Pass orm Query.Each as rows to read from the database lazily.
Example ¶
users := seq(user{1, "Ana"}, user{2, "=SUM(A1:A9)"})
handler := func(w http.ResponseWriter, r *http.Request) {
// With the ORM: orm.QueryOf[User](db).Each(r.Context()) as rows.
err := export.CSV(w, r, "users.csv", []string{"id", "name"}, users,
func(u user) []string { return []string{strconv.Itoa(u.ID), u.Name} })
// Before the first row nothing was sent and an error page still fits;
// ErrIncomplete means the download already started and was aborted.
if err != nil && !errors.Is(err, export.ErrIncomplete) {
http.Error(w, "export failed", http.StatusInternalServerError)
return
}
}
rec := httptest.NewRecorder()
handler(rec, httptest.NewRequest(http.MethodGet, "/users.csv", http.NoBody))
fmt.Println(rec.Header().Get("Content-Disposition"))
fmt.Print(rec.Body.String())
Output: attachment; filename="users.csv" id,name 1,Ana 2,'=SUM(A1:A9)
func ContentDisposition ¶
ContentDisposition returns an attachment Content-Disposition for filename (RFC 6266). The name is reduced to its last path element (either separator) and loses control characters (C0, C1, U+2028/2029), format characters (bidi overrides, zero-width), leading dots, trailing dots and spaces and the characters Windows forbids; Windows device names (CON, NUL, COM1...) get a "_" prefix; it is cut to 200 characters keeping the extension, and is "download" when nothing is left. Non-ASCII names get an ASCII filename plus an RFC 5987 filename*.
func JSON ¶
func JSON[T any]( w http.ResponseWriter, r *http.Request, filename string, rows iter.Seq2[T, error], opts ...Option, ) error
JSON streams rows as a JSON array attachment, one element per row.
func NDJSON ¶
func NDJSON[T any]( w http.ResponseWriter, r *http.Request, filename string, rows iter.Seq2[T, error], opts ...Option, ) error
NDJSON streams rows as newline-delimited JSON (application/x-ndjson), one object per line.
func SafeCell ¶
SafeCell guards a CSV cell against formula injection (OWASP CSV injection): a cell starting with =, +, -, @, tab or carriage return gets a leading apostrophe so spreadsheets show it as text. Some spreadsheets skip leading spaces, line breaks and quotes, so a cell whose first other character is =, +, - or @ is guarded too; the trade-off is that text such as " - note" or "'@handle" also gains the apostrophe. Plain decimal numbers ("-42", "+3.14") cannot run anything and stay as they are.
Types ¶
type CSVWriter ¶
type CSVWriter struct {
// contains filtered or unexported fields
}
CSVWriter writes CSV records to an export started by WriteCSV.
type Option ¶
type Option func(*options)
Option configures an export.
func FlushEvery ¶
FlushEvery flushes the response after every n rows (values below 1 flush after every row). The end of the export is always flushed.
func InsecureRawCells ¶
func InsecureRawCells() Option
InsecureRawCells writes CSV cells as given, without the formula-injection guard. Use it only for files never opened in a spreadsheet.
func WithBOM ¶
func WithBOM() Option
WithBOM starts a CSV export with a UTF-8 byte order mark, which Excel needs to read non-ASCII text correctly. JSON and NDJSON ignore it.
func WriteTimeout ¶
WriteTimeout keeps the write deadline d ahead of every write to the client, so a long export outlives the server's WriteTimeout while a client that stops reading for d is still cut. The deadline is cleared when the export ends. Zero or negative keeps the server's deadline.