116 lines
2.7 KiB
Go
116 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
pb "github.com/bergabruh/stats-gateway/gen/stats/v1"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func main() {
|
|
dbPath := flag.String("source-db", "/var/lib/librespeed-db/speedtest_telemetry.db",
|
|
"Path to LibreSpeed SQLite (read-only)")
|
|
statsAddr := flag.String("stats-addr", "stats.projectshitpost.fun:443",
|
|
"stats-gateway gRPC address")
|
|
cursorPath := flag.String("cursor", "/var/lib/speedtest-sync/cursor",
|
|
"File holding the last synced source ID")
|
|
tokenEnv := flag.String("token-env", "JWT_TOKEN",
|
|
"Environment variable with JWT token")
|
|
insecureFlag := flag.Bool("insecure", false, "Use insecure gRPC (testing only)")
|
|
flag.Parse()
|
|
|
|
token := os.Getenv(*tokenEnv)
|
|
if token == "" {
|
|
log.Fatalf("env %s is empty", *tokenEnv)
|
|
}
|
|
|
|
cursor := readCursor(*cursorPath)
|
|
|
|
db, err := sql.Open("sqlite", "file:"+*dbPath+"?mode=ro")
|
|
if err != nil {
|
|
log.Fatalf("open source db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
rows, err := extractRows(ctx, db, cursor)
|
|
if err != nil {
|
|
log.Fatalf("extract: %v", err)
|
|
}
|
|
if len(rows) == 0 {
|
|
log.Printf("no new rows since id=%d", cursor)
|
|
return
|
|
}
|
|
|
|
var dialOpt grpc.DialOption
|
|
if *insecureFlag {
|
|
dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
} else {
|
|
dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(nil))
|
|
}
|
|
conn, err := grpc.NewClient(*statsAddr, dialOpt)
|
|
if err != nil {
|
|
log.Fatalf("grpc dial: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := pb.NewSpeedStatusClient(conn)
|
|
|
|
authCtx := metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
|
|
sent := 0
|
|
for _, r := range rows {
|
|
if err := sendRow(authCtx, client, r); err != nil {
|
|
log.Printf("send id=%d failed: %v (stopping; cursor not advanced)", r.SourceID, err)
|
|
break
|
|
}
|
|
cursor = r.SourceID
|
|
sent++
|
|
}
|
|
|
|
if err := writeCursor(*cursorPath, cursor); err != nil {
|
|
log.Printf("WARN: cursor write failed: %v", err)
|
|
}
|
|
log.Printf("synced %d rows, cursor=%d", sent, cursor)
|
|
}
|
|
|
|
func readCursor(path string) int64 {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
n, err := strconv.ParseInt(string(b), 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
func writeCursor(path string, id int64) error {
|
|
if err := os.MkdirAll(parentDir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(strconv.FormatInt(id, 10)), 0o600)
|
|
}
|
|
|
|
func parentDir(p string) string {
|
|
for i := len(p) - 1; i >= 0; i-- {
|
|
if p[i] == '/' {
|
|
return p[:i]
|
|
}
|
|
}
|
|
return "."
|
|
}
|