[All Platforms] 2.1.0 - Sync Optimizations
This commit is contained in:
309
sync/main.go
309
sync/main.go
@@ -13,7 +13,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const VERSION = "2.0.0"
|
||||
const VERSION = "2.1.0"
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
@@ -39,6 +39,24 @@ type ClimbDataBackup struct {
|
||||
DeletedItems []DeletedItem `json:"deletedItems"`
|
||||
}
|
||||
|
||||
type DeltaSyncRequest struct {
|
||||
LastSyncTime string `json:"lastSyncTime"`
|
||||
Gyms []BackupGym `json:"gyms"`
|
||||
Problems []BackupProblem `json:"problems"`
|
||||
Sessions []BackupClimbSession `json:"sessions"`
|
||||
Attempts []BackupAttempt `json:"attempts"`
|
||||
DeletedItems []DeletedItem `json:"deletedItems"`
|
||||
}
|
||||
|
||||
type DeltaSyncResponse struct {
|
||||
ServerTime string `json:"serverTime"`
|
||||
Gyms []BackupGym `json:"gyms"`
|
||||
Problems []BackupProblem `json:"problems"`
|
||||
Sessions []BackupClimbSession `json:"sessions"`
|
||||
Attempts []BackupAttempt `json:"attempts"`
|
||||
DeletedItems []DeletedItem `json:"deletedItems"`
|
||||
}
|
||||
|
||||
type BackupGym struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -154,6 +172,174 @@ func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
|
||||
return &backup, nil
|
||||
}
|
||||
|
||||
func (s *SyncServer) mergeGyms(existing []BackupGym, updates []BackupGym) []BackupGym {
|
||||
gymMap := make(map[string]BackupGym)
|
||||
for _, gym := range existing {
|
||||
gymMap[gym.ID] = gym
|
||||
}
|
||||
|
||||
for _, gym := range updates {
|
||||
if existingGym, exists := gymMap[gym.ID]; exists {
|
||||
// Keep newer version based on updatedAt timestamp
|
||||
if gym.UpdatedAt >= existingGym.UpdatedAt {
|
||||
gymMap[gym.ID] = gym
|
||||
}
|
||||
} else {
|
||||
gymMap[gym.ID] = gym
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]BackupGym, 0, len(gymMap))
|
||||
for _, gym := range gymMap {
|
||||
result = append(result, gym)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SyncServer) mergeProblems(existing []BackupProblem, updates []BackupProblem) []BackupProblem {
|
||||
problemMap := make(map[string]BackupProblem)
|
||||
for _, problem := range existing {
|
||||
problemMap[problem.ID] = problem
|
||||
}
|
||||
|
||||
for _, problem := range updates {
|
||||
if existingProblem, exists := problemMap[problem.ID]; exists {
|
||||
if problem.UpdatedAt >= existingProblem.UpdatedAt {
|
||||
problemMap[problem.ID] = problem
|
||||
}
|
||||
} else {
|
||||
problemMap[problem.ID] = problem
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]BackupProblem, 0, len(problemMap))
|
||||
for _, problem := range problemMap {
|
||||
result = append(result, problem)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SyncServer) mergeSessions(existing []BackupClimbSession, updates []BackupClimbSession) []BackupClimbSession {
|
||||
sessionMap := make(map[string]BackupClimbSession)
|
||||
for _, session := range existing {
|
||||
sessionMap[session.ID] = session
|
||||
}
|
||||
|
||||
for _, session := range updates {
|
||||
if existingSession, exists := sessionMap[session.ID]; exists {
|
||||
if session.UpdatedAt >= existingSession.UpdatedAt {
|
||||
sessionMap[session.ID] = session
|
||||
}
|
||||
} else {
|
||||
sessionMap[session.ID] = session
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]BackupClimbSession, 0, len(sessionMap))
|
||||
for _, session := range sessionMap {
|
||||
result = append(result, session)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SyncServer) mergeAttempts(existing []BackupAttempt, updates []BackupAttempt) []BackupAttempt {
|
||||
attemptMap := make(map[string]BackupAttempt)
|
||||
for _, attempt := range existing {
|
||||
attemptMap[attempt.ID] = attempt
|
||||
}
|
||||
|
||||
for _, attempt := range updates {
|
||||
if existingAttempt, exists := attemptMap[attempt.ID]; exists {
|
||||
if attempt.CreatedAt >= existingAttempt.CreatedAt {
|
||||
attemptMap[attempt.ID] = attempt
|
||||
}
|
||||
} else {
|
||||
attemptMap[attempt.ID] = attempt
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]BackupAttempt, 0, len(attemptMap))
|
||||
for _, attempt := range attemptMap {
|
||||
result = append(result, attempt)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SyncServer) mergeDeletedItems(existing []DeletedItem, updates []DeletedItem) []DeletedItem {
|
||||
deletedMap := make(map[string]DeletedItem)
|
||||
for _, item := range existing {
|
||||
key := item.Type + ":" + item.ID
|
||||
deletedMap[key] = item
|
||||
}
|
||||
|
||||
for _, item := range updates {
|
||||
key := item.Type + ":" + item.ID
|
||||
if existingItem, exists := deletedMap[key]; exists {
|
||||
if item.DeletedAt >= existingItem.DeletedAt {
|
||||
deletedMap[key] = item
|
||||
}
|
||||
} else {
|
||||
deletedMap[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]DeletedItem, 0, len(deletedMap))
|
||||
for _, item := range deletedMap {
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SyncServer) applyDeletions(backup *ClimbDataBackup, deletedItems []DeletedItem) {
|
||||
deletedMap := make(map[string]map[string]bool)
|
||||
for _, item := range deletedItems {
|
||||
if deletedMap[item.Type] == nil {
|
||||
deletedMap[item.Type] = make(map[string]bool)
|
||||
}
|
||||
deletedMap[item.Type][item.ID] = true
|
||||
}
|
||||
|
||||
if deletedMap["gym"] != nil {
|
||||
filtered := []BackupGym{}
|
||||
for _, gym := range backup.Gyms {
|
||||
if !deletedMap["gym"][gym.ID] {
|
||||
filtered = append(filtered, gym)
|
||||
}
|
||||
}
|
||||
backup.Gyms = filtered
|
||||
}
|
||||
|
||||
if deletedMap["problem"] != nil {
|
||||
filtered := []BackupProblem{}
|
||||
for _, problem := range backup.Problems {
|
||||
if !deletedMap["problem"][problem.ID] {
|
||||
filtered = append(filtered, problem)
|
||||
}
|
||||
}
|
||||
backup.Problems = filtered
|
||||
}
|
||||
|
||||
if deletedMap["session"] != nil {
|
||||
filtered := []BackupClimbSession{}
|
||||
for _, session := range backup.Sessions {
|
||||
if !deletedMap["session"][session.ID] {
|
||||
filtered = append(filtered, session)
|
||||
}
|
||||
}
|
||||
backup.Sessions = filtered
|
||||
}
|
||||
|
||||
if deletedMap["attempt"] != nil {
|
||||
filtered := []BackupAttempt{}
|
||||
for _, attempt := range backup.Attempts {
|
||||
if !deletedMap["attempt"][attempt.ID] {
|
||||
filtered = append(filtered, attempt)
|
||||
}
|
||||
}
|
||||
backup.Attempts = filtered
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SyncServer) saveData(backup *ClimbDataBackup) error {
|
||||
backup.ExportedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
@@ -167,7 +353,6 @@ func (s *SyncServer) saveData(backup *ClimbDataBackup) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure images directory exists
|
||||
if err := os.MkdirAll(s.imagesDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -315,6 +500,123 @@ func (s *SyncServer) handleImageDownload(w http.ResponseWriter, r *http.Request)
|
||||
w.Write(imageData)
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.authenticate(r) {
|
||||
log.Printf("Unauthorized delta sync attempt from %s", r.RemoteAddr)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var deltaRequest DeltaSyncRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&deltaRequest); err != nil {
|
||||
log.Printf("Invalid JSON from %s: %v", r.RemoteAddr, err)
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Delta sync from %s: lastSyncTime=%s, gyms=%d, problems=%d, sessions=%d, attempts=%d, deletedItems=%d",
|
||||
r.RemoteAddr, deltaRequest.LastSyncTime,
|
||||
len(deltaRequest.Gyms), len(deltaRequest.Problems),
|
||||
len(deltaRequest.Sessions), len(deltaRequest.Attempts),
|
||||
len(deltaRequest.DeletedItems))
|
||||
|
||||
// Load current server data
|
||||
serverBackup, err := s.loadData()
|
||||
if err != nil {
|
||||
log.Printf("Failed to load data: %v", err)
|
||||
http.Error(w, "Failed to load data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Merge client changes into server data
|
||||
serverBackup.Gyms = s.mergeGyms(serverBackup.Gyms, deltaRequest.Gyms)
|
||||
serverBackup.Problems = s.mergeProblems(serverBackup.Problems, deltaRequest.Problems)
|
||||
serverBackup.Sessions = s.mergeSessions(serverBackup.Sessions, deltaRequest.Sessions)
|
||||
serverBackup.Attempts = s.mergeAttempts(serverBackup.Attempts, deltaRequest.Attempts)
|
||||
serverBackup.DeletedItems = s.mergeDeletedItems(serverBackup.DeletedItems, deltaRequest.DeletedItems)
|
||||
|
||||
// Apply deletions to remove deleted items
|
||||
s.applyDeletions(serverBackup, serverBackup.DeletedItems)
|
||||
|
||||
// Save merged data
|
||||
if err := s.saveData(serverBackup); err != nil {
|
||||
log.Printf("Failed to save data: %v", err)
|
||||
http.Error(w, "Failed to save data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse client's last sync time
|
||||
clientLastSync, err := time.Parse(time.RFC3339, deltaRequest.LastSyncTime)
|
||||
if err != nil {
|
||||
// If parsing fails, send everything
|
||||
clientLastSync = time.Time{}
|
||||
}
|
||||
|
||||
// Prepare response with items modified since client's last sync
|
||||
response := DeltaSyncResponse{
|
||||
ServerTime: time.Now().UTC().Format(time.RFC3339),
|
||||
Gyms: []BackupGym{},
|
||||
Problems: []BackupProblem{},
|
||||
Sessions: []BackupClimbSession{},
|
||||
Attempts: []BackupAttempt{},
|
||||
DeletedItems: []DeletedItem{},
|
||||
}
|
||||
|
||||
// Filter gyms modified after client's last sync
|
||||
for _, gym := range serverBackup.Gyms {
|
||||
gymTime, err := time.Parse(time.RFC3339, gym.UpdatedAt)
|
||||
if err == nil && gymTime.After(clientLastSync) {
|
||||
response.Gyms = append(response.Gyms, gym)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter problems modified after client's last sync
|
||||
for _, problem := range serverBackup.Problems {
|
||||
problemTime, err := time.Parse(time.RFC3339, problem.UpdatedAt)
|
||||
if err == nil && problemTime.After(clientLastSync) {
|
||||
response.Problems = append(response.Problems, problem)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter sessions modified after client's last sync
|
||||
for _, session := range serverBackup.Sessions {
|
||||
sessionTime, err := time.Parse(time.RFC3339, session.UpdatedAt)
|
||||
if err == nil && sessionTime.After(clientLastSync) {
|
||||
response.Sessions = append(response.Sessions, session)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter attempts created after client's last sync
|
||||
for _, attempt := range serverBackup.Attempts {
|
||||
attemptTime, err := time.Parse(time.RFC3339, attempt.CreatedAt)
|
||||
if err == nil && attemptTime.After(clientLastSync) {
|
||||
response.Attempts = append(response.Attempts, attempt)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter deletions after client's last sync
|
||||
for _, deletedItem := range serverBackup.DeletedItems {
|
||||
deletedTime, err := time.Parse(time.RFC3339, deletedItem.DeletedAt)
|
||||
if err == nil && deletedTime.After(clientLastSync) {
|
||||
response.DeletedItems = append(response.DeletedItems, deletedItem)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Delta sync response to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d, deletedItems=%d",
|
||||
r.RemoteAddr,
|
||||
len(response.Gyms), len(response.Problems),
|
||||
len(response.Sessions), len(response.Attempts),
|
||||
len(response.DeletedItems))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (s *SyncServer) handleSync(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
@@ -354,6 +656,7 @@ func main() {
|
||||
}
|
||||
|
||||
http.HandleFunc("/sync", server.handleSync)
|
||||
http.HandleFunc("/sync/delta", server.handleDeltaSync)
|
||||
http.HandleFunc("/health", server.handleHealth)
|
||||
http.HandleFunc("/images/upload", server.handleImageUpload)
|
||||
http.HandleFunc("/images/download", server.handleImageDownload)
|
||||
@@ -362,6 +665,8 @@ func main() {
|
||||
fmt.Printf("Data file: %s\n", dataFile)
|
||||
fmt.Printf("Images directory: %s\n", imagesDir)
|
||||
fmt.Printf("Health check available at /health\n")
|
||||
fmt.Printf("Delta sync: POST /sync/delta (incremental sync)\n")
|
||||
fmt.Printf("Full sync: GET /sync (download all), PUT /sync (upload all)\n")
|
||||
fmt.Printf("Image upload: POST /images/upload?filename=<name>\n")
|
||||
fmt.Printf("Image download: GET /images/download?filename=<name>\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user