mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-06 05:52:19 +08:00
first commit
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"clicd/internal/config"
|
||||
"clicd/internal/lxc"
|
||||
)
|
||||
|
||||
// ImageInfo represents a template image with its download/enable status.
|
||||
type ImageInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Distro string `json:"distro"`
|
||||
Release string `json:"release"`
|
||||
Arch string `json:"arch"`
|
||||
Description string `json:"description"`
|
||||
Downloaded bool `json:"downloaded"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Downloading bool `json:"downloading"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
var imageDownloadsMu sync.Mutex
|
||||
var imageDownloads = map[string]bool{}
|
||||
|
||||
// isImageDownloaded checks if the LXC download cache exists for a template.
|
||||
func isImageDownloaded(distro, release, arch string) bool {
|
||||
downloaded, _ := imageDownloadedInfo(distro, release, arch)
|
||||
return downloaded
|
||||
}
|
||||
|
||||
// imageDownloadedInfo returns whether the image is downloaded and its total size in bytes.
|
||||
func imageDownloadedInfo(distro, release, arch string) (bool, int64) {
|
||||
cachePath := filepath.Join("/var/cache/lxc/download", distro, release, arch)
|
||||
info, err := os.Stat(cachePath)
|
||||
if err != nil || !info.IsDir() {
|
||||
return false, 0
|
||||
}
|
||||
// Check directly for rootfs.tar.xz (some LXC versions store it here)
|
||||
if fi, err := os.Stat(filepath.Join(cachePath, "rootfs.tar.xz")); err == nil {
|
||||
return true, fi.Size()
|
||||
}
|
||||
if fi, err := os.Stat(filepath.Join(cachePath, "meta.tar.xz")); err == nil {
|
||||
return true, fi.Size()
|
||||
}
|
||||
// Check one level deeper (LXC uses variant subdirectories like "default")
|
||||
entries, err := os.ReadDir(cachePath)
|
||||
if err != nil {
|
||||
return false, 0
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
subPath := filepath.Join(cachePath, entry.Name())
|
||||
if fi, err := os.Stat(filepath.Join(subPath, "rootfs.tar.xz")); err == nil {
|
||||
return true, fi.Size()
|
||||
}
|
||||
if fi, err := os.Stat(filepath.Join(subPath, "meta.tar.xz")); err == nil {
|
||||
return true, fi.Size()
|
||||
}
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// getEnabledImageSet returns the set of enabled image IDs.
|
||||
// If none have been explicitly set, all templates are enabled by default.
|
||||
func getEnabledImageSet() map[string]bool {
|
||||
set := make(map[string]bool)
|
||||
if len(config.AppConfig.EnabledImages) == 0 {
|
||||
for _, t := range lxc.GetTemplates() {
|
||||
set[t.ID] = true
|
||||
}
|
||||
} else {
|
||||
for _, id := range config.AppConfig.EnabledImages {
|
||||
set[id] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// HandleImages returns the list of templates with download/enable status.
|
||||
func HandleImages(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
templates := lxc.GetTemplates()
|
||||
enabledSet := getEnabledImageSet()
|
||||
|
||||
images := make([]ImageInfo, 0, len(templates))
|
||||
for _, t := range templates {
|
||||
_, downloading := imageDownloads[t.ID]
|
||||
downloaded, size := imageDownloadedInfo(t.Distro, t.Release, t.Arch)
|
||||
images = append(images, ImageInfo{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
Distro: t.Distro,
|
||||
Release: t.Release,
|
||||
Arch: t.Arch,
|
||||
Description: t.Description,
|
||||
Downloaded: downloaded,
|
||||
Enabled: enabledSet[t.ID],
|
||||
Downloading: downloading,
|
||||
SizeBytes: size,
|
||||
})
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: images})
|
||||
}
|
||||
|
||||
// HandleImageDownload downloads a template image from the LXC image server.
|
||||
func HandleImageDownload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := lxc.FindTemplate(req.TemplateID)
|
||||
if tmpl == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Already downloaded? Just enable if needed.
|
||||
if isImageDownloaded(tmpl.Distro, tmpl.Release, tmpl.Arch) {
|
||||
ensureImageEnabled(tmpl.ID)
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Already downloaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// Already downloading?
|
||||
imageDownloadsMu.Lock()
|
||||
if imageDownloads[req.TemplateID] {
|
||||
imageDownloadsMu.Unlock()
|
||||
jsonResponse(w, http.StatusConflict, APIResponse{Success: false, Message: "Already downloading"})
|
||||
return
|
||||
}
|
||||
imageDownloads[req.TemplateID] = true
|
||||
imageDownloadsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
imageDownloadsMu.Lock()
|
||||
delete(imageDownloads, req.TemplateID)
|
||||
imageDownloadsMu.Unlock()
|
||||
}()
|
||||
|
||||
// Auto-enable on download
|
||||
ensureImageEnabled(tmpl.ID)
|
||||
|
||||
// Download via lxc-create with a temp container, then destroy it.
|
||||
tmpName := fmt.Sprintf("clicd-img-dl-%s", tmpl.ID)
|
||||
args := []string{"-n", tmpName, "-t", "download", "--",
|
||||
"-d", tmpl.Distro, "-r", tmpl.Release, "-a", tmpl.Arch}
|
||||
if tmpl.Variant != "" {
|
||||
args = append(args, "--variant", tmpl.Variant)
|
||||
}
|
||||
cmd := exec.Command("lxc-create", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Clean up the temp container unconditionally.
|
||||
exec.Command("lxc-destroy", "-n", tmpName, "-f").Run()
|
||||
os.RemoveAll(filepath.Join("/var/lib/lxc", tmpName))
|
||||
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("Download failed: %v, output: %s", err, string(output)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Downloaded successfully"})
|
||||
}
|
||||
|
||||
// HandleImageDelete deletes a cached template image from disk.
|
||||
func HandleImageDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := lxc.FindTemplate(req.TemplateID)
|
||||
if tmpl == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove cache directory
|
||||
cachePath := filepath.Join("/var/cache/lxc/download", tmpl.Distro, tmpl.Release, tmpl.Arch)
|
||||
if err := os.RemoveAll(cachePath); err != nil {
|
||||
jsonResponse(w, http.StatusInternalServerError, APIResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("Failed to delete image cache: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Remove from enabled list
|
||||
removeImageEnabled(tmpl.ID)
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Deleted"})
|
||||
}
|
||||
|
||||
// HandleImageToggle enables or disables a template image.
|
||||
func HandleImageToggle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TemplateID == "" {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "template_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Enabled {
|
||||
ensureImageEnabled(req.TemplateID)
|
||||
} else {
|
||||
removeImageEnabled(req.TemplateID)
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "OK"})
|
||||
}
|
||||
|
||||
// HandleEnabledImages returns only the enabled AND downloaded templates.
|
||||
// Used by container create / reinstall to filter available templates.
|
||||
func HandleEnabledImages(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
templates := lxc.GetTemplates()
|
||||
enabledSet := getEnabledImageSet()
|
||||
|
||||
result := make([]lxc.Template, 0)
|
||||
for _, t := range templates {
|
||||
if enabledSet[t.ID] && isImageDownloaded(t.Distro, t.Release, t.Arch) {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: result})
|
||||
}
|
||||
|
||||
func ensureImageEnabled(id string) {
|
||||
// If the enabled list is empty, all templates are currently enabled by default.
|
||||
// We must populate the list with all template IDs first so that explicit toggles stick.
|
||||
if len(config.AppConfig.EnabledImages) == 0 {
|
||||
for _, t := range lxc.GetTemplates() {
|
||||
config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, t.ID)
|
||||
}
|
||||
config.SaveConfig()
|
||||
return // Already contains all IDs including this one
|
||||
}
|
||||
found := false
|
||||
for _, eid := range config.AppConfig.EnabledImages {
|
||||
if eid == id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, id)
|
||||
config.SaveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func removeImageEnabled(id string) {
|
||||
// If the enabled list is empty, populate it first with all templates,
|
||||
// then remove the one being disabled.
|
||||
if len(config.AppConfig.EnabledImages) == 0 {
|
||||
for _, t := range lxc.GetTemplates() {
|
||||
if t.ID != id {
|
||||
config.AppConfig.EnabledImages = append(config.AppConfig.EnabledImages, t.ID)
|
||||
}
|
||||
}
|
||||
config.SaveConfig()
|
||||
return
|
||||
}
|
||||
filtered := make([]string, 0, len(config.AppConfig.EnabledImages))
|
||||
for _, eid := range config.AppConfig.EnabledImages {
|
||||
if eid != id {
|
||||
filtered = append(filtered, eid)
|
||||
}
|
||||
}
|
||||
if len(filtered) != len(config.AppConfig.EnabledImages) {
|
||||
config.AppConfig.EnabledImages = filtered
|
||||
config.SaveConfig()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user