// monorepo/cloud/maplefile-backend/internal/maplefile/repo/filemetadata/get.go package filemetadata import ( "fmt" "sync" "time" "github.com/gocql/gocql" "go.uber.org/zap" dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file" ) func (impl *fileMetadataRepositoryImpl) Get(id gocql.UUID) (*dom_file.File, error) { var ( collectionID, ownerID, createdByUserID, modifiedByUserID gocql.UUID encryptedMetadata, encryptedKeyJSON, encryptionVersion string encryptedHash, encryptedFileObjectKey string encryptedThumbnailObjectKey string encryptedFileSizeInBytes, encryptedThumbnailSizeInBytes int64 tagsJSON string createdAt, modifiedAt, tombstoneExpiry time.Time version, tombstoneVersion uint64 state string ) query := `SELECT id, collection_id, owner_id, encrypted_metadata, encrypted_file_key, encryption_version, encrypted_hash, encrypted_file_object_key, encrypted_file_size_in_bytes, encrypted_thumbnail_object_key, encrypted_thumbnail_size_in_bytes, tags, created_at, created_by_user_id, modified_at, modified_by_user_id, version, state, tombstone_version, tombstone_expiry FROM maplefile.files_by_id WHERE id = ?` err := impl.Session.Query(query, id).Scan( &id, &collectionID, &ownerID, &encryptedMetadata, &encryptedKeyJSON, &encryptionVersion, &encryptedHash, &encryptedFileObjectKey, &encryptedFileSizeInBytes, &encryptedThumbnailObjectKey, &encryptedThumbnailSizeInBytes, &tagsJSON, &createdAt, &createdByUserID, &modifiedAt, &modifiedByUserID, &version, &state, &tombstoneVersion, &tombstoneExpiry) if err != nil { if err == gocql.ErrNotFound { return nil, nil } return nil, fmt.Errorf("failed to get file: %w", err) } // Deserialize encrypted file key encryptedFileKey, err := impl.deserializeEncryptedFileKey(encryptedKeyJSON) if err != nil { return nil, fmt.Errorf("failed to deserialize encrypted file key: %w", err) } // Deserialize tags tags, err := impl.deserializeTags(tagsJSON) if err != nil { return nil, fmt.Errorf("failed to deserialize tags: %w", err) } file := &dom_file.File{ ID: id, CollectionID: collectionID, OwnerID: ownerID, EncryptedMetadata: encryptedMetadata, EncryptedFileKey: encryptedFileKey, EncryptionVersion: encryptionVersion, EncryptedHash: encryptedHash, EncryptedFileObjectKey: encryptedFileObjectKey, EncryptedFileSizeInBytes: encryptedFileSizeInBytes, EncryptedThumbnailObjectKey: encryptedThumbnailObjectKey, EncryptedThumbnailSizeInBytes: encryptedThumbnailSizeInBytes, Tags: tags, CreatedAt: createdAt, CreatedByUserID: createdByUserID, ModifiedAt: modifiedAt, ModifiedByUserID: modifiedByUserID, Version: version, State: state, TombstoneVersion: tombstoneVersion, TombstoneExpiry: tombstoneExpiry, } return file, nil } func (impl *fileMetadataRepositoryImpl) GetByIDs(ids []gocql.UUID) ([]*dom_file.File, error) { if len(ids) == 0 { return []*dom_file.File{}, nil } // Use a buffered channel to collect results from goroutines resultsChan := make(chan *dom_file.File, len(ids)) var wg sync.WaitGroup // Launch a goroutine for each ID lookup for _, id := range ids { wg.Add(1) go func(id gocql.UUID) { defer wg.Done() // Call the existing state-aware Get method file, err := impl.Get(id) if err != nil { impl.Logger.Warn("failed to get file by ID", zap.String("file_id", id.String()), zap.Error(err)) // Send nil on error to indicate failure/absence for this ID resultsChan <- nil return } // Get returns nil for ErrNotFound or inactive state when stateAware is true. // Send the potentially nil file result to the channel. resultsChan <- file }(id) // Pass id into the closure } // Goroutine to close the channel once all workers are done go func() { wg.Wait() close(resultsChan) }() // Collect results from the channel var files []*dom_file.File for file := range resultsChan { // Only append non-nil files (found and active) if file != nil { files = append(files, file) } } // The original function logs warnings for errors but doesn't return an error // from GetByIDs itself. We maintain this behavior. return files, nil } func (impl *fileMetadataRepositoryImpl) GetByCollection(collectionID gocql.UUID) ([]*dom_file.File, error) { var fileIDs []gocql.UUID query := `SELECT id FROM maplefile.files_by_collection WHERE collection_id = ?` iter := impl.Session.Query(query, collectionID).Iter() var fileID gocql.UUID for iter.Scan(&fileID) { fileIDs = append(fileIDs, fileID) } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to get files by collection: %w", err) } return impl.loadMultipleFiles(fileIDs) } func (impl *fileMetadataRepositoryImpl) loadMultipleFiles(fileIDs []gocql.UUID) ([]*dom_file.File, error) { if len(fileIDs) == 0 { return []*dom_file.File{}, nil } // Use a buffered channel to collect results from goroutines // We expect up to len(fileIDs) results, some of which might be nil. resultsChan := make(chan *dom_file.File, len(fileIDs)) var wg sync.WaitGroup // Launch a goroutine for each ID lookup for _, id := range fileIDs { wg.Add(1) go func(id gocql.UUID) { defer wg.Done() // Call the existing state-aware Get method // This method returns nil if the file is not found, or if it's // found but not in the 'active' state. file, err := impl.Get(id) if err != nil { // Log the error but continue processing other IDs. impl.Logger.Warn("failed to load file", zap.String("file_id", id.String()), zap.Error(err)) // Send nil on error, consistent with how Get returns nil for not found/inactive. resultsChan <- nil return } // Get returns nil for ErrNotFound or inactive state when stateAware is true. // Send the potentially nil file result to the channel. resultsChan <- file }(id) // Pass id into the closure } // Goroutine to close the channel once all workers are done go func() { wg.Wait() close(resultsChan) }() // Collect results from the channel var files []*dom_file.File for file := range resultsChan { // Only append non-nil files (found and active, or found but error logged) if file != nil { files = append(files, file) } } // The original function logged warnings for errors but didn't return an error // from loadMultipleFiles itself. We maintain this behavior. return files, nil }