package api import ( "encoding/json" "net/http" "sort" "strconv" "strings" "time" "clicd/internal/config" ) func HandleSnapshots(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"}) return } snapshots := append([]config.Snapshot(nil), config.AppConfig.Snapshots...) sortSnapshotsNewestFirst(snapshots) jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: snapshots}) } func handleContainerSnapshots(w http.ResponseWriter, r *http.Request, containerID int, action string) { switch { case action == "snapshots" && r.Method == http.MethodGet: listContainerSnapshots(w, r, containerID) case action == "snapshots" && r.Method == http.MethodPost: createContainerSnapshot(w, r, containerID) case action == "snapshots/schedule" && r.Method == http.MethodPost: updateSnapshotSchedule(w, r, containerID) case action == "snapshots/quota" && r.Method == http.MethodPut: updateSnapshotQuota(w, r, containerID) case strings.HasPrefix(action, "snapshots/") && strings.HasSuffix(action, "/restore") && r.Method == http.MethodPost: snapshotID := strings.TrimSuffix(strings.TrimPrefix(action, "snapshots/"), "/restore") restoreContainerSnapshot(w, r, containerID, snapshotID) case strings.HasPrefix(action, "snapshots/") && r.Method == http.MethodDelete: snapshotID := strings.TrimPrefix(action, "snapshots/") deleteContainerSnapshot(w, r, containerID, snapshotID) default: jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Snapshot action not found"}) } } func listContainerSnapshots(w http.ResponseWriter, r *http.Request, containerID int) { c := config.FindContainer(containerID) if c == nil { jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"}) return } snapshots := config.ContainerSnapshots(containerID) sortSnapshotsNewestFirst(snapshots) jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]interface{}{ "snapshots": snapshots, "quota": config.ContainerSnapshotLimit(c), "schedule": map[string]interface{}{ "enabled": c.SnapshotScheduleEnabled, "interval_hours": c.SnapshotScheduleIntervalHours, "time": c.SnapshotScheduleTime, "last_run": c.SnapshotScheduleLastRun, "next_run": c.SnapshotScheduleNextRun, "created_by": c.SnapshotScheduleCreatedBy, }, }}) } func createContainerSnapshot(w http.ResponseWriter, r *http.Request, containerID int) { user := requestUser(r) if isSubUserRequest(r) { c := config.FindContainer(containerID) limit := config.ContainerSnapshotLimit(c) if len(config.ContainerSnapshots(containerID)) >= limit { jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Snapshot quota reached. Delete an old snapshot first."}) return } } snapshot, err := lxcManager.CreateSnapshot(containerID, user, false, 0) if err != nil { jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()}) return } config.AddAuditLog("snapshot.create", snapshot.ContainerName, snapshot.ID, user) jsonResponse(w, http.StatusCreated, APIResponse{Success: true, Data: snapshot}) } func updateSnapshotQuota(w http.ResponseWriter, r *http.Request, containerID int) { if isSubUserRequest(r) { jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Sub-users cannot change snapshot quota"}) return } var req struct { SnapshotLimit int `json:"snapshot_limit"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"}) return } if req.SnapshotLimit <= 0 { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Snapshot quota must be at least 1"}) return } c := config.FindContainer(containerID) if c == nil { jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"}) return } c.SnapshotLimit = req.SnapshotLimit if err := config.SaveConfig(); err != nil { jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to save config"}) return } user := requestUser(r) config.AddAuditLog("snapshot.quota", c.Name, "limit="+strconv.Itoa(req.SnapshotLimit), user) jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]interface{}{ "container": c, "quota": c.SnapshotLimit, }}) } func updateSnapshotSchedule(w http.ResponseWriter, r *http.Request, containerID int) { var req struct { Enabled bool `json:"enabled"` IntervalHours int `json:"interval_hours"` Time string `json:"time"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"}) return } if req.IntervalHours <= 0 { req.IntervalHours = 24 } if req.IntervalHours < 24 { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Snapshot schedule interval cannot be less than 24 hours"}) return } if req.Time == "" { req.Time = "03:00" } user := requestUser(r) c, err := lxcManager.SetSnapshotSchedule(containerID, req.Enabled, req.IntervalHours, req.Time, user) if err != nil { jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()}) return } if req.Enabled { config.AddAuditLog("snapshot.schedule", c.Name, "enabled", user) } else { config.AddAuditLog("snapshot.schedule", c.Name, "disabled", user) } jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]interface{}{ "container": c, }}) } func deleteContainerSnapshot(w http.ResponseWriter, r *http.Request, containerID int, snapshotID string) { snapshot := config.FindSnapshot(snapshotID) if snapshot == nil || snapshot.ContainerID != containerID { jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Snapshot not found"}) return } user := requestUser(r) if err := lxcManager.DeleteSnapshot(snapshotID); err != nil { jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()}) return } config.AddAuditLog("snapshot.delete", snapshot.ContainerName, snapshot.ID, user) jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Snapshot deleted"}) } func restoreContainerSnapshot(w http.ResponseWriter, r *http.Request, containerID int, snapshotID string) { snapshot := config.FindSnapshot(snapshotID) if snapshot == nil || snapshot.ContainerID != containerID { jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Snapshot not found"}) return } user := requestUser(r) if err := lxcManager.RestoreSnapshot(snapshotID); err != nil { jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()}) return } config.AddAuditLog("snapshot.restore", snapshot.ContainerName, snapshot.ID, user) jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Snapshot restored"}) } func requestUser(r *http.Request) string { if claims, ok := claimsFromRequest(r); ok { if subUser, _ := claims["sub_user"].(string); subUser != "" { return "user:" + subUser } if username, _ := claims["username"].(string); username != "" { return username } } return "admin" } func sortSnapshotsNewestFirst(snapshots []config.Snapshot) { sort.SliceStable(snapshots, func(i, j int) bool { ti, _ := time.Parse("2006-01-02 15:04:05", snapshots[i].CreatedAt) tj, _ := time.Parse("2006-01-02 15:04:05", snapshots[j].CreatedAt) return tj.Before(ti) }) }