package themes_service import ( "fmt" "log" "os" "path/filepath" "sync" "autolibrary.top/webserver/backend/models/themes_model" "autolibrary.top/webserver/backend/services/theme_files" "gorm.io/gorm" ) // Service coordinates database changes and their corresponding theme archive. type Service struct { DB *gorm.DB Files *theme_files.Store locks sync.Map } func (s *Service) lock(id uint) func() { value, _ := s.locks.LoadOrStore(id, &sync.Mutex{}) mutex := value.(*sync.Mutex) mutex.Lock() return mutex.Unlock } func (s *Service) ArchivePath(id uint) string { return filepath.Join(s.Files.Root(), fmt.Sprintf("%d.altheme", id)) } func (s *Service) Create(theme *themes_model.Theme, qss []byte) error { var path string err := s.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(theme).Error; err != nil { return err } path = s.ArchivePath(theme.ID) data, err := theme_files.CreateAlthemeArchive(qss, theme_files.BuildInfoJSON(*theme)) if err != nil { return err } if err := s.Files.WriteFile(path, data, 0644); err != nil { return err } theme.FilePath = path theme.FileSize = fmt.Sprintf("%.1f KB", float64(len(data))/1024) return tx.Save(theme).Error }) if err != nil && path != "" { _ = s.Files.Remove(path) } return err } func (s *Service) Save(theme *themes_model.Theme, qss []byte) error { unlock := s.lock(theme.ID) defer unlock() path := s.ArchivePath(theme.ID) previous, readErr := os.ReadFile(path) if qss == nil { var err error qss, err = theme_files.ExtractQSSFromAltheme(path) if err != nil { return fmt.Errorf("cannot read existing theme archive: %w", err) } } data, err := theme_files.CreateAlthemeArchive(qss, theme_files.BuildInfoJSON(*theme)) if err != nil { return err } theme.FilePath = path theme.FileSize = fmt.Sprintf("%.1f KB", float64(len(data))/1024) wrote := false err = s.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Save(theme).Error; err != nil { return err } if err := s.Files.WriteFile(path, data, 0644); err != nil { return err } wrote = true return nil }) if err != nil && wrote { var restoreErr error if readErr == nil { restoreErr = s.Files.WriteFile(path, previous, 0644) } else { restoreErr = s.Files.Remove(path) } if restoreErr != nil { log.Printf("[E] archive rollback failed for theme %d: %v", theme.ID, restoreErr) } } return err } func (s *Service) Delete(theme *themes_model.Theme) error { unlock := s.lock(theme.ID) defer unlock() if err := s.DB.Delete(theme).Error; err != nil { return err } paths := []string{s.ArchivePath(theme.ID)} if theme.PreviewURL != "" { paths = append(paths, filepath.Join(s.Files.Root(), "previews", filepath.Base(theme.PreviewURL))) } for _, path := range paths { if err := s.Files.Remove(path); err != nil { log.Printf("[W] theme %d file cleanup: %v", theme.ID, err) } } return nil }