// monorepo/cloud/maplefile-backend/internal/maplefile/repo/filemetadata/update.go package filemetadata import ( "context" "fmt" "time" "github.com/gocql/gocql" "go.uber.org/zap" dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file" ) func (impl *fileMetadataRepositoryImpl) Update(file *dom_file.File) error { if file == nil { return fmt.Errorf("file cannot be nil") } if !impl.isValidUUID(file.ID) { return fmt.Errorf("file ID is required") } // Get existing file to compare changes existing, err := impl.Get(file.ID) if err != nil { return fmt.Errorf("failed to get existing file: %w", err) } if existing == nil { return fmt.Errorf("file not found") } // Update modified timestamp file.ModifiedAt = time.Now() // Serialize encrypted file key encryptedKeyJSON, err := impl.serializeEncryptedFileKey(file.EncryptedFileKey) if err != nil { return fmt.Errorf("failed to serialize encrypted file key: %w", err) } // Serialize tags tagsJSON, err := impl.serializeTags(file.Tags) if err != nil { return fmt.Errorf("failed to serialize tags: %w", err) } batch := impl.Session.NewBatch(gocql.LoggedBatch) // 1. Update main table batch.Query(`UPDATE maplefile.files_by_id SET 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 = ? WHERE id = ?`, file.CollectionID, file.OwnerID, file.EncryptedMetadata, encryptedKeyJSON, file.EncryptionVersion, file.EncryptedHash, file.EncryptedFileObjectKey, file.EncryptedFileSizeInBytes, file.EncryptedThumbnailObjectKey, file.EncryptedThumbnailSizeInBytes, tagsJSON, file.CreatedAt, file.CreatedByUserID, file.ModifiedAt, file.ModifiedByUserID, file.Version, file.State, file.TombstoneVersion, file.TombstoneExpiry, file.ID) // 2. Update collection table - delete old entry and insert new one if existing.CollectionID != file.CollectionID || existing.ModifiedAt != file.ModifiedAt { batch.Query(`DELETE FROM maplefile.files_by_collection WHERE collection_id = ? AND modified_at = ? AND id = ?`, existing.CollectionID, existing.ModifiedAt, file.ID) batch.Query(`INSERT INTO maplefile.files_by_collection (collection_id, modified_at, 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, created_at, created_by_user_id, modified_by_user_id, version, state, tombstone_version, tombstone_expiry) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, file.CollectionID, file.ModifiedAt, file.ID, file.OwnerID, file.EncryptedMetadata, encryptedKeyJSON, file.EncryptionVersion, file.EncryptedHash, file.EncryptedFileObjectKey, file.EncryptedFileSizeInBytes, file.EncryptedThumbnailObjectKey, file.EncryptedThumbnailSizeInBytes, file.CreatedAt, file.CreatedByUserID, file.ModifiedByUserID, file.Version, file.State, file.TombstoneVersion, file.TombstoneExpiry) } // 3. Update owner table - delete old entry and insert new one if existing.OwnerID != file.OwnerID || existing.ModifiedAt != file.ModifiedAt { batch.Query(`DELETE FROM maplefile.files_by_owner WHERE owner_id = ? AND modified_at = ? AND id = ?`, existing.OwnerID, existing.ModifiedAt, file.ID) batch.Query(`INSERT INTO maplefile.files_by_owner (owner_id, modified_at, id, collection_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, created_at, created_by_user_id, modified_by_user_id, version, state, tombstone_version, tombstone_expiry) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, file.OwnerID, file.ModifiedAt, file.ID, file.CollectionID, file.EncryptedMetadata, encryptedKeyJSON, file.EncryptionVersion, file.EncryptedHash, file.EncryptedFileObjectKey, file.EncryptedFileSizeInBytes, file.EncryptedThumbnailObjectKey, file.EncryptedThumbnailSizeInBytes, file.CreatedAt, file.CreatedByUserID, file.ModifiedByUserID, file.Version, file.State, file.TombstoneVersion, file.TombstoneExpiry) } // 4. Update created_by table - only if creator changed (rare) or created date changed if existing.CreatedByUserID != file.CreatedByUserID || existing.CreatedAt != file.CreatedAt { batch.Query(`DELETE FROM maplefile.files_by_creator WHERE created_by_user_id = ? AND created_at = ? AND id = ?`, existing.CreatedByUserID, existing.CreatedAt, file.ID) batch.Query(`INSERT INTO maplefile.files_by_creator (created_by_user_id, created_at, 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, modified_at, modified_by_user_id, version, state, tombstone_version, tombstone_expiry) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, file.CreatedByUserID, file.CreatedAt, file.ID, file.CollectionID, file.OwnerID, file.EncryptedMetadata, encryptedKeyJSON, file.EncryptionVersion, file.EncryptedHash, file.EncryptedFileObjectKey, file.EncryptedFileSizeInBytes, file.EncryptedThumbnailObjectKey, file.EncryptedThumbnailSizeInBytes, file.ModifiedAt, file.ModifiedByUserID, file.Version, file.State, file.TombstoneVersion, file.TombstoneExpiry) } // 5. Update user sync table - delete old entry and insert new one for owner batch.Query(`DELETE FROM maplefile.files_by_user WHERE user_id = ? AND modified_at = ? AND id = ?`, existing.OwnerID, existing.ModifiedAt, file.ID) batch.Query(`INSERT INTO maplefile.files_by_user (user_id, modified_at, 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_by_user_id, version, state, tombstone_version, tombstone_expiry) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, file.OwnerID, file.ModifiedAt, file.ID, file.CollectionID, file.OwnerID, file.EncryptedMetadata, encryptedKeyJSON, file.EncryptionVersion, file.EncryptedHash, file.EncryptedFileObjectKey, file.EncryptedFileSizeInBytes, file.EncryptedThumbnailObjectKey, file.EncryptedThumbnailSizeInBytes, tagsJSON, file.CreatedAt, file.CreatedByUserID, file.ModifiedByUserID, file.Version, file.State, file.TombstoneVersion, file.TombstoneExpiry) // 6. Update denormalized files_by_tag_id table // Calculate tag changes oldTagsMap := make(map[gocql.UUID]bool) for _, tag := range existing.Tags { oldTagsMap[tag.ID] = true } newTagsMap := make(map[gocql.UUID]bool) for _, tag := range file.Tags { newTagsMap[tag.ID] = true } // Delete entries for removed tags for tagID := range oldTagsMap { if !newTagsMap[tagID] { impl.Logger.Debug("removing file from tag denormalized table", zap.String("file_id", file.ID.String()), zap.String("tag_id", tagID.String())) batch.Query(`DELETE FROM maplefile.files_by_tag_id WHERE tag_id = ? AND file_id = ?`, tagID, file.ID) } } // Insert/Update entries for current tags for _, tag := range file.Tags { impl.Logger.Debug("updating file in tag denormalized table", zap.String("file_id", file.ID.String()), zap.String("tag_id", tag.ID.String())) batch.Query(`INSERT INTO maplefile.files_by_tag_id (tag_id, file_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, tag_ids, created_at, created_by_user_id, modified_at, modified_by_user_id, version, state, tombstone_version, tombstone_expiry, created_from_ip_address, modified_from_ip_address, ip_anonymized_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, tag.ID, file.ID, file.CollectionID, file.OwnerID, file.EncryptedMetadata, encryptedKeyJSON, file.EncryptionVersion, file.EncryptedHash, file.EncryptedFileObjectKey, file.EncryptedFileSizeInBytes, file.EncryptedThumbnailObjectKey, file.EncryptedThumbnailSizeInBytes, tagsJSON, file.CreatedAt, file.CreatedByUserID, file.ModifiedAt, file.ModifiedByUserID, file.Version, file.State, file.TombstoneVersion, file.TombstoneExpiry, nil, nil, nil) // IP tracking fields not yet in domain model } // Execute batch if err := impl.Session.ExecuteBatch(batch); err != nil { impl.Logger.Error("failed to update file", zap.String("file_id", file.ID.String()), zap.Error(err)) return fmt.Errorf("failed to update file: %w", err) } // Handle file count updates based on state changes wasActive := existing.State == dom_file.FileStateActive isActive := file.State == dom_file.FileStateActive // Handle collection change for active files if existing.CollectionID != file.CollectionID && wasActive && isActive { // File moved from one collection to another while remaining active // Decrement old collection count if err := impl.CollectionRepo.DecrementFileCount(context.Background(), existing.CollectionID); err != nil { impl.Logger.Error("failed to decrement old collection file count", zap.String("file_id", file.ID.String()), zap.String("collection_id", existing.CollectionID.String()), zap.Error(err)) // Don't fail the entire operation if count update fails } // Increment new collection count if err := impl.CollectionRepo.IncrementFileCount(context.Background(), file.CollectionID); err != nil { impl.Logger.Error("failed to increment new collection file count", zap.String("file_id", file.ID.String()), zap.String("collection_id", file.CollectionID.String()), zap.Error(err)) // Don't fail the entire operation if count update fails } } else if wasActive && !isActive { // File transitioned from active to non-active (e.g., deleted) if err := impl.CollectionRepo.DecrementFileCount(context.Background(), existing.CollectionID); err != nil { impl.Logger.Error("failed to decrement collection file count", zap.String("file_id", file.ID.String()), zap.String("collection_id", existing.CollectionID.String()), zap.Error(err)) // Don't fail the entire operation if count update fails } } else if !wasActive && isActive { // File transitioned from non-active to active (e.g., restored) if err := impl.CollectionRepo.IncrementFileCount(context.Background(), file.CollectionID); err != nil { impl.Logger.Error("failed to increment collection file count", zap.String("file_id", file.ID.String()), zap.String("collection_id", file.CollectionID.String()), zap.Error(err)) // Don't fail the entire operation if count update fails } } impl.Logger.Info("file updated successfully", zap.String("file_id", file.ID.String())) return nil }