From 74c2e0982f336ab135d8b63d26fa49bcd58e57a9 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 1 May 2026 20:14:19 +0300 Subject: service: split media.go (745 lines) into 5 focused files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the monolithic media.go into cohesive files to fix KISS/SRP violation: - media_access.go: permission verification helpers (verifyAccess, verifyModifyAccess, verifySetModifyAccess) - media_browse.go: browsing, listing, streaming, thumbnails, set cover, tags and favorites - media_write.go: upload, create, soft-delete, restore - media_share.go: share token creation, validation, revocation, streaming via token - media_notes.go: note get/upsert/delete The shared mediaService struct, NewMediaService constructor, and common helpers (supported extensions, type guessing, thumbnail generation) remain in media.go. No functional changes — all tests pass. --- internal/service/media_write.go | 156 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 internal/service/media_write.go (limited to 'internal/service/media_write.go') diff --git a/internal/service/media_write.go b/internal/service/media_write.go new file mode 100644 index 0000000..ff316c7 --- /dev/null +++ b/internal/service/media_write.go @@ -0,0 +1,156 @@ +package service + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "codeberg.org/snonux/player/internal/model" +) + +func (s *mediaService) SoftDeleteMedia(ctx context.Context, mediaID, userID int64) error { + _, err := s.verifyModifyAccess(ctx, mediaID, userID) + if err != nil { + return err + } + return s.store.SoftDeleteMedia(ctx, mediaID) +} + +func (s *mediaService) RestoreMedia(ctx context.Context, mediaID, userID int64) error { + _, err := s.verifyModifyAccess(ctx, mediaID, userID) + if err != nil { + return err + } + return s.store.RestoreMedia(ctx, mediaID) +} + +func (s *mediaService) uniqueFilename(dir, filename string) string { + filename = filepath.Base(filename) + if filename == "." || filename == ".." || filename == "" { + return "" + } + ext := filepath.Ext(filename) + base := strings.TrimSuffix(filename, ext) + + candidate := filepath.Join(dir, filename) + if _, err := os.Stat(candidate); os.IsNotExist(err) { + return candidate + } + + for i := 1; ; i++ { + candidate = filepath.Join(dir, fmt.Sprintf("%s(%d)%s", base, i, ext)) + if _, err := os.Stat(candidate); os.IsNotExist(err) { + return candidate + } + } +} + +func (s *mediaService) UploadMedia(ctx context.Context, setID, userID int64, filename string, data io.Reader, size int64) (*model.Media, error) { + set, err := s.store.GetSetByID(ctx, setID) + if err != nil { + return nil, fmt.Errorf("get set: %w", err) + } + if set == nil { + return nil, ErrNotFound + } + + if err := s.verifySetModifyAccess(ctx, setID, userID); err != nil { + return nil, err + } + + if !isSupportedExtension(filename) { + return nil, fmt.Errorf("%w: %s", ErrUnsupportedExtension, filepath.Ext(filename)) + } + + dir := filepath.Clean(filepath.Join(s.mediaRoot, set.RootPath)) + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("mkdir: %w", err) + } + + path := s.uniqueFilename(dir, filename) + if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dir)+string(filepath.Separator)) { + return nil, errors.New("invalid filename") + } + + media, err := s.saveUploadedMedia(ctx, setID, path, data, size) + if err != nil { + os.Remove(path) + return nil, err + } + + meta, err := s.probeMedia(ctx, path) + if err != nil { + os.Remove(path) + s.store.HardDeleteMedia(ctx, media.ID) + return nil, err + } + media.Duration = meta.Duration + media.Codec = meta.Codec + media.Resolution = meta.Resolution + media.Bitrate = meta.Bitrate + + if media.Type == model.MediaTypeVideo { + if err := s.generateThumbnail(ctx, media, meta.Duration); err != nil { + os.Remove(path) + _ = s.store.HardDeleteMedia(ctx, media.ID) + return nil, err + } + } + + if err := s.store.UpdateMedia(ctx, media); err != nil { + os.Remove(path) + _ = s.store.HardDeleteMedia(ctx, media.ID) + return nil, fmt.Errorf("update media metadata: %w", err) + } + + return media, nil +} + +// saveUploadedMedia writes data to disk and creates a minimal media row. +func (s *mediaService) saveUploadedMedia(ctx context.Context, setID int64, path string, data io.Reader, size int64) (*model.Media, error) { + f, err := os.Create(path) + if err != nil { + return nil, fmt.Errorf("create file: %w", err) + } + defer f.Close() + + n, err := io.Copy(f, data) + if err != nil { + return nil, fmt.Errorf("write file: %w", err) + } + + now := s.clock.Now() + media := &model.Media{ + SetID: setID, + RelPath: filepath.Base(path), + FileName: filepath.Base(path), + AbsPath: path, + Type: guessMediaType(filepath.Base(path)), + FileSizeBytes: n, + CreatedAt: now, + } + _ = size + + id, err := s.store.CreateMedia(ctx, media) + if err != nil { + return nil, fmt.Errorf("create media: %w", err) + } + media.ID = id + return media, nil +} + +// probeMedia extracts metadata from the uploaded file. +func (s *mediaService) probeMedia(ctx context.Context, path string) (*model.Metadata, error) { + if s.prober == nil { + return &model.Metadata{}, nil + } + meta, err := s.prober.Probe(ctx, path) + if err != nil { + return nil, fmt.Errorf("probe media: %w", err) + } + return meta, nil +} -- cgit v1.2.3