You've already forked speedtest-go
Sync PHP backend feature parity: IP detection, database backends, API endpoints, and frontend
- IP detection: Cloudflare IPv6, ULA IPv6, proxy header chain, offline GeoIP DB - Database: add SQLite (pure Go, no CGo) and MSSQL backends - API: add JSON result sharing endpoint and ID obfuscation - Frontend: add modern CSS design, design switcher, favicon - Compatibility: ?cors parameter support, human-friendly distance rounding - Update Go to 1.21, add modernc.org/sqlite and maxminddb deps
This commit is contained in:
@@ -4,10 +4,12 @@ import (
|
||||
"github.com/librespeed/speedtest-go/config"
|
||||
"github.com/librespeed/speedtest-go/database/bolt"
|
||||
"github.com/librespeed/speedtest-go/database/memory"
|
||||
"github.com/librespeed/speedtest-go/database/mssql"
|
||||
"github.com/librespeed/speedtest-go/database/mysql"
|
||||
"github.com/librespeed/speedtest-go/database/none"
|
||||
"github.com/librespeed/speedtest-go/database/postgresql"
|
||||
"github.com/librespeed/speedtest-go/database/schema"
|
||||
"github.com/librespeed/speedtest-go/database/sqlite"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -30,6 +32,10 @@ func SetDBInfo(conf *config.Config) {
|
||||
DB = mysql.Open(conf.DatabaseHostname, conf.DatabaseUsername, conf.DatabasePassword, conf.DatabaseName)
|
||||
case "bolt":
|
||||
DB = bolt.Open(conf.DatabaseFile)
|
||||
case "sqlite":
|
||||
DB = sqlite.Open(conf.DatabaseFile)
|
||||
case "mssql":
|
||||
DB = mssql.Open(conf.DatabaseHostname, conf.DatabaseUsername, conf.DatabasePassword, conf.DatabaseName, conf.DatabasePort)
|
||||
case "memory":
|
||||
DB = memory.Open("")
|
||||
case "none":
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/librespeed/speedtest-go/database/schema"
|
||||
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MSSQL struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func Open(hostname, username, password, database, port string) *MSSQL {
|
||||
if port == "" {
|
||||
port = "1433"
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Add("database", database)
|
||||
|
||||
connStr := fmt.Sprintf("sqlserver://%s:%s@%s:%s?%s",
|
||||
url.QueryEscape(username),
|
||||
url.QueryEscape(password),
|
||||
hostname,
|
||||
port,
|
||||
query.Encode(),
|
||||
)
|
||||
|
||||
conn, err := sql.Open("sqlserver", connStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot open MSSQL database: %s", err)
|
||||
}
|
||||
|
||||
return &MSSQL{db: conn}
|
||||
}
|
||||
|
||||
func (p *MSSQL) Insert(data *schema.TelemetryData) error {
|
||||
stmt := `INSERT INTO speedtest_users (ip, ispinfo, extra, ua, lang, dl, ul, ping, jitter, log, uuid)
|
||||
VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);`
|
||||
_, err := p.db.Exec(stmt,
|
||||
data.IPAddress, data.ISPInfo, data.Extra, data.UserAgent, data.Language,
|
||||
data.Download, data.Upload, data.Ping, data.Jitter, data.Log, data.UUID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *MSSQL) FetchByUUID(uuid string) (*schema.TelemetryData, error) {
|
||||
var record schema.TelemetryData
|
||||
row := p.db.QueryRow(`SELECT * FROM speedtest_users WHERE uuid = @p1`, uuid)
|
||||
if row != nil {
|
||||
var id int64
|
||||
if err := row.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
|
||||
return nil, fmt.Errorf("mssql fetch by uuid: %w", err)
|
||||
}
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (p *MSSQL) FetchLast100() ([]schema.TelemetryData, error) {
|
||||
var records []schema.TelemetryData
|
||||
rows, err := p.db.Query(`SELECT TOP 100 * FROM speedtest_users ORDER BY timestamp DESC;`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mssql fetch last 100: %w", err)
|
||||
}
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var record schema.TelemetryData
|
||||
var id int64
|
||||
if err := rows.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
|
||||
return nil, fmt.Errorf("mssql scan row: %w", err)
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
--
|
||||
-- MSSQL database schema for speedtest telemetry
|
||||
--
|
||||
|
||||
CREATE TABLE [dbo].[speedtest_users](
|
||||
[id] [bigint] IDENTITY(120,1) NOT NULL,
|
||||
[timestamp] [datetime] NOT NULL,
|
||||
[ip] [nvarchar](max) NOT NULL,
|
||||
[ispinfo] [nvarchar](max) NULL,
|
||||
[extra] [nvarchar](max) NULL,
|
||||
[ua] [nvarchar](max) NOT NULL,
|
||||
[lang] [nvarchar](max) NOT NULL,
|
||||
[dl] [nvarchar](max) NULL,
|
||||
[ul] [nvarchar](max) NULL,
|
||||
[ping] [nvarchar](max) NULL,
|
||||
[jitter] [nvarchar](max) NULL,
|
||||
[log] [nvarchar](max) NULL,
|
||||
[uuid] [nvarchar](max) NULL,
|
||||
CONSTRAINT [PK_speedtest_users] PRIMARY KEY CLUSTERED
|
||||
(
|
||||
[id] ASC
|
||||
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
|
||||
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
|
||||
GO
|
||||
|
||||
ALTER TABLE [dbo].[speedtest_users] ADD CONSTRAINT [DF_speedtest_users_timestamp] DEFAULT (getdate()) FOR [timestamp]
|
||||
GO
|
||||
@@ -0,0 +1,95 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/librespeed/speedtest-go/database/schema"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SQLite struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func Open(databaseFile string) *SQLite {
|
||||
conn, err := sql.Open("sqlite", databaseFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot open SQLite database: %s", err)
|
||||
}
|
||||
|
||||
// Enable WAL mode for better concurrent performance
|
||||
if _, err := conn.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
log.Warnf("Failed to set SQLite journal mode to WAL: %s", err)
|
||||
}
|
||||
|
||||
// Create table if not exists (matching the PHP SQLite auto-creation behavior)
|
||||
stmt := `CREATE TABLE IF NOT EXISTS speedtest_users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ip TEXT NOT NULL,
|
||||
ispinfo TEXT,
|
||||
extra TEXT,
|
||||
ua TEXT NOT NULL,
|
||||
lang TEXT NOT NULL,
|
||||
dl TEXT,
|
||||
ul TEXT,
|
||||
ping TEXT,
|
||||
jitter TEXT,
|
||||
log TEXT,
|
||||
uuid TEXT
|
||||
);`
|
||||
if _, err := conn.Exec(stmt); err != nil {
|
||||
log.Fatalf("Failed to create speedtest_users table: %s", err)
|
||||
}
|
||||
|
||||
return &SQLite{db: conn}
|
||||
}
|
||||
|
||||
func (p *SQLite) Insert(data *schema.TelemetryData) error {
|
||||
var existingID int
|
||||
// Check for duplicate UUID first
|
||||
err := p.db.QueryRow(`SELECT id FROM speedtest_users WHERE uuid = ?`, data.UUID).Scan(&existingID)
|
||||
if err == nil {
|
||||
// Record with this UUID already exists - skip insert
|
||||
return nil
|
||||
}
|
||||
|
||||
stmt := `INSERT INTO speedtest_users (ip, ispinfo, extra, ua, lang, dl, ul, ping, jitter, log, uuid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
_, err = p.db.Exec(stmt, data.IPAddress, data.ISPInfo, data.Extra, data.UserAgent, data.Language, data.Download, data.Upload, data.Ping, data.Jitter, data.Log, data.UUID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *SQLite) FetchByUUID(uuid string) (*schema.TelemetryData, error) {
|
||||
var record schema.TelemetryData
|
||||
row := p.db.QueryRow(`SELECT * FROM speedtest_users WHERE uuid = ?`, uuid)
|
||||
if row != nil {
|
||||
var id int
|
||||
if err := row.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
|
||||
return nil, fmt.Errorf("sqlite fetch by uuid: %w", err)
|
||||
}
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (p *SQLite) FetchLast100() ([]schema.TelemetryData, error) {
|
||||
var records []schema.TelemetryData
|
||||
rows, err := p.db.Query(`SELECT * FROM speedtest_users ORDER BY timestamp DESC LIMIT 100;`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sqlite fetch last 100: %w", err)
|
||||
}
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var record schema.TelemetryData
|
||||
var id int
|
||||
if err := rows.Scan(&id, &record.Timestamp, &record.IPAddress, &record.ISPInfo, &record.Extra, &record.UserAgent, &record.Language, &record.Download, &record.Upload, &record.Ping, &record.Jitter, &record.Log, &record.UUID); err != nil {
|
||||
return nil, fmt.Errorf("sqlite scan row: %w", err)
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
--
|
||||
-- SQLite database schema for speedtest telemetry
|
||||
-- Auto-created by the sqlite backend if it doesn't exist.
|
||||
-- This file is provided for reference / manual setup.
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `speedtest_users` (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ip` TEXT NOT NULL,
|
||||
`ispinfo` TEXT,
|
||||
`extra` TEXT,
|
||||
`ua` TEXT NOT NULL,
|
||||
`lang` TEXT NOT NULL,
|
||||
`dl` TEXT,
|
||||
`ul` TEXT,
|
||||
`ping` TEXT,
|
||||
`jitter` TEXT,
|
||||
`log` TEXT,
|
||||
`uuid` TEXT
|
||||
);
|
||||
Reference in New Issue
Block a user