2026-05-02 13:02:38 +08:00

104 lines
2.7 KiB
Go

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
}