package image import ( "context" "fmt" "io" "log" "os" "path/filepath" "regexp" "provisioning/internal/model" "provisioning/internal/store" ) type Service struct { Store *store.Images ImageDir string PublicURL string } type UploadParams struct { Name string Kind string Version string ISO io.Reader OnProgress ProgressFunc } var slugRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9.-]*$`) func (s *Service) Upload(ctx context.Context, p UploadParams) (*model.Image, error) { if !slugRegex.MatchString(p.Name) { return nil, fmt.Errorf("invalid name %q: must be lowercase alphanumeric with hyphens/dots", p.Name) } if p.Kind == "" { p.Kind = "proxmox" } if p.Version == "" { return nil, fmt.Errorf("version is required") } if _, err := s.Store.GetByName(ctx, p.Name); err == nil { return nil, fmt.Errorf("image %q already exists", p.Name) } destDir := filepath.Join(s.ImageDir, p.Name) if err := os.MkdirAll(destDir, 0o755); err != nil { return nil, fmt.Errorf("create image dir: %w", err) } result, err := ExtractFromISOWithProgress(p.ISO, destDir, p.OnProgress) if err != nil { os.RemoveAll(destDir) return nil, fmt.Errorf("extract ISO: %w", err) } if s.PublicURL != "" { report := func(stage, detail string) { if p.OnProgress != nil { p.OnProgress(stage, detail) } } report("packaging", "Configuring ISO for automated installation...") isoFile := filepath.Join(destDir, result.ISOFilename) answerURL := s.PublicURL + "/api/boot/auto-answer" if err := ModifyISOForAutoInstall(isoFile, answerURL); err != nil { log.Printf("image: ISO auto-install modification failed (non-fatal): %v", err) } } kernelPath := filepath.Join(p.Name, result.KernelFilename) initrdPath := filepath.Join(p.Name, result.InitrdFilename) isoPath := filepath.Join(p.Name, result.ISOFilename) id, err := s.Store.Create(ctx, model.Image{ Name: p.Name, Kind: p.Kind, Version: p.Version, KernelPath: kernelPath, InitrdPath: initrdPath, ISOPath: isoPath, }) if err != nil { os.RemoveAll(destDir) return nil, fmt.Errorf("save image record: %w", err) } img, err := s.Store.Get(ctx, id) if err != nil { return nil, err } return img, nil } func (s *Service) Delete(ctx context.Context, id int64) error { img, err := s.Store.Get(ctx, id) if err != nil { return err } destDir := filepath.Join(s.ImageDir, img.Name) os.RemoveAll(destDir) return s.Store.Delete(ctx, id) }