package job import ( "encoding/json" "errors" "os" "path/filepath" "strings" "hyapp-admin-server/internal/config" "hyapp-admin-server/internal/model" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/repository" ) const userExportJobType = "user-export" type JobService struct { store *repository.Store cfg config.Config } func NewService(store *repository.Store, cfg config.Config) *JobService { return &JobService{store: store, cfg: cfg} } func (s *JobService) ListJobs(options repository.ListOptions) ([]model.AdminJob, int64, error) { return s.store.ListJobs(options) } func (s *JobService) CreateUserExportJob(actor shared.Actor, options repository.ListOptions) (*model.AdminJob, error) { payload, err := json.Marshal(options) if err != nil { return nil, err } job := model.AdminJob{ Type: userExportJobType, Status: model.JobStatusPending, PayloadJSON: string(payload), MaxAttempts: s.cfg.Jobs.MaxAttempts, CreatedBy: actor.UserID, CreatedByName: actor.Username, } if job.MaxAttempts <= 0 { job.MaxAttempts = 3 } if err := s.store.CreateJob(&job); err != nil { return nil, err } return &job, nil } func (s *JobService) CancelJob(id uint) error { return s.store.CancelJob(id) } func (s *JobService) JobArtifact(id uint) (*model.AdminJob, string, error) { job, err := s.store.FindJobByID(id) if err != nil { return nil, "", err } if job.Status != model.JobStatusSucceeded || strings.TrimSpace(job.ArtifactPath) == "" { return nil, "", errors.New("job artifact is not ready") } artifactPath, err := safeArtifactPath(s.cfg.Jobs.ArtifactDir, job.ArtifactPath) if err != nil { return nil, "", err } if _, err := os.Stat(artifactPath); err != nil { return nil, "", err } return job, artifactPath, nil } func safeArtifactPath(root string, artifactPath string) (string, error) { root = strings.TrimSpace(root) if root == "" { root = "storage/exports" } cleanRoot, err := filepath.Abs(root) if err != nil { return "", err } cleanArtifact := filepath.Clean(strings.TrimSpace(artifactPath)) if filepath.IsAbs(cleanArtifact) { cleanArtifact, err = filepath.Abs(cleanArtifact) if err != nil { return "", err } if cleanArtifact == cleanRoot || strings.HasPrefix(cleanArtifact, cleanRoot+string(os.PathSeparator)) { return cleanArtifact, nil } return "", errors.New("invalid artifact path") } fullPath, err := filepath.Abs(filepath.Join(cleanRoot, cleanArtifact)) if err != nil { return "", err } if fullPath != cleanRoot && !strings.HasPrefix(fullPath, cleanRoot+string(os.PathSeparator)) { return "", errors.New("invalid artifact path") } return fullPath, nil }