mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-04 04:22:57 +00:00
396 lines
9.8 KiB
Go
396 lines
9.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// UpdateInfo represents information about an available update
|
|
type UpdateInfo struct {
|
|
Version string `json:"version"`
|
|
ReleaseNotes string `json:"releaseNotes"`
|
|
DownloadURL string `json:"downloadUrl"`
|
|
Mandatory bool `json:"mandatory"`
|
|
Size string `json:"size"`
|
|
}
|
|
|
|
// UpdateStatus represents the current status of an update
|
|
type UpdateStatus struct {
|
|
Available bool `json:"available"`
|
|
Downloading bool `json:"downloading"`
|
|
Installing bool `json:"installing"`
|
|
Completed bool `json:"completed"`
|
|
Error string `json:"error,omitempty"`
|
|
Progress float64 `json:"progress"`
|
|
}
|
|
|
|
// UpdateRequest represents an update installation request
|
|
type UpdateRequest struct {
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// Global update state
|
|
var (
|
|
updateMutex sync.RWMutex
|
|
currentUpdate *UpdateInfo
|
|
updateProgress *UpdateStatus
|
|
)
|
|
|
|
func init() {
|
|
updateProgress = &UpdateStatus{
|
|
Available: false,
|
|
Downloading: false,
|
|
Installing: false,
|
|
Completed: false,
|
|
Error: "",
|
|
Progress: 0,
|
|
}
|
|
}
|
|
|
|
// CheckForUpdates checks if a new version is available
|
|
func CheckForUpdates(c *gin.Context) {
|
|
updateMutex.Lock()
|
|
defer updateMutex.Unlock()
|
|
|
|
// Get current version from environment or default
|
|
currentVersion := os.Getenv("APP_VERSION")
|
|
if currentVersion == "" {
|
|
currentVersion = "1.0.0"
|
|
}
|
|
|
|
// In a real implementation, this would check against a remote update server
|
|
// For demo purposes, we'll simulate checking for updates
|
|
latestVersion, updateAvailable := simulateUpdateCheck(currentVersion)
|
|
|
|
if updateAvailable {
|
|
currentUpdate = &UpdateInfo{
|
|
Version: latestVersion,
|
|
ReleaseNotes: "• New AI features added\n• Performance improvements\n• Bug fixes and security patches\n• Enhanced user interface",
|
|
DownloadURL: "https://github.com/trackeep/trackeep/releases/latest",
|
|
Mandatory: false,
|
|
Size: "~25MB",
|
|
}
|
|
updateProgress.Available = true
|
|
} else {
|
|
currentUpdate = nil
|
|
updateProgress.Available = false
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"updateAvailable": updateAvailable,
|
|
"currentVersion": currentVersion,
|
|
"latestVersion": latestVersion,
|
|
"updateInfo": currentUpdate,
|
|
})
|
|
}
|
|
|
|
// InstallUpdate starts the update installation process
|
|
func InstallUpdate(c *gin.Context) {
|
|
var req UpdateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
|
|
return
|
|
}
|
|
|
|
updateMutex.Lock()
|
|
defer updateMutex.Unlock()
|
|
|
|
if currentUpdate == nil || currentUpdate.Version != req.Version {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Update not available"})
|
|
return
|
|
}
|
|
|
|
if updateProgress.Downloading || updateProgress.Installing {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Update already in progress"})
|
|
return
|
|
}
|
|
|
|
// Start update process in background
|
|
go performUpdate(currentUpdate)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Update started",
|
|
"version": req.Version,
|
|
})
|
|
}
|
|
|
|
// GetUpdateProgress returns the current update progress
|
|
func GetUpdateProgress(c *gin.Context) {
|
|
updateMutex.RLock()
|
|
defer updateMutex.RUnlock()
|
|
|
|
c.JSON(http.StatusOK, updateProgress)
|
|
}
|
|
|
|
// WebSocket endpoint for real-time update progress
|
|
func UpdateProgressWebSocket(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "WebSocket support not implemented, using polling instead",
|
|
"progress": updateProgress,
|
|
})
|
|
}
|
|
|
|
// simulateUpdateCheck simulates checking for updates
|
|
func simulateUpdateCheck(currentVersion string) (string, bool) {
|
|
// Simulate version check - in reality this would call an update API
|
|
versions := []string{"1.0.1", "1.1.0", "1.2.0"}
|
|
|
|
// For demo, always return a newer version
|
|
if len(versions) > 0 {
|
|
return versions[0], true
|
|
}
|
|
|
|
return currentVersion, false
|
|
}
|
|
|
|
// performUpdate performs the actual update process
|
|
func performUpdate(updateInfo *UpdateInfo) {
|
|
updateMutex.Lock()
|
|
updateProgress.Downloading = true
|
|
updateProgress.Progress = 0
|
|
updateProgress.Error = ""
|
|
updateMutex.Unlock()
|
|
|
|
// Broadcast progress update
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
|
|
// Simulate download
|
|
for i := 0; i <= 100; i += 10 {
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
updateMutex.Lock()
|
|
updateProgress.Progress = float64(i)
|
|
updateMutex.Unlock()
|
|
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
}
|
|
|
|
// Start installation
|
|
updateMutex.Lock()
|
|
updateProgress.Downloading = false
|
|
updateProgress.Installing = true
|
|
updateProgress.Progress = 0
|
|
updateMutex.Unlock()
|
|
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
|
|
// Backup user data
|
|
if err := backupUserData(); err != nil {
|
|
updateMutex.Lock()
|
|
updateProgress.Installing = false
|
|
updateProgress.Error = fmt.Sprintf("Failed to backup user data: %v", err)
|
|
updateMutex.Unlock()
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
return
|
|
}
|
|
|
|
// Simulate installation
|
|
for i := 0; i <= 100; i += 20 {
|
|
time.Sleep(1 * time.Second)
|
|
|
|
updateMutex.Lock()
|
|
updateProgress.Progress = float64(i)
|
|
updateMutex.Unlock()
|
|
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
}
|
|
|
|
// Perform the actual update
|
|
if err := applyUpdate(updateInfo); err != nil {
|
|
updateMutex.Lock()
|
|
updateProgress.Installing = false
|
|
updateProgress.Error = fmt.Sprintf("Failed to apply update: %v", err)
|
|
updateMutex.Unlock()
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
return
|
|
}
|
|
|
|
// Mark as completed
|
|
updateMutex.Lock()
|
|
updateProgress.Installing = false
|
|
updateProgress.Completed = true
|
|
updateProgress.Progress = 100
|
|
updateMutex.Unlock()
|
|
|
|
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
|
|
|
// Trigger application restart after a delay
|
|
time.Sleep(2 * time.Second)
|
|
restartApplication()
|
|
}
|
|
|
|
// backupUserData creates a backup of user data
|
|
func backupUserData() error {
|
|
backupDir := filepath.Join(os.TempDir(), "trackeep_backup", time.Now().Format("20060102_150405"))
|
|
|
|
// Create backup directory
|
|
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Backup database
|
|
dbPath := os.Getenv("DB_PATH")
|
|
if dbPath == "" {
|
|
dbPath = "./trackeep.db"
|
|
}
|
|
|
|
if _, err := os.Stat(dbPath); err == nil {
|
|
backupDBPath := filepath.Join(backupDir, "trackeep.db")
|
|
if err := copyFile(dbPath, backupDBPath); err != nil {
|
|
return fmt.Errorf("failed to backup database: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backup uploads directory
|
|
uploadsDir := "./uploads"
|
|
if _, err := os.Stat(uploadsDir); err == nil {
|
|
backupUploadsDir := filepath.Join(backupDir, "uploads")
|
|
if err := copyDirectory(uploadsDir, backupUploadsDir); err != nil {
|
|
return fmt.Errorf("failed to backup uploads: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backup configuration files
|
|
configFiles := []string{".env", "docker-compose.yml"}
|
|
for _, file := range configFiles {
|
|
if _, err := os.Stat(file); err == nil {
|
|
backupFile := filepath.Join(backupDir, file)
|
|
if err := copyFile(file, backupFile); err != nil {
|
|
log.Printf("Warning: failed to backup %s: %v", file, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("User data backed up to: %s", backupDir)
|
|
return nil
|
|
}
|
|
|
|
// applyUpdate applies the update
|
|
func applyUpdate(updateInfo *UpdateInfo) error {
|
|
// In a real implementation, this would:
|
|
// 1. Download the new version
|
|
// 2. Verify checksums
|
|
// 3. Extract/update files
|
|
// 4. Run database migrations if needed
|
|
// 5. Restore user data if necessary
|
|
|
|
log.Printf("Applying update to version %s", updateInfo.Version)
|
|
|
|
// Simulate file update
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Update version in environment
|
|
os.Setenv("APP_VERSION", updateInfo.Version)
|
|
|
|
return nil
|
|
}
|
|
|
|
// restartApplication restarts the application
|
|
func restartApplication() {
|
|
log.Println("Restarting application to complete update...")
|
|
|
|
// Create a new process to replace the current one
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
log.Printf("Failed to get executable path: %v", err)
|
|
return
|
|
}
|
|
|
|
// Use different commands based on OS
|
|
var cmd *exec.Cmd
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
cmd = exec.Command("powershell", "-Command", "Start-Sleep 2; "+executable)
|
|
default:
|
|
cmd = exec.Command("sh", "-c", fmt.Sprintf("sleep 2 && %s", executable))
|
|
}
|
|
|
|
// Start the new process
|
|
if err := cmd.Start(); err != nil {
|
|
log.Printf("Failed to start new process: %v", err)
|
|
return
|
|
}
|
|
|
|
// Exit the current process
|
|
os.Exit(0)
|
|
}
|
|
|
|
// broadcastProgress broadcasts update progress to all WebSocket clients (simplified version)
|
|
func broadcastProgress() {
|
|
updateMutex.RLock()
|
|
progress := *updateProgress
|
|
updateMutex.RUnlock()
|
|
|
|
log.Printf("Update progress: %.1f%% - Status: %v", progress.Progress, getUpdateStatusString(progress))
|
|
}
|
|
|
|
// getUpdateStatusString returns a human-readable status string
|
|
func getUpdateStatusString(status UpdateStatus) string {
|
|
if status.Completed {
|
|
return "Completed"
|
|
}
|
|
if status.Error != "" {
|
|
return "Error: " + status.Error
|
|
}
|
|
if status.Installing {
|
|
return "Installing"
|
|
}
|
|
if status.Downloading {
|
|
return "Downloading"
|
|
}
|
|
if status.Available {
|
|
return "Available"
|
|
}
|
|
return "Not Available"
|
|
}
|
|
|
|
// copyFile copies a file from src to dst
|
|
func copyFile(src, dst string) error {
|
|
sourceFile, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
destFile, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = io.Copy(destFile, sourceFile)
|
|
return err
|
|
}
|
|
|
|
// copyDirectory copies a directory recursively
|
|
func copyDirectory(src, dst string) error {
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, err := filepath.Rel(src, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dstPath := filepath.Join(dst, relPath)
|
|
|
|
if info.IsDir() {
|
|
return os.MkdirAll(dstPath, info.Mode())
|
|
}
|
|
|
|
return copyFile(path, dstPath)
|
|
})
|
|
}
|