Refactor database handling and API endpoints

- Removed fsnotify dependency and related file watching logic.
- Introduced SQLite database for storing user profiles and history.
- Created new API endpoints for fetching latest profiles and user history.
- Migrated existing JSON data to the new database structure.
- Simplified HTML and JavaScript for fetching and displaying profile data.
- Improved error handling and logging throughout the application.
This commit is contained in:
2025-06-12 22:42:22 +02:00
parent 1ed17d152f
commit 5084c70295
5 changed files with 471 additions and 474 deletions
+174
View File
@@ -0,0 +1,174 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"os"
"time"
_ "github.com/mattn/go-sqlite3"
)
// ProfileData struct to match the structure in data.json
type ProfileData struct {
Owner string `json:"owner"`
Timestamp time.Time `json:"timestamp"`
Rank int `json:"rank"`
Upload string `json:"upload"`
CurrentUpload string `json:"current_upload"`
CurrentDownload string `json:"current_download"`
Points int `json:"points"`
SeedingCount int `json:"seeding_count"`
}
var (
db *sql.DB
dbFile = "../data/ncore_stats.db"
jsonFile = "../data/data.json"
profilesFile = "../data/profiles.json"
)
// initDB is the same function from the main app to set up the database.
func initDB() {
var err error
if err := os.MkdirAll("./data", 0755); err != nil {
log.Fatalf("Failed to create data directory: %v", err)
}
db, err = sql.Open("sqlite3", dbFile)
if err != nil {
log.Fatalf("Failed to open database: %v", err)
}
// Create users table
usersTableSQL := `
CREATE TABLE IF NOT EXISTS users (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"display_name" TEXT NOT NULL UNIQUE,
"profile_id" TEXT NOT NULL
);`
if _, err = db.Exec(usersTableSQL); err != nil {
log.Fatalf("Failed to create users table: %v", err)
}
// Create profile_history table
profileHistoryTableSQL := `
CREATE TABLE IF NOT EXISTS profile_history (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL,
"timestamp" DATETIME NOT NULL,
"rank" INTEGER,
"upload" TEXT,
"current_upload" TEXT,
"current_download" TEXT,
"points" INTEGER,
"seeding_count" INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);`
if _, err = db.Exec(profileHistoryTableSQL); err != nil {
log.Fatalf("Failed to create profile_history table: %v", err)
}
log.Println("Database initialized successfully.")
}
func main() {
log.Println("Starting data migration...")
// 1. Initialize the database and tables
initDB()
defer db.Close()
// 2. Begin a transaction
tx, err := db.Begin()
if err != nil {
log.Fatalf("Failed to begin transaction: %v", err)
}
// Defer a rollback. If the transaction is committed, this is a no-op.
// If something fails, this will undo all changes.
defer tx.Rollback()
// --- Migrate Users ---
log.Println("Reading profiles.json...")
profFile, err := os.Open(profilesFile)
if err != nil {
log.Fatalf("Failed to open profiles file: %v", err)
}
defer profFile.Close()
var profiles map[string]string
if err := json.NewDecoder(profFile).Decode(&profiles); err != nil {
log.Fatalf("Failed to decode profiles.json: %v", err)
}
// This map will hold the new database ID for each user display name
displayNameToID := make(map[string]int64)
userStmt, err := tx.Prepare("INSERT INTO users(display_name, profile_id) VALUES(?, ?)")
if err != nil {
log.Fatalf("Failed to prepare user insert statement: %v", err)
}
defer userStmt.Close()
log.Println("Migrating users to the database...")
for name, id := range profiles {
res, err := userStmt.Exec(name, id)
if err != nil {
log.Fatalf("Failed to insert user %s: %v", name, err)
}
newID, err := res.LastInsertId()
if err != nil {
log.Fatalf("Failed to get last insert ID for user %s: %v", name, err)
}
displayNameToID[name] = newID
log.Printf(" > Migrated user: %s (New DB ID: %d)", name, newID)
}
log.Println("User migration complete.")
// --- Migrate History ---
log.Println("Reading data.json...")
histFile, err := os.Open(jsonFile)
if err != nil {
log.Fatalf("Failed to open data file: %v", err)
}
defer histFile.Close()
byteValue, _ := io.ReadAll(histFile)
var history []ProfileData
if err := json.Unmarshal(byteValue, &history); err != nil {
log.Fatalf("Failed to decode data.json: %v", err)
}
historyStmt, err := tx.Prepare(`
INSERT INTO profile_history(user_id, timestamp, rank, upload, current_upload, current_download, points, seeding_count)
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
log.Fatalf("Failed to prepare history insert statement: %v", err)
}
defer historyStmt.Close()
log.Println("Migrating profile history to the database...")
for i, record := range history {
userID, ok := displayNameToID[record.Owner]
if !ok {
log.Printf("WARNING: Skipping history record for '%s' as they were not found in profiles.json.", record.Owner)
continue
}
_, err := historyStmt.Exec(userID, record.Timestamp, record.Rank, record.Upload, record.CurrentUpload, record.CurrentDownload, record.Points, record.SeedingCount)
if err != nil {
log.Fatalf("Failed to insert history record for %s: %v", record.Owner, err)
}
if (i+1)%100 == 0 { // Log progress every 100 records
log.Printf(" > Migrated %d history records...", i+1)
}
}
log.Println("History migration complete.")
// 3. Commit the transaction
if err := tx.Commit(); err != nil {
log.Fatalf("Failed to commit transaction: %v", err)
}
fmt.Println("\n✅ MIGRATION COMPLETED SUCCESSFULLY!")
}