← back to jearl4__PhotoGallery

Function bodies 1,000 total

All specs Real LLM only Function bodies
gallery.Service.SetExpiration method · go · L383-L394 (12 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) SetExpiration(ctx context.Context, galleryID string, expiresAt *time.Time) (*repository.Gallery, error) {
	gallery, err := s.GetByID(ctx, galleryID)
	if err != nil {
		return nil, err
	}
	gallery.ExpiresAt = expiresAt
	if err := s.galleryRepo.Update(ctx, gallery); err != nil {
		return nil, errors.Wrap(err, 500, "Failed to update gallery expiration")
	}
	logger.Info("Gallery expiration updated", map[string]interface{}{"galleryId": gallery.GalleryID})
	return gallery, nil
}
gallery.Service.ProcessExpiredGalleries method · go · L397-L419 (23 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) ProcessExpiredGalleries(ctx context.Context, limit int) error {
	galleries, err := s.galleryRepo.ListExpired(ctx, limit)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to list expired galleries")
	}

	var errorCount int
	for _, gallery := range galleries {
		if err := s.Delete(ctx, gallery.GalleryID); err != nil {
			logger.Error("Failed to delete expired gallery", map[string]interface{}{
				"galleryId": gallery.GalleryID, "error": err.Error(),
			})
			errorCount++
			continue
		}
		logger.Info("Deleted expired gallery", map[string]interface{}{"galleryId": gallery.GalleryID})
	}

	if errorCount > 0 {
		return fmt.Errorf("completed with %d errors out of %d galleries", errorCount, len(galleries))
	}
	return nil
}
gallery.Service.ProcessScheduledGalleries method · go · L423-L477 (55 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) ProcessScheduledGalleries(ctx context.Context, limit int) error {
	galleries, err := s.galleryRepo.ListScheduled(ctx, limit)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to list scheduled galleries")
	}

	var errorCount int
	var skippedCount int
	for _, gallery := range galleries {
		// Use atomic conditional update to prevent race conditions.
		// Only updates if the gallery is still in "scheduled" status.
		updated, err := s.galleryRepo.UpdateStatusIfScheduled(ctx, gallery.GalleryID, gallery.PhotographerID)
		if err != nil {
			logger.Error("Failed to publish scheduled gallery", map[string]interface{}{
				"galleryId": gallery.GalleryID, "error": err.Error(),
			})
			errorCount++
			continue
		}

		if !updated {
			// Gallery was modified by user or already published by another scheduler instance
			logger.Info("Skipped gallery (already modified or published)", map[string]interface{}{
				"galleryId": gallery.GalleryID,
			})
			skippedCount++
			continu
gallery.Service.PublishScheduledGalleriesForPhotographer method · go · L481-L507 (27 LOC)
backend/internal/domain/gallery/service.go
func (s *Service) PublishScheduledGalleriesForPhotographer(ctx context.Context, photographerID string) error {
	galleries, err := s.galleryRepo.ListScheduledByPhotographer(ctx, photographerID)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to list scheduled galleries for photographer")
	}

	var errorCount int
	for _, gallery := range galleries {
		gallery.Status = "active"
		gallery.PublishAt = nil // Clear the scheduled time
		if err := s.galleryRepo.Update(ctx, gallery); err != nil {
			logger.Error("Failed to publish scheduled gallery on tier downgrade", map[string]interface{}{
				"galleryId": gallery.GalleryID, "photographerId": photographerID, "error": err.Error(),
			})
			errorCount++
			continue
		}
		logger.Info("Published scheduled gallery due to tier downgrade", map[string]interface{}{
			"galleryId": gallery.GalleryID, "photographerId": photographerID,
		})
	}

	if errorCount > 0 {
		return fmt.Errorf("completed with %d errors out of %d galleries", errorCount, len
gallery.BaseValidator.ValidateNext method · go · L30-L35 (6 LOC)
backend/internal/domain/gallery/validators.go
func (v *BaseValidator) ValidateNext(ctx context.Context, req interface{}) error {
	if v.next != nil {
		return v.next.Validate(ctx, req)
	}
	return nil
}
gallery.CustomURLValidator.Validate method · go · L48-L66 (19 LOC)
backend/internal/domain/gallery/validators.go
func (v *CustomURLValidator) Validate(ctx context.Context, req interface{}) error {
	createReq, ok := req.(CreateGalleryRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if createReq.CustomURL != "" {
		if len(createReq.CustomURL) < 3 || len(createReq.CustomURL) > 100 {
			return errors.NewBadRequest("Custom URL must be between 3 and 100 characters")
		}

		matched, _ := regexp.MatchString("^[a-z0-9-]+$", createReq.CustomURL)
		if !matched {
			return errors.NewBadRequest("Custom URL can only contain lowercase letters, numbers, and hyphens")
		}
	}

	return v.ValidateNext(ctx, req)
}
gallery.PasswordValidator.Validate method · go · L80-L95 (16 LOC)
backend/internal/domain/gallery/validators.go
func (v *PasswordValidator) Validate(ctx context.Context, req interface{}) error {
	createReq, ok := req.(CreateGalleryRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if createReq.Password == "" {
		return errors.NewBadRequest("Password is required")
	}

	if len(createReq.Password) < v.MinLength {
		return errors.NewBadRequest("Password must be at least " + string(rune(v.MinLength+'0')) + " characters")
	}

	return v.ValidateNext(ctx, req)
}
Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
gallery.NameValidator.Validate method · go · L110-L125 (16 LOC)
backend/internal/domain/gallery/validators.go
func (v *NameValidator) Validate(ctx context.Context, req interface{}) error {
	createReq, ok := req.(CreateGalleryRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if createReq.Name == "" {
		return errors.NewBadRequest("Gallery name is required")
	}

	if len(createReq.Name) < v.MinLength || len(createReq.Name) > v.MaxLength {
		return errors.NewBadRequest("Gallery name must be between " + string(rune(v.MinLength+'0')) + " and " + string(rune(v.MaxLength+'0')) + " characters")
	}

	return v.ValidateNext(ctx, req)
}
gallery.ExpirationValidator.Validate method · go · L138-L149 (12 LOC)
backend/internal/domain/gallery/validators.go
func (v *ExpirationValidator) Validate(ctx context.Context, req interface{}) error {
	createReq, ok := req.(CreateGalleryRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if createReq.ExpiresAt != nil && createReq.ExpiresAt.Before(time.Now()) {
		return errors.NewBadRequest("Expiration date must be in the future")
	}

	return v.ValidateNext(ctx, req)
}
gallery.WatermarkValidator.Validate method · go · L162-L205 (44 LOC)
backend/internal/domain/gallery/validators.go
func (v *WatermarkValidator) Validate(ctx context.Context, req interface{}) error {
	createReq, ok := req.(CreateGalleryRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	// Validate watermark type if provided
	if createReq.WatermarkType != "" {
		validTypes := map[string]bool{
			"text":  true,
			"image": true,
		}
		if !validTypes[createReq.WatermarkType] {
			return errors.NewBadRequest("Invalid watermark type. Must be 'text' or 'image'")
		}
	}

	// Validate watermark position if provided
	if createReq.WatermarkPosition != "" {
		validPositions := map[string]bool{
			"bottom-right": true,
			"bottom-left":  true,
			"center":       true,
		}
		if !validPositions[createReq.WatermarkPosition] {
			return errors.NewBadRequest("Invalid watermark position")
		}
	}

	// Validate phase if provided
	if createReq.Phase != "" {
		validPhases := map[string]bool{
			"uploading": true,
			"proofing":  true,
			"editing":   true,
			"delivered": true,
		}
		if !validPhases[createReq.Phas
gallery.TemplateValidator.Validate method · go · L218-L237 (20 LOC)
backend/internal/domain/gallery/validators.go
func (v *TemplateValidator) Validate(ctx context.Context, req interface{}) error {
	createReq, ok := req.(CreateGalleryRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if createReq.Template != "" {
		validTemplates := map[string]bool{
			"grid":      true,
			"masonry":   true,
			"justified": true,
			"slideshow": true,
		}
		if !validTemplates[createReq.Template] {
			return errors.NewBadRequest("Invalid template type. Must be one of: grid, masonry, justified, slideshow")
		}
	}

	return v.ValidateNext(ctx, req)
}
gallery.NewCreateGalleryValidationChain function · go · L240-L252 (13 LOC)
backend/internal/domain/gallery/validators.go
func NewCreateGalleryValidationChain() Validator {
	name := NewNameValidator(1, 200)
	password := NewPasswordValidator(6)
	customURL := NewCustomURLValidator()
	expiration := NewExpirationValidator()
	watermark := NewWatermarkValidator()
	template := NewTemplateValidator()

	// Build the chain
	name.SetNext(password).SetNext(customURL).SetNext(expiration).SetNext(watermark).SetNext(template)

	return name
}
photo.NewService function · go · L40-L54 (15 LOC)
backend/internal/domain/photo/service.go
func NewService(
	photoRepo repository.PhotoRepository,
	galleryRepo repository.GalleryRepository,
	favoriteRepo repository.FavoriteRepository,
	storageService StorageService,
	eventBus events.EventBus,
) *Service {
	return &Service{
		photoRepo:      photoRepo,
		galleryRepo:    galleryRepo,
		favoriteRepo:   favoriteRepo,
		storageService: storageService,
		eventBus:       eventBus,
	}
}
photo.Service.GenerateUploadURL method · go · L71-L112 (42 LOC)
backend/internal/domain/photo/service.go
func (s *Service) GenerateUploadURL(ctx context.Context, req UploadURLRequest) (*UploadURLResponse, error) {
	// Verify gallery exists
	gallery, err := s.galleryRepo.GetByID(ctx, req.GalleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}

	// Validate file type
	if !isValidImageType(req.MimeType) {
		return nil, errors.NewBadRequest("Invalid image type. Supported: JPEG, PNG, GIF, WebP, HEIC/HEIF, TIFF, BMP, RAW (CR2, NEF, ARW, DNG, ORF, RAF), PSD, EPS, SVG")
	}

	// Generate photo ID
	photoID := utils.GenerateID("photo")

	// Generate presigned upload URL
	uploadResp, err := s.storageService.GenerateUploadURL(ctx, storage.UploadURLRequest{
		GalleryID: req.GalleryID,
		PhotoID:   photoID,
		FileName:  req.FileName,
		MimeType:  req.MimeType,
	})

	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to generate upload URL")
	}

	logger.Info("Generated upload URL for photo", m
photo.Service.Create method · go · L132-L194 (63 LOC)
backend/internal/domain/photo/service.go
func (s *Service) Create(ctx context.Context, req CreatePhotoRequest) (*repository.Photo, error) {
	// Verify gallery exists
	gallery, err := s.galleryRepo.GetByID(ctx, req.GalleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}

	photo := &repository.Photo{
		PhotoID:        req.PhotoID,
		GalleryID:      req.GalleryID,
		SetID:          req.SetID,
		SetOrder:       req.SetOrder,
		FileName:       req.FileName,
		OriginalKey:    req.OriginalKey,
		OptimizedKey:   req.OptimizedKey,
		ThumbnailKey:   req.ThumbnailKey,
		MimeType:       req.MimeType,
		Size:           req.Size,
		Width:          req.Width,
		Height:         req.Height,
		UploadedAt:     time.Now(),
		FavoriteCount:  0,
		DownloadCount:  0,
		CurrentVersion: 1, // Start at version 1
		Metadata:       req.Metadata,
	}

	// Create photo record
	if err := s.photoRepo.Create(ctx, photo); err != nil {
		logger.Error("Failed 
Want this analysis on your repo? https://repobility.com/scan/
photo.Service.GetByID method · go · L197-L206 (10 LOC)
backend/internal/domain/photo/service.go
func (s *Service) GetByID(ctx context.Context, photoID string) (*repository.Photo, error) {
	photo, err := s.photoRepo.GetByID(ctx, photoID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get photo")
	}
	if photo == nil {
		return nil, errors.NewNotFound("Photo")
	}
	return photo, nil
}
photo.Service.Delete method · go · L218-L262 (45 LOC)
backend/internal/domain/photo/service.go
func (s *Service) Delete(ctx context.Context, photoID string) error {
	// Get photo
	photo, err := s.GetByID(ctx, photoID)
	if err != nil {
		return err
	}

	// Delete from S3
	if err := s.storageService.DeletePhoto(ctx, photo.OriginalKey, photo.OptimizedKey, photo.ThumbnailKey); err != nil {
		logger.Error("Failed to delete photo files", map[string]interface{}{"error": err.Error()})
		// Continue with deletion even if S3 fails
	}

	// Delete from DynamoDB
	if err := s.photoRepo.Delete(ctx, photoID); err != nil {
		logger.Error("Failed to delete photo record", map[string]interface{}{"error": err.Error()})
		return errors.Wrap(err, 500, "Failed to delete photo")
	}

	// Update gallery stats
	if err := s.galleryRepo.UpdatePhotoCount(ctx, photo.GalleryID, -1); err != nil {
		logger.Error("Failed to update photo count", map[string]interface{}{"error": err.Error()})
	}
	if err := s.galleryRepo.UpdateTotalSize(ctx, photo.GalleryID, -photo.Size); err != nil {
		logger.Error("Failed to update 
photo.Service.GetDownloadURL method · go · L265-L304 (40 LOC)
backend/internal/domain/photo/service.go
func (s *Service) GetDownloadURL(ctx context.Context, photoID string) (string, error) {
	photo, err := s.GetByID(ctx, photoID)
	if err != nil {
		return "", err
	}

	// Get gallery to find photographer ID for analytics
	gallery, err := s.galleryRepo.GetByID(ctx, photo.GalleryID)
	if err != nil {
		logger.Error("Failed to get gallery for download analytics", map[string]interface{}{"error": err.Error()})
	}

	// Increment download count
	if err := s.photoRepo.IncrementDownloadCount(ctx, photoID); err != nil {
		logger.Error("Failed to increment download count", map[string]interface{}{"error": err.Error()})
		// Continue anyway
	}

	// Generate download URL for optimized version
	url, err := s.storageService.GenerateDownloadURL(ctx, photo.OptimizedKey, "", photo.FileName)
	if err != nil {
		return "", errors.Wrap(err, 500, "Failed to generate download URL")
	}

	logger.Info("Generated download URL", map[string]interface{}{
		"photoId": photo.PhotoID,
	})

	// Publish PhotoDownloaded event
photo.Service.ToggleFavorite method · go · L307-L367 (61 LOC)
backend/internal/domain/photo/service.go
func (s *Service) ToggleFavorite(ctx context.Context, galleryID, sessionID, photoID string) (bool, error) {
	// Check if already favorited
	isFavorited, err := s.favoriteRepo.IsFavorited(ctx, galleryID, sessionID, photoID)
	if err != nil {
		return false, errors.Wrap(err, 500, "Failed to check favorite status")
	}

	if isFavorited {
		// Remove favorite
		if err := s.favoriteRepo.Delete(ctx, galleryID, sessionID, photoID); err != nil {
			return false, errors.Wrap(err, 500, "Failed to remove favorite")
		}
		// Decrement count
		if err := s.photoRepo.IncrementFavoriteCount(ctx, photoID, -1); err != nil {
			logger.Error("Failed to decrement favorite count", map[string]interface{}{"error": err.Error()})
		}
		logger.Info("Photo unfavorited", map[string]interface{}{"photoId": photoID})

		// Publish FavoriteToggled event for analytics
		if s.eventBus != nil {
			event := events.NewEvent(events.FavoriteToggled, &events.FavoriteToggledPayload{
				PhotoID:   photoID,
				GalleryID: gallery
photo.Service.ListFavoritesBySession method · go · L370-L376 (7 LOC)
backend/internal/domain/photo/service.go
func (s *Service) ListFavoritesBySession(ctx context.Context, galleryID, sessionID string) ([]*repository.Favorite, error) {
	favorites, err := s.favoriteRepo.ListBySession(ctx, galleryID, sessionID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to list favorites")
	}
	return favorites, nil
}
photo.Service.ListFavoritesByGallery method · go · L379-L385 (7 LOC)
backend/internal/domain/photo/service.go
func (s *Service) ListFavoritesByGallery(ctx context.Context, galleryID string) ([]*repository.Favorite, error) {
	favorites, err := s.favoriteRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to list gallery favorites")
	}
	return favorites, nil
}
photo.Service.UpdateSetID method · go · L388-L407 (20 LOC)
backend/internal/domain/photo/service.go
func (s *Service) UpdateSetID(ctx context.Context, photoID string, setID string, setOrder int) (*repository.Photo, error) {
	photo, err := s.GetByID(ctx, photoID)
	if err != nil {
		return nil, err
	}

	if err := s.photoRepo.UpdateSetID(ctx, photoID, setID, setOrder); err != nil {
		return nil, errors.Wrap(err, 500, "Failed to update photo set")
	}

	photo.SetID = setID
	photo.SetOrder = setOrder
	logger.Info("Photo set updated", map[string]interface{}{
		"photoId":  photoID,
		"setId":    setID,
		"setOrder": setOrder,
	})

	return photo, nil
}
photo.Service.ListBySet method · go · L410-L416 (7 LOC)
backend/internal/domain/photo/service.go
func (s *Service) ListBySet(ctx context.Context, setID string) ([]*repository.Photo, error) {
	photos, err := s.photoRepo.ListBySet(ctx, setID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to list photos by set")
	}
	return photos, nil
}
Open data scored by Repobility · https://repobility.com
photo.Service.GenerateGalleryZipURL method · go · L420-L557 (138 LOC)
backend/internal/domain/photo/service.go
func (s *Service) GenerateGalleryZipURL(ctx context.Context, galleryID string, zipFilename string) (string, error) {
	// Verify gallery exists
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return "", errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return "", errors.NewNotFound("Gallery")
	}

	// Fetch all photos for the gallery with pagination
	var allPhotos []*repository.Photo
	var lastKey map[string]interface{}
	const pageSize = 500

	for {
		photos, nextKey, err := s.photoRepo.ListByGallery(ctx, galleryID, pageSize, lastKey)
		if err != nil {
			return "", errors.Wrap(err, 500, "Failed to list photos")
		}
		allPhotos = append(allPhotos, photos...)

		if nextKey == nil || len(photos) < pageSize {
			break
		}
		lastKey = nextKey
	}

	if len(allPhotos) == 0 {
		return "", errors.NewBadRequest("Gallery has no photos to download")
	}

	// Create ZIP in memory buffer
	var buf bytes.Buffer
	zipWriter := zip.NewWriter(&buf)

	// Track f
photo.isValidImageType function · go · L560-L585 (26 LOC)
backend/internal/domain/photo/service.go
func isValidImageType(mimeType string) bool {
	validTypes := map[string]bool{
		// Standard image formats
		"image/jpeg": true,
		"image/jpg":  true,
		"image/png":  true,
		"image/webp": true,
		"image/gif":  true,
		"image/heic": true,
		"image/heif": true,
		"image/tiff": true,
		"image/bmp":  true,
		// RAW camera formats
		"image/x-canon-cr2":   true,
		"image/x-nikon-nef":   true,
		"image/x-sony-arw":    true,
		"image/x-adobe-dng":   true,
		"image/x-olympus-orf": true,
		"image/x-fuji-raf":    true,
		// Professional formats
		"image/vnd.adobe.photoshop": true,
		"application/postscript":    true, // EPS
		"image/svg+xml":             true,
	}
	return validTypes[mimeType]
}
photoset.NewService function · go · L20-L28 (9 LOC)
backend/internal/domain/photoset/service.go
func NewService(
	photoSetRepo repository.PhotoSetRepository,
	galleryRepo repository.GalleryRepository,
) *Service {
	return &Service{
		photoSetRepo: photoSetRepo,
		galleryRepo:  galleryRepo,
	}
}
photoset.Service.Create method · go · L31-L75 (45 LOC)
backend/internal/domain/photoset/service.go
func (s *Service) Create(ctx context.Context, galleryID, photographerID, name, description string) (*repository.PhotoSet, error) {
	// Verify gallery exists and belongs to photographer
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	if gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to modify this gallery")
	}

	// Get existing sets to determine display order
	existingSets, err := s.photoSetRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to list existing sets")
	}

	// New set goes at the end
	displayOrder := len(existingSets)

	set := &repository.PhotoSet{
		SetID:        utils.GenerateID("set"),
		GalleryID:    galleryID,
		Name:         name,
		Description:  description,
		DisplayOrder: displayOrder,
		PhotoCount:   0,
		Crea
photoset.Service.GetByID method · go · L78-L87 (10 LOC)
backend/internal/domain/photoset/service.go
func (s *Service) GetByID(ctx context.Context, setID string) (*repository.PhotoSet, error) {
	set, err := s.photoSetRepo.GetByID(ctx, setID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get photo set")
	}
	if set == nil {
		return nil, errors.NewNotFound("PhotoSet")
	}
	return set, nil
}
photoset.Service.ListByGallery method · go · L90-L96 (7 LOC)
backend/internal/domain/photoset/service.go
func (s *Service) ListByGallery(ctx context.Context, galleryID string) ([]*repository.PhotoSet, error) {
	sets, err := s.photoSetRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to list photo sets")
	}
	return sets, nil
}
photoset.Service.Update method · go · L99-L137 (39 LOC)
backend/internal/domain/photoset/service.go
func (s *Service) Update(ctx context.Context, setID, photographerID, name, description string, displayOrder *int) (*repository.PhotoSet, error) {
	// Get existing set
	set, err := s.GetByID(ctx, setID)
	if err != nil {
		return nil, err
	}

	// Verify ownership through gallery
	gallery, err := s.galleryRepo.GetByID(ctx, set.GalleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil || gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to modify this set")
	}

	// Update fields
	if name != "" {
		set.Name = name
	}
	if description != "" {
		set.Description = description
	}
	if displayOrder != nil {
		set.DisplayOrder = *displayOrder
	}
	set.UpdatedAt = time.Now()

	if err := s.photoSetRepo.Update(ctx, set); err != nil {
		return nil, errors.Wrap(err, 500, "Failed to update photo set")
	}

	logger.Info("Photo set updated", map[string]interface{}{
		"setId": setID,
		"name":  set.Name
photoset.Service.Delete method · go · L140-L171 (32 LOC)
backend/internal/domain/photoset/service.go
func (s *Service) Delete(ctx context.Context, setID, photographerID string) error {
	// Get existing set
	set, err := s.GetByID(ctx, setID)
	if err != nil {
		return err
	}

	// Verify ownership through gallery
	gallery, err := s.galleryRepo.GetByID(ctx, set.GalleryID)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil || gallery.PhotographerID != photographerID {
		return errors.NewForbidden("You do not have permission to delete this set")
	}

	// Check if set has photos
	if set.PhotoCount > 0 {
		return errors.NewBadRequest("Cannot delete a set that contains photos. Move or delete the photos first.")
	}

	if err := s.photoSetRepo.Delete(ctx, setID); err != nil {
		return errors.Wrap(err, 500, "Failed to delete photo set")
	}

	logger.Info("Photo set deleted", map[string]interface{}{
		"setId":     setID,
		"galleryId": set.GalleryID,
	})

	return nil
}
All rows scored by the Repobility analyzer (https://repobility.com)
photoset.Service.ReorderSets method · go · L174-L214 (41 LOC)
backend/internal/domain/photoset/service.go
func (s *Service) ReorderSets(ctx context.Context, galleryID, photographerID string, setIDs []string) error {
	// Verify gallery ownership
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return errors.NewNotFound("Gallery")
	}
	if gallery.PhotographerID != photographerID {
		return errors.NewForbidden("You do not have permission to modify this gallery")
	}

	// Verify all sets belong to this gallery
	existingSets, err := s.photoSetRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return errors.Wrap(err, 500, "Failed to list sets")
	}

	existingSetIDs := make(map[string]bool)
	for _, set := range existingSets {
		existingSetIDs[set.SetID] = true
	}

	for _, setID := range setIDs {
		if !existingSetIDs[setID] {
			return errors.NewBadRequest("Set " + setID + " does not belong to this gallery")
		}
	}

	if err := s.photoSetRepo.ReorderSets(ctx, galleryID, setIDs); err != nil {
		r
photo.BaseValidator.ValidateNext method · go · L38-L43 (6 LOC)
backend/internal/domain/photo/validators.go
func (v *BaseValidator) ValidateNext(ctx context.Context, req interface{}) error {
	if v.next != nil {
		return v.next.Validate(ctx, req)
	}
	return nil
}
photo.NewFileTypeValidator function · go · L52-L78 (27 LOC)
backend/internal/domain/photo/validators.go
func NewFileTypeValidator() *FileTypeValidator {
	return &FileTypeValidator{
		AllowedTypes: []string{
			// Standard image formats
			"image/jpeg",
			"image/jpg", // Non-standard but sometimes used
			"image/png",
			"image/gif",
			"image/webp",
			"image/heic",
			"image/heif",
			"image/tiff",
			"image/bmp",
			// RAW camera formats
			"image/x-canon-cr2",
			"image/x-nikon-nef",
			"image/x-sony-arw",
			"image/x-adobe-dng",
			"image/x-olympus-orf",
			"image/x-fuji-raf",
			// Professional formats
			"image/vnd.adobe.photoshop",
			"application/postscript", // EPS
			"image/svg+xml",
		},
	}
}
photo.FileTypeValidator.Validate method · go · L81-L101 (21 LOC)
backend/internal/domain/photo/validators.go
func (v *FileTypeValidator) Validate(ctx context.Context, req interface{}) error {
	uploadReq, ok := req.(UploadRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	contentType := strings.ToLower(uploadReq.ContentType)
	allowed := false
	for _, t := range v.AllowedTypes {
		if t == contentType {
			allowed = true
			break
		}
	}

	if !allowed {
		return errors.NewBadRequest("File type not allowed. Supported: JPEG, PNG, GIF, WebP, HEIC/HEIF, TIFF, BMP, RAW (CR2, NEF, ARW, DNG, ORF, RAF), PSD, EPS, SVG")
	}

	return v.ValidateNext(ctx, req)
}
photo.NewFileExtensionValidator function · go · L110-L122 (13 LOC)
backend/internal/domain/photo/validators.go
func NewFileExtensionValidator() *FileExtensionValidator {
	return &FileExtensionValidator{
		AllowedExtensions: []string{
			// Standard image extensions
			".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".heif",
			".tiff", ".tif", ".bmp",
			// RAW camera extensions
			".cr2", ".nef", ".arw", ".dng", ".orf", ".raf",
			// Professional format extensions
			".psd", ".eps", ".svg",
		},
	}
}
photo.FileExtensionValidator.Validate method · go · L125-L145 (21 LOC)
backend/internal/domain/photo/validators.go
func (v *FileExtensionValidator) Validate(ctx context.Context, req interface{}) error {
	uploadReq, ok := req.(UploadRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	ext := strings.ToLower(path.Ext(uploadReq.FileName))
	allowed := false
	for _, e := range v.AllowedExtensions {
		if e == ext {
			allowed = true
			break
		}
	}

	if !allowed {
		return errors.NewBadRequest("File extension not allowed. Supported: .jpg, .jpeg, .png, .gif, .webp, .heic, .heif, .tiff, .tif, .bmp, .cr2, .nef, .arw, .dng, .orf, .raf, .psd, .eps, .svg")
	}

	return v.ValidateNext(ctx, req)
}
photo.NewFileSizeValidator function · go · L155-L160 (6 LOC)
backend/internal/domain/photo/validators.go
func NewFileSizeValidator() *FileSizeValidator {
	return &FileSizeValidator{
		MinSize: 1,                 // 1 byte minimum
		MaxSize: 100 * 1024 * 1024, // 100 MB maximum (increased for RAW files)
	}
}
photo.FileSizeValidator.Validate method · go · L163-L178 (16 LOC)
backend/internal/domain/photo/validators.go
func (v *FileSizeValidator) Validate(ctx context.Context, req interface{}) error {
	uploadReq, ok := req.(UploadRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if uploadReq.Size < v.MinSize {
		return errors.NewBadRequest("File is empty")
	}

	if uploadReq.Size > v.MaxSize {
		return errors.NewBadRequest("File size exceeds maximum limit of 100 MB")
	}

	return v.ValidateNext(ctx, req)
}
Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
photo.FileNameValidator.Validate method · go · L194-L217 (24 LOC)
backend/internal/domain/photo/validators.go
func (v *FileNameValidator) Validate(ctx context.Context, req interface{}) error {
	uploadReq, ok := req.(UploadRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if uploadReq.FileName == "" {
		return errors.NewBadRequest("File name is required")
	}

	if len(uploadReq.FileName) > v.MaxLength {
		return errors.NewBadRequest("File name is too long")
	}

	// Check for dangerous characters
	dangerous := []string{"..", "/", "\\", "\x00"}
	for _, d := range dangerous {
		if strings.Contains(uploadReq.FileName, d) {
			return errors.NewBadRequest("File name contains invalid characters")
		}
	}

	return v.ValidateNext(ctx, req)
}
photo.GalleryIDValidator.Validate method · go · L230-L241 (12 LOC)
backend/internal/domain/photo/validators.go
func (v *GalleryIDValidator) Validate(ctx context.Context, req interface{}) error {
	uploadReq, ok := req.(UploadRequest)
	if !ok {
		return v.ValidateNext(ctx, req)
	}

	if uploadReq.GalleryID == "" {
		return errors.NewBadRequest("Gallery ID is required")
	}

	return v.ValidateNext(ctx, req)
}
photo.NewUploadValidationChain function · go · L244-L255 (12 LOC)
backend/internal/domain/photo/validators.go
func NewUploadValidationChain() Validator {
	galleryID := NewGalleryIDValidator()
	fileName := NewFileNameValidator()
	fileType := NewFileTypeValidator()
	fileExt := NewFileExtensionValidator()
	fileSize := NewFileSizeValidator()

	// Build the chain
	galleryID.SetNext(fileName).SetNext(fileType).SetNext(fileExt).SetNext(fileSize)

	return galleryID
}
photoversion.NewService function · go · L31-L47 (17 LOC)
backend/internal/domain/photoversion/service.go
func NewService(
	versionRepo repository.PhotoVersionRepository,
	photoRepo repository.PhotoRepository,
	galleryRepo repository.GalleryRepository,
	favoriteRepo repository.FavoriteRepository,
	storage StorageService,
	eventBus events.EventBus,
) *Service {
	return &Service{
		versionRepo:  versionRepo,
		photoRepo:    photoRepo,
		galleryRepo:  galleryRepo,
		favoriteRepo: favoriteRepo,
		storage:      storage,
		eventBus:     eventBus,
	}
}
photoversion.Service.GenerateReplaceUploadURL method · go · L67-L122 (56 LOC)
backend/internal/domain/photoversion/service.go
func (s *Service) GenerateReplaceUploadURL(ctx context.Context, photoID, photographerID, fileName, mimeType string) (*ReplaceUploadURLResponse, error) {
	// Get the photo
	photo, err := s.photoRepo.GetByID(ctx, photoID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get photo")
	}
	if photo == nil {
		return nil, errors.NewNotFound("Photo")
	}

	// Verify ownership through gallery
	gallery, err := s.galleryRepo.GetByID(ctx, photo.GalleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil || gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to replace this photo")
	}

	// Get the next version number
	currentVersion := photo.CurrentVersion
	if currentVersion == 0 {
		currentVersion = 1
	}
	newVersion := currentVersion + 1

	// Generate version ID
	versionID := utils.GenerateID("ver")

	// Generate presigned upload URL
	uploadResp, err := s.storage.GenerateUploadURL(
photoversion.Service.ReplacePhoto method · go · L125-L205 (81 LOC)
backend/internal/domain/photoversion/service.go
func (s *Service) ReplacePhoto(ctx context.Context, photoID, photographerID string, input ReplacePhotoInput) (*repository.Photo, error) {
	// Get the photo
	photo, err := s.photoRepo.GetByID(ctx, photoID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get photo")
	}
	if photo == nil {
		return nil, errors.NewNotFound("Photo")
	}

	// Verify ownership through gallery
	gallery, err := s.galleryRepo.GetByID(ctx, photo.GalleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil || gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to replace this photo")
	}

	// Archive the current version
	oldVersion := photo.CurrentVersion
	if oldVersion == 0 {
		oldVersion = 1
	}

	version := &repository.PhotoVersion{
		VersionID:       utils.GenerateID("ver"),
		PhotoID:         photoID,
		GalleryID:       photo.GalleryID,
		VersionNumber:   oldVersion,
		FileName:        photo.File
photoversion.Service.GetVersionHistory method · go · L208-L233 (26 LOC)
backend/internal/domain/photoversion/service.go
func (s *Service) GetVersionHistory(ctx context.Context, photoID, photographerID string) ([]*repository.PhotoVersion, error) {
	// Get the photo
	photo, err := s.photoRepo.GetByID(ctx, photoID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get photo")
	}
	if photo == nil {
		return nil, errors.NewNotFound("Photo")
	}

	// Verify ownership through gallery
	gallery, err := s.galleryRepo.GetByID(ctx, photo.GalleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil || gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to view this photo's history")
	}

	versions, err := s.versionRepo.ListByPhoto(ctx, photoID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get version history")
	}

	return versions, nil
}
proofing.NewService function · go · L50-L66 (17 LOC)
backend/internal/domain/proofing/service.go
func NewService(
	galleryRepo GalleryRepository,
	photoRepo PhotoRepository,
	commentRepo repository.PhotoCommentRepository,
	editRequestRepo repository.EditRequestRepository,
	favoriteRepo FavoriteRepository,
	subscriptionSvc SubscriptionService,
) *Service {
	return &Service{
		galleryRepo:     galleryRepo,
		photoRepo:       photoRepo,
		commentRepo:     commentRepo,
		editRequestRepo: editRequestRepo,
		favoriteRepo:    favoriteRepo,
		subscriptionSvc: subscriptionSvc,
	}
}
Want this analysis on your repo? https://repobility.com/scan/
proofing.Service.TransitionToProofing method · go · L80-L111 (32 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) TransitionToProofing(ctx context.Context, req TransitionToProofingRequest) error {
	// Check tier access
	hasFeature, err := s.subscriptionSvc.HasFeature(ctx, req.PhotographerID, subscription.FeatureClientProofing)
	if err != nil {
		return fmt.Errorf("failed to check proofing feature access: %w", err)
	}
	if !hasFeature {
		return errors.NewForbidden("Client proofing requires Pro or Studio tier")
	}

	// Get gallery and verify ownership
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, req.GalleryID, req.PhotographerID)
	if err != nil {
		return fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return errors.NewNotFound("Gallery")
	}

	// Transition to proofing phase
	gallery.Phase = repository.GalleryPhaseProofing
	gallery.PhaseTransitionedAt = time.Now()
	gallery.WatermarksEnabled = true
	gallery.DownloadsEnabled = false
	gallery.CommentsEnabled = true

	if err := s.galleryRepo.Update(ctx, gallery); err != nil {
		return fmt.Errorf("fa
proofing.Service.TransitionToUploading method · go · L114-L136 (23 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) TransitionToUploading(ctx context.Context, galleryID, photographerID string) error {
	// Get gallery and verify ownership
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, galleryID, photographerID)
	if err != nil {
		return fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return errors.NewNotFound("Gallery")
	}

	// Transition to uploading phase
	gallery.Phase = repository.GalleryPhaseUploading
	gallery.PhaseTransitionedAt = time.Now()
	gallery.WatermarksEnabled = false
	gallery.DownloadsEnabled = false
	gallery.CommentsEnabled = false

	if err := s.galleryRepo.Update(ctx, gallery); err != nil {
		return fmt.Errorf("failed to transition to uploading: %w", err)
	}

	return nil
}
proofing.Service.IsProofingEnabled method · go · L139-L149 (11 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) IsProofingEnabled(ctx context.Context, galleryID string) (bool, error) {
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return false, fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return false, errors.NewNotFound("Gallery")
	}

	return isProofingPhase(gallery.Phase), nil
}
‹ prevpage 6 / 20next ›