package image import ( "context" "fmt" "io" "os" "path/filepath" "regexp" "provisioning/internal/model" "provisioning/internal/store" ) type Service struct { Store *store.Images ImageDir 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) } kernelPath := filepath.Join(p.Name, result.KernelFilename) initrdPath := filepath.Join(p.Name, result.InitrdFilename) id, err := s.Store.Create(ctx, model.Image{ Name: p.Name, Kind: p.Kind, Version: p.Version, KernelPath: kernelPath, InitrdPath: initrdPath, }) 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) }