81 lines
2.3 KiB
Go
81 lines
2.3 KiB
Go
package datapoint
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"archive/zip"
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
MaxBatchBytes = 4 << 20 // Maximum ZIP payload size on the wire.
|
||
|
|
MaxBatchJSONBytes = 16 << 20 // Maximum expanded JSON size.
|
||
|
|
)
|
||
|
|
|
||
|
|
// ZipBatch packages GPS fixes into an in-memory ZIP containing points.json.
|
||
|
|
func ZipBatch(points []DataPoint) ([]byte, error) {
|
||
|
|
raw, err := json.Marshal(points)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("encode points: %w", err)
|
||
|
|
}
|
||
|
|
if len(raw) > MaxBatchJSONBytes {
|
||
|
|
return nil, fmt.Errorf("batch JSON exceeds %d bytes", MaxBatchJSONBytes)
|
||
|
|
}
|
||
|
|
var buf bytes.Buffer
|
||
|
|
zw := zip.NewWriter(&buf)
|
||
|
|
file, err := zw.Create("points.json")
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if _, err = file.Write(raw); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if err = zw.Close(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if buf.Len() > MaxBatchBytes {
|
||
|
|
return nil, fmt.Errorf("ZIP batch exceeds %d bytes", MaxBatchBytes)
|
||
|
|
}
|
||
|
|
return buf.Bytes(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// UnzipBatch reads points in memory; archive entries are never written to disk.
|
||
|
|
// Legacy JSON batches are accepted so the server can be upgraded first.
|
||
|
|
func UnzipBatch(payload []byte) ([]DataPoint, error) {
|
||
|
|
if len(payload) > MaxBatchBytes {
|
||
|
|
return nil, fmt.Errorf("batch exceeds %d bytes", MaxBatchBytes)
|
||
|
|
}
|
||
|
|
raw := payload
|
||
|
|
if !bytes.HasPrefix(bytes.TrimSpace(payload), []byte("[")) && !bytes.Equal(bytes.TrimSpace(payload), []byte("null")) {
|
||
|
|
zr, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("open ZIP batch: %w", err)
|
||
|
|
}
|
||
|
|
if len(zr.File) != 1 || zr.File[0].Name != "points.json" {
|
||
|
|
return nil, fmt.Errorf("ZIP batch must contain only points.json")
|
||
|
|
}
|
||
|
|
file := zr.File[0]
|
||
|
|
if file.UncompressedSize64 > MaxBatchJSONBytes {
|
||
|
|
return nil, fmt.Errorf("expanded batch exceeds %d bytes", MaxBatchJSONBytes)
|
||
|
|
}
|
||
|
|
r, err := file.Open()
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("open points.json: %w", err)
|
||
|
|
}
|
||
|
|
defer r.Close()
|
||
|
|
raw, err = io.ReadAll(io.LimitReader(r, MaxBatchJSONBytes+1))
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("read points.json: %w", err)
|
||
|
|
}
|
||
|
|
if len(raw) > MaxBatchJSONBytes {
|
||
|
|
return nil, fmt.Errorf("expanded batch exceeds %d bytes", MaxBatchJSONBytes)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var points []DataPoint
|
||
|
|
if err := json.Unmarshal(raw, &points); err != nil {
|
||
|
|
return nil, fmt.Errorf("decode points: %w", err)
|
||
|
|
}
|
||
|
|
return points, nil
|
||
|
|
}
|