174 lines
4.1 KiB
Go
174 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/appstate"
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
type projectTimestampRow struct {
|
|
UUID string
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type updatePlanRow struct {
|
|
UUID string
|
|
Code string
|
|
Variant string
|
|
LocalUpdatedAt time.Time
|
|
ServerUpdatedAt time.Time
|
|
}
|
|
|
|
func main() {
|
|
defaultLocalDBPath, err := appstate.ResolveDBPath("")
|
|
if err != nil {
|
|
log.Fatalf("failed to resolve default local SQLite path: %v", err)
|
|
}
|
|
|
|
localDBPath := flag.String("localdb", defaultLocalDBPath, "path to local SQLite database (default: user state dir or QFS_DB_PATH)")
|
|
apply := flag.Bool("apply", false, "apply updates to local SQLite (default is preview only)")
|
|
flag.Parse()
|
|
|
|
local, err := localdb.New(*localDBPath)
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize local database: %v", err)
|
|
}
|
|
defer local.Close()
|
|
|
|
if !local.HasSettings() {
|
|
log.Fatalf("SQLite connection settings are not configured. Run qfs setup first.")
|
|
}
|
|
|
|
dsn, err := local.GetDSN()
|
|
if err != nil {
|
|
log.Fatalf("failed to build DSN from SQLite settings: %v", err)
|
|
}
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("failed to connect to MariaDB: %v", err)
|
|
}
|
|
|
|
serverRows, err := loadServerProjects(db)
|
|
if err != nil {
|
|
log.Fatalf("failed to load server projects: %v", err)
|
|
}
|
|
|
|
localProjects, err := local.GetAllProjects(true)
|
|
if err != nil {
|
|
log.Fatalf("failed to load local projects: %v", err)
|
|
}
|
|
|
|
plan := buildUpdatePlan(localProjects, serverRows)
|
|
printPlan(plan, *apply)
|
|
|
|
if !*apply || len(plan) == 0 {
|
|
return
|
|
}
|
|
|
|
updated := 0
|
|
for i := range plan {
|
|
project, err := local.GetProjectByUUID(plan[i].UUID)
|
|
if err != nil {
|
|
log.Printf("skip %s: load local project: %v", plan[i].UUID, err)
|
|
continue
|
|
}
|
|
project.UpdatedAt = plan[i].ServerUpdatedAt
|
|
if err := local.SaveProjectPreservingUpdatedAt(project); err != nil {
|
|
log.Printf("skip %s: save local project: %v", plan[i].UUID, err)
|
|
continue
|
|
}
|
|
updated++
|
|
}
|
|
|
|
log.Printf("updated %d local project timestamps", updated)
|
|
}
|
|
|
|
func loadServerProjects(db *gorm.DB) (map[string]time.Time, error) {
|
|
var rows []projectTimestampRow
|
|
if err := db.Model(&models.Project{}).
|
|
Select("uuid, updated_at").
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make(map[string]time.Time, len(rows))
|
|
for _, row := range rows {
|
|
if row.UUID == "" {
|
|
continue
|
|
}
|
|
out[row.UUID] = row.UpdatedAt
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func buildUpdatePlan(localProjects []localdb.LocalProject, serverRows map[string]time.Time) []updatePlanRow {
|
|
plan := make([]updatePlanRow, 0)
|
|
for i := range localProjects {
|
|
project := localProjects[i]
|
|
serverUpdatedAt, ok := serverRows[project.UUID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if project.UpdatedAt.Equal(serverUpdatedAt) {
|
|
continue
|
|
}
|
|
plan = append(plan, updatePlanRow{
|
|
UUID: project.UUID,
|
|
Code: project.Code,
|
|
Variant: project.Variant,
|
|
LocalUpdatedAt: project.UpdatedAt,
|
|
ServerUpdatedAt: serverUpdatedAt,
|
|
})
|
|
}
|
|
|
|
sort.Slice(plan, func(i, j int) bool {
|
|
if plan[i].Code != plan[j].Code {
|
|
return plan[i].Code < plan[j].Code
|
|
}
|
|
return plan[i].Variant < plan[j].Variant
|
|
})
|
|
|
|
return plan
|
|
}
|
|
|
|
func printPlan(plan []updatePlanRow, apply bool) {
|
|
mode := "preview"
|
|
if apply {
|
|
mode = "apply"
|
|
}
|
|
log.Printf("project updated_at resync mode=%s changes=%d", mode, len(plan))
|
|
if len(plan) == 0 {
|
|
log.Printf("no local project timestamps need resync")
|
|
return
|
|
}
|
|
for _, row := range plan {
|
|
variant := row.Variant
|
|
if variant == "" {
|
|
variant = "main"
|
|
}
|
|
log.Printf("%s [%s] local=%s server=%s", row.Code, variant, formatStamp(row.LocalUpdatedAt), formatStamp(row.ServerUpdatedAt))
|
|
}
|
|
if !apply {
|
|
fmt.Println("Re-run with -apply to write server updated_at into local SQLite.")
|
|
}
|
|
}
|
|
|
|
func formatStamp(value time.Time) string {
|
|
if value.IsZero() {
|
|
return "zero"
|
|
}
|
|
return value.Format(time.RFC3339)
|
|
}
|