Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,24 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/domain/collection/constants.go
|
||||
package collection
|
||||
|
||||
const (
|
||||
CollectionTypeFolder = "folder"
|
||||
CollectionTypeAlbum = "album"
|
||||
)
|
||||
|
||||
const ( // Permission levels
|
||||
CollectionPermissionReadOnly = "read_only"
|
||||
CollectionPermissionReadWrite = "read_write"
|
||||
CollectionPermissionAdmin = "admin"
|
||||
)
|
||||
|
||||
const (
|
||||
CollectionStateActive = "active"
|
||||
CollectionStateDeleted = "deleted"
|
||||
CollectionStateArchived = "archived"
|
||||
)
|
||||
|
||||
const (
|
||||
CollectionAccessTypeOwner = "owner"
|
||||
CollectionAccessTypeMember = "member"
|
||||
)
|
||||
43
cloud/maplefile-backend/internal/domain/collection/filter.go
Normal file
43
cloud/maplefile-backend/internal/domain/collection/filter.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/domain/collection/filter.go
|
||||
package collection
|
||||
|
||||
import "github.com/gocql/gocql"
|
||||
|
||||
// CollectionFilterOptions defines the filtering options for retrieving collections
|
||||
type CollectionFilterOptions struct {
|
||||
// IncludeOwned includes collections where the user is the owner
|
||||
IncludeOwned bool `json:"include_owned"`
|
||||
// IncludeShared includes collections where the user is a member (shared with them)
|
||||
IncludeShared bool `json:"include_shared"`
|
||||
// UserID is the user for whom we're filtering collections
|
||||
UserID gocql.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
// CollectionFilterResult represents the result of a filtered collection query
|
||||
type CollectionFilterResult struct {
|
||||
// OwnedCollections are collections where the user is the owner
|
||||
OwnedCollections []*Collection `json:"owned_collections"`
|
||||
// SharedCollections are collections shared with the user
|
||||
SharedCollections []*Collection `json:"shared_collections"`
|
||||
// TotalCount is the total number of collections returned
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
// GetAllCollections returns all collections (owned + shared) in a single slice
|
||||
func (r *CollectionFilterResult) GetAllCollections() []*Collection {
|
||||
allCollections := make([]*Collection, 0, len(r.OwnedCollections)+len(r.SharedCollections))
|
||||
allCollections = append(allCollections, r.OwnedCollections...)
|
||||
allCollections = append(allCollections, r.SharedCollections...)
|
||||
return allCollections
|
||||
}
|
||||
|
||||
// IsValid checks if the filter options are valid
|
||||
func (options *CollectionFilterOptions) IsValid() bool {
|
||||
// At least one filter option must be enabled
|
||||
return options.IncludeOwned || options.IncludeShared
|
||||
}
|
||||
|
||||
// ShouldIncludeAll returns true if both owned and shared collections should be included
|
||||
func (options *CollectionFilterOptions) ShouldIncludeAll() bool {
|
||||
return options.IncludeOwned && options.IncludeShared
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/collection/interface.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
)
|
||||
|
||||
// CollectionRepository defines the interface for collection persistence operations
|
||||
type CollectionRepository interface {
|
||||
// Collection CRUD operations
|
||||
Create(ctx context.Context, collection *Collection) error
|
||||
Get(ctx context.Context, id gocql.UUID) (*Collection, error)
|
||||
Update(ctx context.Context, collection *Collection) error
|
||||
SoftDelete(ctx context.Context, id gocql.UUID) error // Now soft delete
|
||||
HardDelete(ctx context.Context, id gocql.UUID) error
|
||||
|
||||
// State management operations
|
||||
Archive(ctx context.Context, id gocql.UUID) error
|
||||
Restore(ctx context.Context, id gocql.UUID) error
|
||||
|
||||
// Hierarchical queries (now state-aware)
|
||||
FindByParent(ctx context.Context, parentID gocql.UUID) ([]*Collection, error)
|
||||
FindRootCollections(ctx context.Context, ownerID gocql.UUID) ([]*Collection, error)
|
||||
FindDescendants(ctx context.Context, collectionID gocql.UUID) ([]*Collection, error)
|
||||
// GetFullHierarchy(ctx context.Context, rootID gocql.UUID) (*Collection, error) // DEPRECATED AND WILL BE REMOVED
|
||||
|
||||
// Move collection to a new parent
|
||||
MoveCollection(ctx context.Context, collectionID, newParentID gocql.UUID, updatedAncestors []gocql.UUID, updatedPathSegments []string) error
|
||||
|
||||
// Collection ownership and access queries (now state-aware)
|
||||
CheckIfExistsByID(ctx context.Context, id gocql.UUID) (bool, error)
|
||||
GetAllByUserID(ctx context.Context, ownerID gocql.UUID) ([]*Collection, error)
|
||||
GetCollectionsSharedWithUser(ctx context.Context, userID gocql.UUID) ([]*Collection, error)
|
||||
IsCollectionOwner(ctx context.Context, collectionID, userID gocql.UUID) (bool, error)
|
||||
CheckAccess(ctx context.Context, collectionID, userID gocql.UUID, requiredPermission string) (bool, error)
|
||||
GetUserPermissionLevel(ctx context.Context, collectionID, userID gocql.UUID) (string, error)
|
||||
|
||||
// Filtered collection queries (now state-aware)
|
||||
GetCollectionsWithFilter(ctx context.Context, options CollectionFilterOptions) (*CollectionFilterResult, error)
|
||||
|
||||
// Collection membership operations
|
||||
AddMember(ctx context.Context, collectionID gocql.UUID, membership *CollectionMembership) error
|
||||
RemoveMember(ctx context.Context, collectionID, recipientID gocql.UUID) error
|
||||
RemoveUserFromAllCollections(ctx context.Context, userID gocql.UUID, userEmail string) ([]gocql.UUID, error)
|
||||
UpdateMemberPermission(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error
|
||||
GetCollectionMembership(ctx context.Context, collectionID, recipientID gocql.UUID) (*CollectionMembership, error)
|
||||
|
||||
// Hierarchical sharing
|
||||
AddMemberToHierarchy(ctx context.Context, rootID gocql.UUID, membership *CollectionMembership) error
|
||||
RemoveMemberFromHierarchy(ctx context.Context, rootID, recipientID gocql.UUID) error
|
||||
|
||||
// GetCollectionSyncData retrieves collection sync data with pagination for the specified user
|
||||
GetCollectionSyncData(ctx context.Context, userID gocql.UUID, cursor *CollectionSyncCursor, limit int64) (*CollectionSyncResponse, error)
|
||||
GetCollectionSyncDataByAccessType(ctx context.Context, userID gocql.UUID, cursor *CollectionSyncCursor, limit int64, accessType string) (*CollectionSyncResponse, error)
|
||||
|
||||
// Count operations for all collection types (folders + albums)
|
||||
CountOwnedCollections(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
CountSharedCollections(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
CountOwnedFolders(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
CountSharedFolders(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
CountTotalUniqueFolders(ctx context.Context, userID gocql.UUID) (int, error)
|
||||
|
||||
// IP Anonymization for GDPR compliance
|
||||
AnonymizeOldIPs(ctx context.Context, cutoffDate time.Time) (int, error)
|
||||
AnonymizeCollectionIPsByOwner(ctx context.Context, ownerID gocql.UUID) (int, error) // For GDPR right-to-be-forgotten
|
||||
|
||||
// File count maintenance operations
|
||||
IncrementFileCount(ctx context.Context, collectionID gocql.UUID) error
|
||||
DecrementFileCount(ctx context.Context, collectionID gocql.UUID) error
|
||||
|
||||
// RecalculateAllFileCounts recalculates file_count for all collections
|
||||
// by counting active files. Used for data migration/repair.
|
||||
RecalculateAllFileCounts(ctx context.Context) (*RecalculateAllFileCountsResult, error)
|
||||
|
||||
// Tag-related operations
|
||||
// ListByTagID retrieves all collections that have the specified tag assigned
|
||||
// Used for tag update propagation (updating embedded tag data across all collections)
|
||||
ListByTagID(ctx context.Context, tagID gocql.UUID) ([]*Collection, error)
|
||||
}
|
||||
|
||||
// RecalculateAllFileCountsResult holds the results of the recalculation operation
|
||||
type RecalculateAllFileCountsResult struct {
|
||||
TotalCollections int
|
||||
UpdatedCount int
|
||||
ErrorCount int
|
||||
}
|
||||
124
cloud/maplefile-backend/internal/domain/collection/model.go
Normal file
124
cloud/maplefile-backend/internal/domain/collection/model.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/collection/model.go
|
||||
package collection
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
|
||||
"github.com/gocql/gocql"
|
||||
)
|
||||
|
||||
// Collection represents a folder or album.
|
||||
// Can be used for both root collections and embedded subcollections
|
||||
type Collection struct {
|
||||
// Identifiers
|
||||
// ID is the unique identifier for the collection in the cloud backend.
|
||||
ID gocql.UUID `bson:"_id" json:"id"`
|
||||
// OwnerID is the ID of the user who originally created and owns this collection.
|
||||
// The owner has administrative privileges by default.
|
||||
OwnerID gocql.UUID `bson:"owner_id" json:"owner_id"`
|
||||
|
||||
// Encryption and Content Details
|
||||
// EncryptedName is the name of the collection, encrypted using the collection's unique key.
|
||||
// Stored and transferred in encrypted form.
|
||||
EncryptedName string `bson:"encrypted_name" json:"encrypted_name"`
|
||||
// CollectionType indicates the nature of the collection, either "folder" or "album".
|
||||
// Defined by CollectionTypeFolder and CollectionTypeAlbum constants.
|
||||
CollectionType string `bson:"collection_type" json:"collection_type"` // "folder" or "album"
|
||||
// EncryptedCollectionKey is the unique symmetric key used to encrypt the collection's data (like name and file metadata).
|
||||
// This key is encrypted with the owner's master key for storage and transmission,
|
||||
// allowing the owner's device to decrypt it using their master key.
|
||||
EncryptedCollectionKey *crypto.EncryptedCollectionKey `bson:"encrypted_collection_key" json:"encrypted_collection_key"`
|
||||
// EncryptedCustomIcon stores the custom icon for this collection, encrypted with the collection key.
|
||||
// Empty string means use default folder/album icon.
|
||||
// Contains either an emoji character (e.g., "📷") or "icon:<identifier>" for predefined icons.
|
||||
EncryptedCustomIcon string `bson:"encrypted_custom_icon" json:"encrypted_custom_icon"`
|
||||
|
||||
// Sharing
|
||||
// Collection members (users with access)
|
||||
Members []CollectionMembership `bson:"members" json:"members"`
|
||||
|
||||
// Hierarchical structure fields
|
||||
// ParentID is the ID of the parent collection if this is a subcollection.
|
||||
// It is omitted (nil) for root collections. Used to reconstruct the hierarchy.
|
||||
ParentID gocql.UUID `bson:"parent_id,omitempty" json:"parent_id,omitempty"` // Parent collection ID, not stored for root collections
|
||||
// AncestorIDs is an array containing the IDs of all parent collections up to the root.
|
||||
// This field is used for efficient querying and traversal of the collection hierarchy without joins.
|
||||
AncestorIDs []gocql.UUID `bson:"ancestor_ids,omitempty" json:"ancestor_ids,omitempty"` // Array of ancestor IDs for efficient querying
|
||||
|
||||
// File count for performance optimization
|
||||
// FileCount stores the number of active files in this collection.
|
||||
// This denormalized field eliminates N+1 queries when listing collections.
|
||||
FileCount int64 `bson:"file_count" json:"file_count"`
|
||||
|
||||
// DEPRECATED: Replaced by Tags field below
|
||||
// TagIDs []gocql.UUID `bson:"tag_ids,omitempty" json:"tag_ids,omitempty"`
|
||||
|
||||
// Tags stores full embedded tag data (eliminates frontend API lookups)
|
||||
// Stored as JSON text in database, marshaled/unmarshaled automatically
|
||||
Tags []tag.EmbeddedTag `bson:"tags,omitempty" json:"tags,omitempty"`
|
||||
|
||||
// Ownership, timestamps and conflict resolution
|
||||
// CreatedAt is the timestamp when the collection was initially created.
|
||||
// Recorded on the local device and synced.
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
// CreatedByUserID is the ID of the user who created this file.
|
||||
CreatedByUserID gocql.UUID `bson:"created_by_user_id" json:"created_by_user_id"`
|
||||
// ModifiedAt is the timestamp of the last modification to the collection's metadata or content.
|
||||
// Updated on the local device and synced.
|
||||
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
|
||||
ModifiedByUserID gocql.UUID `bson:"modified_by_user_id" json:"modified_by_user_id"`
|
||||
// The current version of the file.
|
||||
Version uint64 `bson:"version" json:"version"` // Every mutation (create, update, delete, etc) is a versioned operation, keep track of the version number with this variable
|
||||
|
||||
// State management
|
||||
State string `bson:"state" json:"state"` // active, deleted, archived
|
||||
TombstoneVersion uint64 `bson:"tombstone_version" json:"tombstone_version"` // The `version` number that this collection was deleted at.
|
||||
TombstoneExpiry time.Time `bson:"tombstone_expiry" json:"tombstone_expiry"`
|
||||
}
|
||||
|
||||
// CollectionMembership represents a user's access to a collection
|
||||
type CollectionMembership struct {
|
||||
ID gocql.UUID `bson:"_id" json:"id"`
|
||||
CollectionID gocql.UUID `bson:"collection_id" json:"collection_id"` // ID of the collection (redundant but helpful for queries)
|
||||
RecipientID gocql.UUID `bson:"recipient_id" json:"recipient_id"` // User receiving access
|
||||
RecipientEmail string `bson:"recipient_email" json:"recipient_email"` // Email for display purposes
|
||||
GrantedByID gocql.UUID `bson:"granted_by_id" json:"granted_by_id"` // User who shared the collection
|
||||
|
||||
// Collection key encrypted with recipient's public key using box_seal. This matches the box_seal format which doesn't need a separate nonce.
|
||||
EncryptedCollectionKey []byte `bson:"encrypted_collection_key" json:"encrypted_collection_key"`
|
||||
|
||||
// Access details
|
||||
PermissionLevel string `bson:"permission_level" json:"permission_level"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
|
||||
// Sharing origin tracking
|
||||
IsInherited bool `bson:"is_inherited" json:"is_inherited"` // Tracks whether access was granted directly or inherited from a parent
|
||||
InheritedFromID gocql.UUID `bson:"inherited_from_id,omitempty" json:"inherited_from_id,omitempty"` // InheritedFromID identifies which parent collection granted this access
|
||||
}
|
||||
|
||||
// CollectionSyncCursor represents cursor-based pagination for sync operations
|
||||
type CollectionSyncCursor struct {
|
||||
LastModified time.Time `json:"last_modified" bson:"last_modified"`
|
||||
LastID gocql.UUID `json:"last_id" bson:"last_id"`
|
||||
}
|
||||
|
||||
// CollectionSyncItem represents minimal collection data for sync operations
|
||||
type CollectionSyncItem struct {
|
||||
ID gocql.UUID `json:"id" bson:"_id"`
|
||||
Version uint64 `json:"version" bson:"version"`
|
||||
ModifiedAt time.Time `json:"modified_at" bson:"modified_at"`
|
||||
State string `json:"state" bson:"state"`
|
||||
ParentID *gocql.UUID `json:"parent_id,omitempty" bson:"parent_id,omitempty"`
|
||||
TombstoneVersion uint64 `bson:"tombstone_version" json:"tombstone_version"`
|
||||
TombstoneExpiry time.Time `bson:"tombstone_expiry" json:"tombstone_expiry"`
|
||||
EncryptedCustomIcon string `json:"encrypted_custom_icon,omitempty" bson:"encrypted_custom_icon,omitempty"`
|
||||
}
|
||||
|
||||
// CollectionSyncResponse represents the response for collection sync data
|
||||
type CollectionSyncResponse struct {
|
||||
Collections []CollectionSyncItem `json:"collections"`
|
||||
NextCursor *CollectionSyncCursor `json:"next_cursor,omitempty"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// monorepo/cloud/backend/internal/maplefile/domain/collection/state_validator.go
|
||||
package collection
|
||||
|
||||
import "errors"
|
||||
|
||||
// StateTransition validates collection state transitions
|
||||
type StateTransition struct {
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
// IsValidStateTransition checks if a state transition is allowed
|
||||
func IsValidStateTransition(from, to string) error {
|
||||
validTransitions := map[StateTransition]bool{
|
||||
// From active
|
||||
{CollectionStateActive, CollectionStateDeleted}: true,
|
||||
{CollectionStateActive, CollectionStateArchived}: true,
|
||||
|
||||
// From deleted (cannot be restored nor archived)
|
||||
{CollectionStateDeleted, CollectionStateActive}: false,
|
||||
{CollectionStateDeleted, CollectionStateArchived}: false,
|
||||
|
||||
// From archived (can only be restored to active)
|
||||
{CollectionStateArchived, CollectionStateActive}: true,
|
||||
|
||||
// Same state transitions (no-op)
|
||||
{CollectionStateActive, CollectionStateActive}: true,
|
||||
{CollectionStateDeleted, CollectionStateDeleted}: true,
|
||||
{CollectionStateArchived, CollectionStateArchived}: true,
|
||||
}
|
||||
|
||||
if !validTransitions[StateTransition{from, to}] {
|
||||
return errors.New("invalid state transition from " + from + " to " + to)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue