122 lines
2.6 KiB
Go
122 lines
2.6 KiB
Go
package store
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"speedtest/internal/probe"
|
|
)
|
|
|
|
type Store struct {
|
|
path string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
const maxRecordLineBytes = 64 * 1024 * 1024
|
|
|
|
type ClientInfo struct {
|
|
IP string `json:"ip"`
|
|
ISP string `json:"isp,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
Country string `json:"country,omitempty"`
|
|
City string `json:"city,omitempty"`
|
|
ASN string `json:"asn,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
}
|
|
|
|
type Record struct {
|
|
ID string `json:"id"`
|
|
Client ClientInfo `json:"client"`
|
|
Summary probe.Summary `json:"summary"`
|
|
Samples []probe.Sample `json:"samples"`
|
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
|
}
|
|
|
|
func New(path string) *Store {
|
|
return &Store{path: path}
|
|
}
|
|
|
|
func (s *Store) Append(ctx context.Context, record Record) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
|
return fmt.Errorf("create record directory: %w", err)
|
|
}
|
|
|
|
file, err := os.OpenFile(s.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("open record file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
encoder := json.NewEncoder(file)
|
|
if err := encoder.Encode(record); err != nil {
|
|
return fmt.Errorf("write record: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) List(ctx context.Context, limit int) ([]Record, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if limit < 1 {
|
|
limit = 100
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
file, err := os.Open(s.path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return []Record{}, nil
|
|
}
|
|
return nil, fmt.Errorf("open record file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var records []Record
|
|
scanner := bufio.NewScanner(file)
|
|
scanner.Buffer(make([]byte, 64*1024), maxRecordLineBytes)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var record Record
|
|
if err := json.Unmarshal([]byte(line), &record); err != nil {
|
|
return nil, fmt.Errorf("parse record line: %w", err)
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("read record file: %w", err)
|
|
}
|
|
|
|
reverse(records)
|
|
if len(records) > limit {
|
|
records = records[:limit]
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func reverse(records []Record) {
|
|
for left, right := 0, len(records)-1; left < right; left, right = left+1, right-1 {
|
|
records[left], records[right] = records[right], records[left]
|
|
}
|
|
}
|