247 lines
11 KiB
Go
247 lines
11 KiB
Go
// 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
|
|
}
|