adding documentation
This commit is contained in:
@@ -0,0 +1,708 @@
|
|||||||
|
# OxiCloud Code Documentation
|
||||||
|
|
||||||
|
This document provides a detailed description of each file in the OxiCloud codebase, organized by architectural layers according to the clean hexagonal architecture pattern.
|
||||||
|
|
||||||
|
## Domain Layer
|
||||||
|
|
||||||
|
The domain layer forms the core of the application, containing business entities and repository interfaces.
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
|
||||||
|
**File Entity (`src/domain/entities/file.rs`)**
|
||||||
|
- Core domain entity representing files in the system
|
||||||
|
- Implements an immutable design pattern for file operations
|
||||||
|
- Provides validation, creation, and manipulation methods for files
|
||||||
|
- Maintains both physical storage information and logical metadata
|
||||||
|
- Includes error handling via `FileError` for validation failures
|
||||||
|
|
||||||
|
**Folder Entity (`src/domain/entities/folder.rs`)**
|
||||||
|
- Represents folders/directories in the domain model
|
||||||
|
- Supports hierarchical structure with parent-child relationships
|
||||||
|
- Provides validation, creation, and update operations
|
||||||
|
- Implements immutable pattern with methods returning new instances
|
||||||
|
- Handles path resolution for proper folder hierarchy
|
||||||
|
|
||||||
|
**User Entity (`src/domain/entities/user.rs`)**
|
||||||
|
- Manages user accounts and authentication
|
||||||
|
- Provides secure password handling with Argon2 hashing
|
||||||
|
- Supports roles (Admin, User) with appropriate permissions
|
||||||
|
- Tracks storage usage and quotas
|
||||||
|
- Includes account management functions (activation, deactivation, login tracking)
|
||||||
|
|
||||||
|
**Share Entity (`src/domain/entities/share.rs`)**
|
||||||
|
- Implements file and folder sharing functionality
|
||||||
|
- Supports various permission levels (read, write, reshare)
|
||||||
|
- Provides password protection for shared resources
|
||||||
|
- Handles expiration dates for temporary sharing
|
||||||
|
- Tracks access statistics for shared resources
|
||||||
|
|
||||||
|
**Calendar Entity (`src/domain/entities/calendar.rs`)**
|
||||||
|
- Supports calendar functionality for CalDAV integration
|
||||||
|
- Manages calendar properties like name, color, and description
|
||||||
|
- Provides ownership and access control
|
||||||
|
- Supports custom properties for extended CalDAV compatibility
|
||||||
|
- Handles validation of calendar data
|
||||||
|
|
||||||
|
**Calendar Event Entity (`src/domain/entities/calendar_event.rs`)**
|
||||||
|
- Represents calendar events with properties like title, description, location
|
||||||
|
- Handles date/time management with recurrence rules
|
||||||
|
- Supports reminders and notifications
|
||||||
|
- Provides validation for event data
|
||||||
|
- Implements custom properties for CalDAV compatibility
|
||||||
|
|
||||||
|
**Trashed Item Entity (`src/domain/entities/trashed_item.rs`)**
|
||||||
|
- Manages files and folders in the trash
|
||||||
|
- Tracks original locations for restoration
|
||||||
|
- Implements automatic cleanup based on retention policies
|
||||||
|
- Provides restoration and permanent deletion functionality
|
||||||
|
|
||||||
|
### Repositories (Interfaces)
|
||||||
|
|
||||||
|
**File Repository (`src/domain/repositories/file_repository.rs`)**
|
||||||
|
- Defines the contract for file storage operations
|
||||||
|
- Abstracts storage implementation details from the domain
|
||||||
|
- Supports file creation, retrieval, updating, and deletion
|
||||||
|
- Provides methods for content streaming and file movement
|
||||||
|
- Includes trash functionality for file lifecycle management
|
||||||
|
|
||||||
|
**Folder Repository (`src/domain/repositories/folder_repository.rs`)**
|
||||||
|
- Defines the interface for folder manipulation
|
||||||
|
- Abstracts storage implementation details for directories
|
||||||
|
- Handles folder creation, listing, and hierarchy management
|
||||||
|
- Supports moving folders and retrieving path information
|
||||||
|
- Includes trash operations for folders
|
||||||
|
|
||||||
|
**User Repository (`src/domain/repositories/user_repository.rs`)**
|
||||||
|
- Defines the interface for user data persistence
|
||||||
|
- Supports user creation, retrieval, and management
|
||||||
|
- Provides authentication and session management
|
||||||
|
- Handles user preferences and settings storage
|
||||||
|
- Manages user quotas and storage usage tracking
|
||||||
|
|
||||||
|
**Share Repository (`src/domain/repositories/share_repository.rs`)**
|
||||||
|
- Defines the interface for share record management
|
||||||
|
- Handles creation and validation of share records
|
||||||
|
- Tracks permissions and expiration settings
|
||||||
|
- Provides access verification for shared resources
|
||||||
|
- Manages share revocation and updates
|
||||||
|
|
||||||
|
**Trash Repository (`src/domain/repositories/trash_repository.rs`)**
|
||||||
|
- Defines the interface for trash operations
|
||||||
|
- Manages soft deletion and restoration of resources
|
||||||
|
- Handles retention policies and automatic cleanup
|
||||||
|
- Provides listing of trashed items with metadata
|
||||||
|
- Supports permanent deletion operations
|
||||||
|
|
||||||
|
### Domain Services
|
||||||
|
|
||||||
|
**Auth Service (`src/domain/services/auth_service.rs`)**
|
||||||
|
- Provides domain-level authentication logic
|
||||||
|
- Implements password validation and hashing
|
||||||
|
- Defines authentication policies and rules
|
||||||
|
- Manages token generation and validation
|
||||||
|
- Handles security-related domain operations
|
||||||
|
|
||||||
|
**I18n Service (`src/domain/services/i18n_service.rs`)**
|
||||||
|
- Defines domain-level internationalization interface
|
||||||
|
- Provides translation lookup capabilities
|
||||||
|
- Manages localization strategies
|
||||||
|
- Supports multiple languages and fallbacks
|
||||||
|
- Handles format localization for dates, numbers, etc.
|
||||||
|
|
||||||
|
**Path Service (`src/domain/services/path_service.rs`)**
|
||||||
|
- Manages domain-level path abstractions
|
||||||
|
- Provides path validation and normalization
|
||||||
|
- Handles path traversal and resolution
|
||||||
|
- Implements path security measures
|
||||||
|
- Supports different path formats and conventions
|
||||||
|
|
||||||
|
## Application Layer
|
||||||
|
|
||||||
|
The application layer orchestrates use cases by coordinating domain objects and providing services to the interfaces layer.
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
**File Service (`src/application/services/file_service.rs`)**
|
||||||
|
- Implements file-related use cases
|
||||||
|
- Coordinates between repositories for file operations
|
||||||
|
- Provides file upload, download, and listing functionality
|
||||||
|
- Handles error translation between layers
|
||||||
|
- Contains business logic for file operations
|
||||||
|
|
||||||
|
**Folder Service (`src/application/services/folder_service.rs`)**
|
||||||
|
- Implements folder management use cases
|
||||||
|
- Manages folder creation, listing, and hierarchy
|
||||||
|
- Coordinates between repositories for folder operations
|
||||||
|
- Maintains folder structure integrity
|
||||||
|
- Handles error translation for folder operations
|
||||||
|
|
||||||
|
**Auth Application Service (`src/application/services/auth_application_service.rs`)**
|
||||||
|
- Manages user authentication flows
|
||||||
|
- Implements login, logout, and session management
|
||||||
|
- Handles token generation and validation
|
||||||
|
- Coordinates with user repository for verification
|
||||||
|
- Manages password reset and account recovery
|
||||||
|
|
||||||
|
**File Management Service (`src/application/services/file_management_service.rs`)**
|
||||||
|
- Provides higher-level file operations
|
||||||
|
- Manages file uploads, versions, and metadata
|
||||||
|
- Handles file operations across repositories
|
||||||
|
- Coordinates transactional file operations
|
||||||
|
- Provides advanced file searching and filtering
|
||||||
|
|
||||||
|
**File Retrieval Service (`src/application/services/file_retrieval_service.rs`)**
|
||||||
|
- Specialized service for file content retrieval
|
||||||
|
- Optimizes file reading operations
|
||||||
|
- Provides streaming and download functionality
|
||||||
|
- Implements read-specific error handling
|
||||||
|
- Supports different retrieval patterns (whole file, ranges)
|
||||||
|
|
||||||
|
**File Upload Service (`src/application/services/file_upload_service.rs`)**
|
||||||
|
- Specialized service for handling file uploads
|
||||||
|
- Manages chunked and multipart uploads
|
||||||
|
- Provides validation during upload
|
||||||
|
- Handles large file uploads efficiently
|
||||||
|
- Supports upload resumption and integrity verification
|
||||||
|
|
||||||
|
**Search Service (`src/application/services/search_service.rs`)**
|
||||||
|
- Implements file and folder search functionality
|
||||||
|
- Provides text-based content searching
|
||||||
|
- Handles metadata-based filtering
|
||||||
|
- Supports sorting and pagination of results
|
||||||
|
- Optimizes search operations for performance
|
||||||
|
|
||||||
|
**Share Service (`src/application/services/share_service.rs`)**
|
||||||
|
- Implements file and folder sharing functionality
|
||||||
|
- Creates and manages share links
|
||||||
|
- Handles permission checking for shared resources
|
||||||
|
- Manages password protection for shares
|
||||||
|
- Processes access requests for shared content
|
||||||
|
|
||||||
|
**Trash Service (`src/application/services/trash_service.rs`)**
|
||||||
|
- Implements trash can functionality
|
||||||
|
- Manages moving items to trash and restoration
|
||||||
|
- Handles automatic cleanup of expired trash
|
||||||
|
- Coordinates with repositories for trash operations
|
||||||
|
- Maintains metadata for trashed items
|
||||||
|
|
||||||
|
**Recent Service (`src/application/services/recent_service.rs`)**
|
||||||
|
- Tracks recently accessed files
|
||||||
|
- Manages user-specific recent file lists
|
||||||
|
- Handles expiration of old entries
|
||||||
|
- Provides sorting and filtering of recent files
|
||||||
|
- Coordinates with file repository for metadata
|
||||||
|
|
||||||
|
**Favorites Service (`src/application/services/favorites_service.rs`)**
|
||||||
|
- Manages user favorite files and folders
|
||||||
|
- Provides adding and removing favorites
|
||||||
|
- Handles listing and sorting of favorites
|
||||||
|
- Coordinates with repositories for data consistency
|
||||||
|
- Maintains user-specific favorite lists
|
||||||
|
|
||||||
|
**I18n Application Service (`src/application/services/i18n_application_service.rs`)**
|
||||||
|
- Handles internationalization and localization
|
||||||
|
- Provides translation lookups for UI components
|
||||||
|
- Manages locale detection and setting
|
||||||
|
- Coordinates with i18n domain service
|
||||||
|
- Supports dynamic language switching
|
||||||
|
|
||||||
|
**Storage Mediator (`src/application/services/storage_mediator.rs`)**
|
||||||
|
- Coordinates between different storage repositories
|
||||||
|
- Manages transaction coordination
|
||||||
|
- Handles path resolution between storage layers
|
||||||
|
- Provides unified view of storage subsystems
|
||||||
|
- Optimizes operations across storage types
|
||||||
|
|
||||||
|
**Batch Operations (`src/application/services/batch_operations.rs`)**
|
||||||
|
- Implements batch processing for file operations
|
||||||
|
- Handles atomic multi-file operations
|
||||||
|
- Provides transaction support for batch operations
|
||||||
|
- Manages failure handling and partial success
|
||||||
|
- Optimizes performance for bulk operations
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
|
||||||
|
**Inbound Ports (`src/application/ports/inbound.rs`)**
|
||||||
|
- Defines interfaces for external systems to use
|
||||||
|
- Contains use case interfaces for application services
|
||||||
|
- Specifies contracts for UI and API interactions
|
||||||
|
- Provides clear boundaries for application functionality
|
||||||
|
- Forms the primary API for interfaces layer
|
||||||
|
|
||||||
|
**Outbound Ports (`src/application/ports/outbound.rs`)**
|
||||||
|
- Defines interfaces used by application services
|
||||||
|
- Specifies contracts that infrastructure must implement
|
||||||
|
- Allows swapping infrastructure implementations
|
||||||
|
- Maintains dependency inversion principle
|
||||||
|
- Protects application layer from external dependencies
|
||||||
|
|
||||||
|
**Auth Ports (`src/application/ports/auth_ports.rs`)**
|
||||||
|
- Defines interfaces for authentication operations
|
||||||
|
- Specifies contracts for login, validation, and sessions
|
||||||
|
- Handles token generation and verification
|
||||||
|
- Provides user identity management
|
||||||
|
- Supports different authentication methods
|
||||||
|
|
||||||
|
**Storage Ports (`src/application/ports/storage_ports.rs`)**
|
||||||
|
- Defines interfaces for storage operations
|
||||||
|
- Specifies contracts for accessing persistent storage
|
||||||
|
- Handles file system and database interactions
|
||||||
|
- Provides transaction support for storage operations
|
||||||
|
- Supports different storage backends
|
||||||
|
|
||||||
|
**File Ports (`src/application/ports/file_ports.rs`)**
|
||||||
|
- Defines interfaces for file operations
|
||||||
|
- Contains file upload and retrieval use cases
|
||||||
|
- Specifies contracts for file management
|
||||||
|
- Handles file-specific error conditions
|
||||||
|
- Supports various file operation patterns
|
||||||
|
|
||||||
|
**Favorites Ports (`src/application/ports/favorites_ports.rs`)**
|
||||||
|
- Defines interfaces for favorites functionality
|
||||||
|
- Specifies contracts for favorite management
|
||||||
|
- Handles favorite-specific operations
|
||||||
|
- Provides user-specific favorite management
|
||||||
|
- Supports different favorite organization structures
|
||||||
|
|
||||||
|
**Recent Ports (`src/application/ports/recent_ports.rs`)**
|
||||||
|
- Defines interfaces for recent files functionality
|
||||||
|
- Specifies contracts for recent file tracking
|
||||||
|
- Handles history and access patterns
|
||||||
|
- Provides user-specific recent file handling
|
||||||
|
- Supports different recency algorithms
|
||||||
|
|
||||||
|
**Share Ports (`src/application/ports/share_ports.rs`)**
|
||||||
|
- Defines interfaces for sharing functionality
|
||||||
|
- Specifies contracts for share creation and access
|
||||||
|
- Handles permission verification for shares
|
||||||
|
- Provides link generation and management
|
||||||
|
- Supports different sharing models
|
||||||
|
|
||||||
|
**Trash Ports (`src/application/ports/trash_ports.rs`)**
|
||||||
|
- Defines interfaces for trash functionality
|
||||||
|
- Specifies contracts for trash operations
|
||||||
|
- Handles trash-specific workflows
|
||||||
|
- Provides retention and cleanup interfaces
|
||||||
|
- Supports different trash implementation strategies
|
||||||
|
|
||||||
|
### DTOs
|
||||||
|
|
||||||
|
**File DTO (`src/application/dtos/file_dto.rs`)**
|
||||||
|
- Data transfer object for file entities
|
||||||
|
- Provides serialization and API representation
|
||||||
|
- Translates between domain model and external interfaces
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains file metadata for API responses
|
||||||
|
|
||||||
|
**Folder DTO (`src/application/dtos/folder_dto.rs`)**
|
||||||
|
- Data transfer object for folder entities
|
||||||
|
- Provides folder data for API responses
|
||||||
|
- Handles serialization and API representation
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains folder structure information
|
||||||
|
|
||||||
|
**User DTO (`src/application/dtos/user_dto.rs`)**
|
||||||
|
- Data transfer object for user information
|
||||||
|
- Provides user data for API responses
|
||||||
|
- Handles serialization with sensitive data protection
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains user profile information
|
||||||
|
|
||||||
|
**Share DTO (`src/application/dtos/share_dto.rs`)**
|
||||||
|
- Data transfer object for share information
|
||||||
|
- Provides share data for API responses
|
||||||
|
- Handles serialization of sharing details
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains share link and permission data
|
||||||
|
|
||||||
|
**Trash DTO (`src/application/dtos/trash_dto.rs`)**
|
||||||
|
- Data transfer object for trashed items
|
||||||
|
- Provides trash information for API responses
|
||||||
|
- Handles serialization of trash metadata
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains restoration information
|
||||||
|
|
||||||
|
**Pagination DTO (`src/application/dtos/pagination.rs`)**
|
||||||
|
- Handles pagination for list responses
|
||||||
|
- Provides page size and number information
|
||||||
|
- Supports offset and cursor-based pagination
|
||||||
|
- Includes metadata for total items and pages
|
||||||
|
- Facilitates consistent pagination across APIs
|
||||||
|
|
||||||
|
**Favorites DTO (`src/application/dtos/favorites_dto.rs`)**
|
||||||
|
- Data transfer object for favorites
|
||||||
|
- Provides favorites data for API responses
|
||||||
|
- Handles serialization of favorite items
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains favorite metadata and organization
|
||||||
|
|
||||||
|
**Recent DTO (`src/application/dtos/recent_dto.rs`)**
|
||||||
|
- Data transfer object for recent files
|
||||||
|
- Provides recent items data for API responses
|
||||||
|
- Handles serialization of access history
|
||||||
|
- Includes conversions to/from domain entities
|
||||||
|
- Contains timing and access metadata
|
||||||
|
|
||||||
|
**Search DTO (`src/application/dtos/search_dto.rs`)**
|
||||||
|
- Data transfer object for search results
|
||||||
|
- Provides search data for API responses
|
||||||
|
- Handles serialization of search results
|
||||||
|
- Includes query and result metadata
|
||||||
|
- Contains relevance and ranking information
|
||||||
|
|
||||||
|
**I18n DTO (`src/application/dtos/i18n_dto.rs`)**
|
||||||
|
- Data transfer object for internationalization
|
||||||
|
- Provides language and translation data
|
||||||
|
- Handles serialization of language resources
|
||||||
|
- Includes locale and preference information
|
||||||
|
- Contains translation bundle structures
|
||||||
|
|
||||||
|
### Adapters
|
||||||
|
|
||||||
|
**WebDAV Adapter (`src/application/adapters/webdav_adapter.rs`)**
|
||||||
|
- Adapts between OxiCloud domain models and WebDAV protocol
|
||||||
|
- Handles XML parsing and generation for WebDAV operations
|
||||||
|
- Implements property handling for WebDAV (PROPFIND, PROPPATCH)
|
||||||
|
- Provides WebDAV-specific error handling
|
||||||
|
- Translates between file operations and WebDAV methods
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
**Storage Transaction (`src/application/transactions/storage_transaction.rs`)**
|
||||||
|
- Manages transactional operations for storage
|
||||||
|
- Implements transaction boundaries and commits
|
||||||
|
- Provides rollback capabilities on failure
|
||||||
|
- Ensures consistency across multiple operations
|
||||||
|
- Handles transaction isolation levels
|
||||||
|
|
||||||
|
## Infrastructure Layer
|
||||||
|
|
||||||
|
This layer provides concrete implementations of repository interfaces and technical services.
|
||||||
|
|
||||||
|
### Repositories (Implementations)
|
||||||
|
|
||||||
|
**File FS Repository (`src/infrastructure/repositories/file_fs_repository.rs`)**
|
||||||
|
- Implements FileRepository interface for filesystem storage
|
||||||
|
- Manages physical file operations on disk
|
||||||
|
- Handles file content reading and writing
|
||||||
|
- Implements optimized large file handling
|
||||||
|
- Provides metadata caching for performance
|
||||||
|
|
||||||
|
**File FS Read Repository (`src/infrastructure/repositories/file_fs_read_repository.rs`)**
|
||||||
|
- Specialized repository for read-only file operations
|
||||||
|
- Optimized for high-performance file retrieval
|
||||||
|
- Implements caching for frequently accessed files
|
||||||
|
- Supports streaming of large files
|
||||||
|
- Handles content type detection and verification
|
||||||
|
|
||||||
|
**File FS Write Repository (`src/infrastructure/repositories/file_fs_write_repository.rs`)**
|
||||||
|
- Specialized repository for file write operations
|
||||||
|
- Handles atomic file writes with transaction support
|
||||||
|
- Implements optimized large file writes
|
||||||
|
- Manages file locking for concurrent writes
|
||||||
|
- Provides integrity verification for written files
|
||||||
|
|
||||||
|
**File FS Repository Trash (`src/infrastructure/repositories/file_fs_repository_trash.rs`)**
|
||||||
|
- Extends file repository with trash functionality
|
||||||
|
- Implements soft delete operations for files
|
||||||
|
- Manages restoration from trash
|
||||||
|
- Handles automatic cleanup of expired trash
|
||||||
|
- Maintains metadata for trashed files
|
||||||
|
|
||||||
|
**Folder FS Repository (`src/infrastructure/repositories/folder_fs_repository.rs`)**
|
||||||
|
- Implements FolderRepository interface for filesystem
|
||||||
|
- Creates and manages directory structures
|
||||||
|
- Handles folder listing and hierarchy traversal
|
||||||
|
- Implements folder permissions and ownership
|
||||||
|
- Provides optimization for deep folder structures
|
||||||
|
|
||||||
|
**Folder FS Repository Trash (`src/infrastructure/repositories/folder_fs_repository_trash.rs`)**
|
||||||
|
- Extends folder repository with trash functionality
|
||||||
|
- Implements soft delete for directories
|
||||||
|
- Handles recursive trash operations for folders
|
||||||
|
- Manages restoration of folder hierarchies
|
||||||
|
- Maintains metadata for trashed folders
|
||||||
|
|
||||||
|
**Share FS Repository (`src/infrastructure/repositories/share_fs_repository.rs`)**
|
||||||
|
- Implements ShareRepository for filesystem-based sharing
|
||||||
|
- Manages share records and permissions
|
||||||
|
- Handles link generation and validation
|
||||||
|
- Provides access control for shared resources
|
||||||
|
- Supports share expiration and revocation
|
||||||
|
|
||||||
|
**Trash FS Repository (`src/infrastructure/repositories/trash_fs_repository.rs`)**
|
||||||
|
- Implements TrashRepository for filesystem
|
||||||
|
- Manages trash directory structure
|
||||||
|
- Handles metadata for trashed items
|
||||||
|
- Implements cleanup policies for expired trash
|
||||||
|
- Supports permanent deletion operations
|
||||||
|
|
||||||
|
**Session PG Repository (`src/infrastructure/repositories/pg/session_pg_repository.rs`)**
|
||||||
|
- Implements session storage using PostgreSQL
|
||||||
|
- Manages user sessions and authentication state
|
||||||
|
- Handles session creation, validation, and expiration
|
||||||
|
- Provides secure token management
|
||||||
|
- Supports multiple concurrent sessions
|
||||||
|
|
||||||
|
**User PG Repository (`src/infrastructure/repositories/pg/user_pg_repository.rs`)**
|
||||||
|
- Implements UserRepository with PostgreSQL
|
||||||
|
- Stores user accounts and profile information
|
||||||
|
- Handles user queries and updates
|
||||||
|
- Manages user roles and permissions
|
||||||
|
- Supports user search and filtering
|
||||||
|
|
||||||
|
**File Metadata Manager (`src/infrastructure/repositories/file_metadata_manager.rs`)**
|
||||||
|
- Manages file metadata independently of content
|
||||||
|
- Handles extended attributes for files
|
||||||
|
- Provides caching for frequently accessed metadata
|
||||||
|
- Optimizes metadata operations
|
||||||
|
- Supports custom metadata fields
|
||||||
|
|
||||||
|
**File Path Resolver (`src/infrastructure/repositories/file_path_resolver.rs`)**
|
||||||
|
- Resolves logical paths to physical storage locations
|
||||||
|
- Handles path normalization and validation
|
||||||
|
- Provides path translation between different systems
|
||||||
|
- Supports virtual paths and redirections
|
||||||
|
- Optimizes path resolution for nested structures
|
||||||
|
|
||||||
|
**Parallel File Processor (`src/infrastructure/repositories/parallel_file_processor.rs`)**
|
||||||
|
- Implements parallel processing for large files
|
||||||
|
- Optimizes file operations with multi-threading
|
||||||
|
- Provides chunked reading and writing
|
||||||
|
- Handles load balancing for file operations
|
||||||
|
- Implements backpressure mechanisms
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
**ID Mapping Service (`src/infrastructure/services/id_mapping_service.rs`)**
|
||||||
|
- Manages mapping between UUIDs and filesystem paths
|
||||||
|
- Provides persistent ID generation and lookup
|
||||||
|
- Handles path changes while maintaining stable IDs
|
||||||
|
- Implements caching for frequently accessed mappings
|
||||||
|
- Ensures consistency between IDs and paths
|
||||||
|
|
||||||
|
**Buffer Pool (`src/infrastructure/services/buffer_pool.rs`)**
|
||||||
|
- Manages memory buffers for file operations
|
||||||
|
- Implements pooling for optimal memory usage
|
||||||
|
- Provides buffer recycling to reduce allocations
|
||||||
|
- Handles buffer sizing for different operations
|
||||||
|
- Implements thread-safe buffer management
|
||||||
|
|
||||||
|
**Cache Manager (`src/infrastructure/services/cache_manager.rs`)**
|
||||||
|
- Provides application-wide caching services
|
||||||
|
- Implements multiple cache levels (memory, disk)
|
||||||
|
- Handles cache invalidation and consistency
|
||||||
|
- Manages cache size limits and eviction
|
||||||
|
- Provides statistics for cache performance
|
||||||
|
|
||||||
|
**Compression Service (`src/infrastructure/services/compression_service.rs`)**
|
||||||
|
- Implements data compression for files and responses
|
||||||
|
- Supports multiple compression algorithms
|
||||||
|
- Provides on-the-fly compression for API responses
|
||||||
|
- Handles selective compression based on file types
|
||||||
|
- Optimizes compression levels for different content
|
||||||
|
|
||||||
|
**File System I18n Service (`src/infrastructure/services/file_system_i18n_service.rs`)**
|
||||||
|
- Implements I18n service using filesystem storage
|
||||||
|
- Loads translations from JSON files
|
||||||
|
- Handles language detection and fallbacks
|
||||||
|
- Provides translation lookups for UI components
|
||||||
|
- Supports dynamic language switching
|
||||||
|
|
||||||
|
**File Metadata Cache (`src/infrastructure/services/file_metadata_cache.rs`)**
|
||||||
|
- Provides caching for file metadata
|
||||||
|
- Optimizes repeated metadata access
|
||||||
|
- Implements cache invalidation strategies
|
||||||
|
- Handles concurrent access to metadata
|
||||||
|
- Supports different cache levels (memory, persistent)
|
||||||
|
|
||||||
|
**ID Mapping Optimizer (`src/infrastructure/services/id_mapping_optimizer.rs`)**
|
||||||
|
- Optimizes ID-to-path mapping operations
|
||||||
|
- Implements batch processing for mapping updates
|
||||||
|
- Provides preloading for frequently accessed mappings
|
||||||
|
- Handles compaction of mapping storage
|
||||||
|
- Optimizes lookup performance for large mappings
|
||||||
|
|
||||||
|
**Zip Service (`src/infrastructure/services/zip_service.rs`)**
|
||||||
|
- Provides ZIP archive creation and extraction
|
||||||
|
- Supports on-the-fly compression for downloads
|
||||||
|
- Handles large directory archiving
|
||||||
|
- Implements streaming ZIP generation
|
||||||
|
- Provides progress tracking for large operations
|
||||||
|
|
||||||
|
**Trash Cleanup Service (`src/infrastructure/services/trash_cleanup_service.rs`)**
|
||||||
|
- Manages automatic cleanup of expired trash items
|
||||||
|
- Implements retention policy enforcement
|
||||||
|
- Provides scheduled cleanup operations
|
||||||
|
- Handles graceful cleanup with resource limits
|
||||||
|
- Supports custom cleanup rules
|
||||||
|
|
||||||
|
## Interfaces Layer
|
||||||
|
|
||||||
|
This layer handles external communication, including API endpoints and web interfaces.
|
||||||
|
|
||||||
|
### API Handlers
|
||||||
|
|
||||||
|
**File Handler (`src/interfaces/api/handlers/file_handler.rs`)**
|
||||||
|
- Handles HTTP requests for file operations
|
||||||
|
- Processes file uploads with multipart support
|
||||||
|
- Provides file downloads with optional compression
|
||||||
|
- Implements CRUD operations for files
|
||||||
|
- Manages error responses and status codes
|
||||||
|
|
||||||
|
**Folder Handler (`src/interfaces/api/handlers/folder_handler.rs`)**
|
||||||
|
- Handles HTTP requests for folder operations
|
||||||
|
- Processes folder creation and listing
|
||||||
|
- Implements CRUD operations for directories
|
||||||
|
- Provides folder hierarchy navigation
|
||||||
|
- Manages error responses for folder operations
|
||||||
|
|
||||||
|
**Auth Handler (`src/interfaces/api/handlers/auth_handler.rs`)**
|
||||||
|
- Handles authentication-related API endpoints
|
||||||
|
- Processes login, logout, and registration
|
||||||
|
- Manages session tokens and refresh
|
||||||
|
- Implements password reset functionality
|
||||||
|
- Provides authentication status information
|
||||||
|
|
||||||
|
**Share Handler (`src/interfaces/api/handlers/share_handler.rs`)**
|
||||||
|
- Handles file and folder sharing endpoints
|
||||||
|
- Processes share creation and management
|
||||||
|
- Provides access to shared resources
|
||||||
|
- Handles permission verification
|
||||||
|
- Manages share links and passwords
|
||||||
|
|
||||||
|
**Trash Handler (`src/interfaces/api/handlers/trash_handler.rs`)**
|
||||||
|
- Handles trash-related API endpoints
|
||||||
|
- Processes moving items to trash
|
||||||
|
- Provides trash listing and filtering
|
||||||
|
- Handles restoration from trash
|
||||||
|
- Manages permanent deletion operations
|
||||||
|
|
||||||
|
**Search Handler (`src/interfaces/api/handlers/search_handler.rs`)**
|
||||||
|
- Handles search-related API endpoints
|
||||||
|
- Processes text search queries
|
||||||
|
- Provides filtering and sorting options
|
||||||
|
- Handles pagination for search results
|
||||||
|
- Manages relevance scoring for results
|
||||||
|
|
||||||
|
**Recent Handler (`src/interfaces/api/handlers/recent_handler.rs`)**
|
||||||
|
- Handles recently accessed files endpoints
|
||||||
|
- Provides listing and filtering of recent files
|
||||||
|
- Manages user-specific recent history
|
||||||
|
- Handles pagination for recent items
|
||||||
|
- Provides sorting options for recent files
|
||||||
|
|
||||||
|
**Favorites Handler (`src/interfaces/api/handlers/favorites_handler.rs`)**
|
||||||
|
- Handles user favorites endpoints
|
||||||
|
- Processes adding and removing favorites
|
||||||
|
- Provides listing and filtering of favorites
|
||||||
|
- Manages user-specific favorite collections
|
||||||
|
- Handles sorting and organization of favorites
|
||||||
|
|
||||||
|
**I18n Handler (`src/interfaces/api/handlers/i18n_handler.rs`)**
|
||||||
|
- Handles internationalization endpoints
|
||||||
|
- Provides language selection and detection
|
||||||
|
- Serves translation resources
|
||||||
|
- Manages locale settings
|
||||||
|
- Handles language preference persistence
|
||||||
|
|
||||||
|
**Batch Handler (`src/interfaces/api/handlers/batch_handler.rs`)**
|
||||||
|
- Handles batch operation endpoints
|
||||||
|
- Processes multiple operations in a single request
|
||||||
|
- Provides transaction support for batches
|
||||||
|
- Handles partial success scenarios
|
||||||
|
- Manages comprehensive error reporting
|
||||||
|
|
||||||
|
**WebDAV Handler (`src/interfaces/api/handlers/webdav_handler.rs`)**
|
||||||
|
- Implements WebDAV protocol (RFC 4918) endpoints
|
||||||
|
- Handles WebDAV methods (PROPFIND, PROPPATCH, etc.)
|
||||||
|
- Provides file system access via HTTP
|
||||||
|
- Manages WebDAV properties and locks
|
||||||
|
- Supports third-party WebDAV clients
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
|
||||||
|
**Routes (`src/interfaces/api/routes.rs`)**
|
||||||
|
- Defines API routes and URL structure
|
||||||
|
- Maps endpoints to appropriate handlers
|
||||||
|
- Configures middleware for routes
|
||||||
|
- Handles versioning for API endpoints
|
||||||
|
- Provides documentation integration
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
**Auth Middleware (`src/interfaces/middleware/auth.rs`)**
|
||||||
|
- Handles authentication for API requests
|
||||||
|
- Verifies tokens and sessions
|
||||||
|
- Provides user context for handlers
|
||||||
|
- Manages authentication errors
|
||||||
|
- Supports different authentication methods
|
||||||
|
|
||||||
|
**Cache Middleware (`src/interfaces/middleware/cache.rs`)**
|
||||||
|
- Implements response caching
|
||||||
|
- Handles cache headers and validation
|
||||||
|
- Provides conditional request processing
|
||||||
|
- Manages cache invalidation
|
||||||
|
- Optimizes for different content types
|
||||||
|
|
||||||
|
**Redirect Middleware (`src/interfaces/middleware/redirect.rs`)**
|
||||||
|
- Handles HTTP redirects
|
||||||
|
- Manages URL normalization
|
||||||
|
- Provides permanent and temporary redirects
|
||||||
|
- Handles protocol upgrades (HTTP to HTTPS)
|
||||||
|
- Supports path-based redirections
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
|
||||||
|
**Web Module (`src/interfaces/web/mod.rs`)**
|
||||||
|
- Coordinates web interface components
|
||||||
|
- Manages static file serving
|
||||||
|
- Provides web application integration
|
||||||
|
- Handles web-specific middleware
|
||||||
|
- Supports single-page application routing
|
||||||
|
|
||||||
|
## Common Layer
|
||||||
|
|
||||||
|
This layer provides shared utilities and configurations used across the application.
|
||||||
|
|
||||||
|
**Config (`src/common/config.rs`)**
|
||||||
|
- Manages application configuration
|
||||||
|
- Loads settings from environment and files
|
||||||
|
- Provides typed configuration access
|
||||||
|
- Handles configuration validation
|
||||||
|
- Supports different environments (dev, prod)
|
||||||
|
|
||||||
|
**Errors (`src/common/errors.rs`)**
|
||||||
|
- Defines error types and handling
|
||||||
|
- Provides consistent error formatting
|
||||||
|
- Implements error context and wrapping
|
||||||
|
- Handles error translation between layers
|
||||||
|
- Supports error categorization and logging
|
||||||
|
|
||||||
|
**DI (`src/common/di.rs`)**
|
||||||
|
- Implements dependency injection
|
||||||
|
- Manages service lifecycles
|
||||||
|
- Provides application state container
|
||||||
|
- Handles service resolution and registration
|
||||||
|
- Supports scoped service instances
|
||||||
|
|
||||||
|
**DB (`src/common/db.rs`)**
|
||||||
|
- Manages database connections
|
||||||
|
- Provides connection pooling
|
||||||
|
- Handles database migrations
|
||||||
|
- Implements query helpers
|
||||||
|
- Supports transaction management
|
||||||
|
|
||||||
|
**Cache (`src/common/cache.rs`)**
|
||||||
|
- Provides generic caching facilities
|
||||||
|
- Implements different cache strategies
|
||||||
|
- Handles cache key generation
|
||||||
|
- Manages cache invalidation
|
||||||
|
- Supports distributed caching
|
||||||
|
|
||||||
|
**Auth Factory (`src/common/auth_factory.rs`)**
|
||||||
|
- Creates authentication components
|
||||||
|
- Configures auth providers based on settings
|
||||||
|
- Provides factory methods for auth services
|
||||||
|
- Handles auth strategy selection
|
||||||
|
- Supports multiple authentication methods
|
||||||
@@ -0,0 +1,684 @@
|
|||||||
|
/**
|
||||||
|
* WebDAV Adapter Module
|
||||||
|
*
|
||||||
|
* This module provides adapters for converting between OxiCloud's domain models
|
||||||
|
* and WebDAV protocol representations. It handles XML parsing and generation
|
||||||
|
* for all WebDAV operations (PROPFIND, PROPPATCH, etc.) according to RFC 4918.
|
||||||
|
*
|
||||||
|
* The adapter serves as a translation layer between the WebDAV protocol's XML-based
|
||||||
|
* communication format and OxiCloud's internal data models, ensuring proper
|
||||||
|
* serialization and deserialization of WebDAV requests and responses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use quick_xml::{Reader, Writer, events::{Event, BytesStart, BytesEnd, BytesText}};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
|
use crate::application::dtos::folder_dto::FolderDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error types specific to WebDAV operations.
|
||||||
|
* These errors encapsulate the various failure modes during WebDAV processing.
|
||||||
|
*/
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum WebDavError {
|
||||||
|
/// Error during XML parsing or generation
|
||||||
|
#[error("XML error: {0}")]
|
||||||
|
XmlError(String),
|
||||||
|
|
||||||
|
/// Error related to property handling
|
||||||
|
#[error("Property error: {0}")]
|
||||||
|
PropertyError(String),
|
||||||
|
|
||||||
|
/// Error in the request format or content
|
||||||
|
#[error("Invalid request: {0}")]
|
||||||
|
InvalidRequest(String),
|
||||||
|
|
||||||
|
/// I/O error during reading or writing
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// Other WebDAV related errors
|
||||||
|
#[error("WebDAV error: {0}")]
|
||||||
|
WebDavError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for WebDAV operation results
|
||||||
|
pub type Result<T> = std::result::Result<T, WebDavError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property namespace and name, used to identify WebDAV properties.
|
||||||
|
* WebDAV properties are identified by a combination of namespace and name.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PropertyName {
|
||||||
|
/// XML namespace for the property (e.g., "DAV:")
|
||||||
|
pub namespace: String,
|
||||||
|
/// Local name of the property (e.g., "displayname")
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a WebDAV property with its name and value.
|
||||||
|
* WebDAV properties contain metadata about resources.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Property {
|
||||||
|
/// The qualified name of the property
|
||||||
|
pub name: PropertyName,
|
||||||
|
/// The property value, if any
|
||||||
|
pub value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a PROPFIND request as defined in RFC 4918.
|
||||||
|
* PROPFIND requests can ask for all properties, named properties,
|
||||||
|
* or property names only.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PropFindRequest {
|
||||||
|
/// Request all properties
|
||||||
|
AllProps,
|
||||||
|
/// Request specific properties by name
|
||||||
|
PropNames(Vec<PropertyName>),
|
||||||
|
/// Request only property names without values
|
||||||
|
PropNameOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for WebDAV operations, providing XML serialization and deserialization.
|
||||||
|
* This struct contains methods for parsing WebDAV requests and generating
|
||||||
|
* appropriate responses according to the WebDAV specification.
|
||||||
|
*/
|
||||||
|
pub struct WebDavAdapter;
|
||||||
|
|
||||||
|
impl WebDavAdapter {
|
||||||
|
// XML namespaces used in WebDAV
|
||||||
|
const DAV_NS: &'static str = "DAV:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a PROPFIND request body into a structured representation.
|
||||||
|
*
|
||||||
|
* Processes the XML body of a PROPFIND request to determine which
|
||||||
|
* properties are being requested (allprop, propname, or specific props).
|
||||||
|
*
|
||||||
|
* @param reader Source providing XML content to parse
|
||||||
|
* @return Result containing the parsed PropFindRequest or an error
|
||||||
|
*/
|
||||||
|
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest> {
|
||||||
|
let mut xml_reader = Reader::from_reader(reader);
|
||||||
|
xml_reader.trim_text(true);
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut inside_propfind = false;
|
||||||
|
let mut prop_names = Vec::new();
|
||||||
|
let mut result = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match xml_reader.read_event(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) => {
|
||||||
|
let name = e.name();
|
||||||
|
let name_str = std::str::from_utf8(name).map_err(|_| {
|
||||||
|
WebDavError::XmlError("Invalid XML element name".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if name_str == "propfind" {
|
||||||
|
inside_propfind = true;
|
||||||
|
} else if inside_propfind {
|
||||||
|
match name_str {
|
||||||
|
"allprop" => {
|
||||||
|
result = Some(PropFindRequest::AllProps);
|
||||||
|
},
|
||||||
|
"propname" => {
|
||||||
|
result = Some(PropFindRequest::PropNameOnly);
|
||||||
|
},
|
||||||
|
"prop" => {
|
||||||
|
// Will collect property names in subsequent iterations
|
||||||
|
},
|
||||||
|
_ if inside_propfind => {
|
||||||
|
// Handle property names within prop element
|
||||||
|
let namespace = Self::get_namespace_from_element(e)?;
|
||||||
|
prop_names.push(PropertyName {
|
||||||
|
namespace: namespace.unwrap_or_else(|| Self::DAV_NS.to_string()),
|
||||||
|
name: name_str.to_string(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Event::Empty(ref e)) => {
|
||||||
|
// Handle self-closing tags
|
||||||
|
let name = e.name();
|
||||||
|
let name_str = std::str::from_utf8(name).map_err(|_| {
|
||||||
|
WebDavError::XmlError("Invalid XML element name".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if inside_propfind && name_str != "prop" {
|
||||||
|
let namespace = Self::get_namespace_from_element(e)?;
|
||||||
|
prop_names.push(PropertyName {
|
||||||
|
namespace: namespace.unwrap_or_else(|| Self::DAV_NS.to_string()),
|
||||||
|
name: name_str.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
let name = e.name();
|
||||||
|
let name_str = std::str::from_utf8(name).map_err(|_| {
|
||||||
|
WebDavError::XmlError("Invalid XML element name".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if name_str == "propfind" {
|
||||||
|
inside_propfind = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(WebDavError::XmlError(format!("Error parsing XML: {}", e))),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !prop_names.is_empty() {
|
||||||
|
return Ok(PropFindRequest::PropNames(prop_names));
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ok_or_else(|| WebDavError::InvalidRequest("Invalid or missing propfind request".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the namespace from an XML element.
|
||||||
|
*
|
||||||
|
* @param element The XML element to extract namespace from
|
||||||
|
* @return Result containing the optional namespace or an error
|
||||||
|
*/
|
||||||
|
fn get_namespace_from_element(element: &BytesStart) -> Result<Option<String>> {
|
||||||
|
// Extract namespace from qualified name (e.g., "d:prop" -> "d")
|
||||||
|
let name = std::str::from_utf8(element.name()).map_err(|_| {
|
||||||
|
WebDavError::XmlError("Invalid XML element name".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(pos) = name.find(':') {
|
||||||
|
let prefix = &name[..pos];
|
||||||
|
|
||||||
|
// Find namespace declaration for this prefix
|
||||||
|
for attr in element.attributes() {
|
||||||
|
let attr = attr.map_err(|e| WebDavError::XmlError(format!("Invalid attribute: {}", e)))?;
|
||||||
|
let key = std::str::from_utf8(attr.key).map_err(|_| {
|
||||||
|
WebDavError::XmlError("Invalid attribute name".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if key == format!("xmlns:{}", prefix) {
|
||||||
|
let value = std::str::from_utf8(&attr.value).map_err(|_| {
|
||||||
|
WebDavError::XmlError("Invalid attribute value".to_string())
|
||||||
|
})?;
|
||||||
|
return Ok(Some(value.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a PROPFIND response for a file.
|
||||||
|
*
|
||||||
|
* Creates an XML response containing the requested properties
|
||||||
|
* for a single file resource.
|
||||||
|
*
|
||||||
|
* @param writer The output destination for the generated XML
|
||||||
|
* @param file The file DTO containing the resource data
|
||||||
|
* @param request The original PROPFIND request specifying which properties to include
|
||||||
|
* @param depth The requested depth (0, 1, or infinity)
|
||||||
|
* @param href The URL of the resource
|
||||||
|
* @return Result indicating success or containing an error
|
||||||
|
*/
|
||||||
|
pub fn generate_propfind_response_for_file<W: Write>(
|
||||||
|
writer: W,
|
||||||
|
file: &FileDto,
|
||||||
|
request: &PropFindRequest,
|
||||||
|
depth: &str,
|
||||||
|
href: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut xml_writer = Writer::new(writer);
|
||||||
|
|
||||||
|
// Start multistatus response
|
||||||
|
let mut multistatus = BytesStart::owned(b"d:multistatus".to_vec(), "d:multistatus".len());
|
||||||
|
multistatus.push_attribute(("xmlns:d", "DAV:"));
|
||||||
|
xml_writer.write_event(Event::Start(multistatus)).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write multistatus start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Generate response for the file
|
||||||
|
Self::write_resource_properties(
|
||||||
|
&mut xml_writer,
|
||||||
|
href,
|
||||||
|
file.updated_at,
|
||||||
|
file.size as u64,
|
||||||
|
false, // is_collection
|
||||||
|
request,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// End multistatus
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:multistatus"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write multistatus end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a PROPFIND response for a directory and its contents.
|
||||||
|
*
|
||||||
|
* Creates an XML response containing the requested properties for a
|
||||||
|
* directory and its children (files and subdirectories) based on the depth.
|
||||||
|
*
|
||||||
|
* @param writer The output destination for the generated XML
|
||||||
|
* @param folder The folder DTO (or None for root)
|
||||||
|
* @param files List of file DTOs contained in the folder
|
||||||
|
* @param subfolders List of subfolder DTOs contained in the folder
|
||||||
|
* @param request The original PROPFIND request specifying which properties to include
|
||||||
|
* @param depth The requested depth (0, 1, or infinity)
|
||||||
|
* @param base_href The base URL of the resource
|
||||||
|
* @return Result indicating success or containing an error
|
||||||
|
*/
|
||||||
|
pub fn generate_propfind_response<W: Write>(
|
||||||
|
writer: W,
|
||||||
|
folder: Option<&FolderDto>,
|
||||||
|
files: &[FileDto],
|
||||||
|
subfolders: &[FolderDto],
|
||||||
|
request: &PropFindRequest,
|
||||||
|
depth: &str,
|
||||||
|
base_href: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut xml_writer = Writer::new(writer);
|
||||||
|
|
||||||
|
// Start multistatus response
|
||||||
|
let mut multistatus = BytesStart::owned(b"d:multistatus".to_vec(), "d:multistatus".len());
|
||||||
|
multistatus.push_attribute(("xmlns:d", "DAV:"));
|
||||||
|
xml_writer.write_event(Event::Start(multistatus)).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write multistatus start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Add folder properties
|
||||||
|
if let Some(folder) = folder {
|
||||||
|
Self::write_resource_properties(
|
||||||
|
&mut xml_writer,
|
||||||
|
base_href,
|
||||||
|
folder.updated_at,
|
||||||
|
0, // Size for directories is typically 0
|
||||||
|
true, // is_collection
|
||||||
|
request,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If depth > 0, include children
|
||||||
|
if depth != "0" {
|
||||||
|
// Add files
|
||||||
|
for file in files {
|
||||||
|
let file_href = format!("{}{}", base_href, file.name);
|
||||||
|
Self::write_resource_properties(
|
||||||
|
&mut xml_writer,
|
||||||
|
&file_href,
|
||||||
|
file.updated_at,
|
||||||
|
file.size as u64,
|
||||||
|
false, // is_collection
|
||||||
|
request,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subfolders
|
||||||
|
for subfolder in subfolders {
|
||||||
|
let folder_href = format!("{}{}/", base_href, subfolder.name);
|
||||||
|
Self::write_resource_properties(
|
||||||
|
&mut xml_writer,
|
||||||
|
&folder_href,
|
||||||
|
subfolder.updated_at,
|
||||||
|
0, // Size for directories is typically 0
|
||||||
|
true, // is_collection
|
||||||
|
request,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End multistatus
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:multistatus"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write multistatus end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the properties for a single resource in a PROPFIND response.
|
||||||
|
*
|
||||||
|
* Helper method to generate XML for a single resource's properties,
|
||||||
|
* used by both file and directory PROPFIND responses.
|
||||||
|
*
|
||||||
|
* @param writer The XML writer to output to
|
||||||
|
* @param href The URL of the resource
|
||||||
|
* @param last_modified Last modification timestamp of the resource
|
||||||
|
* @param size Size of the resource in bytes
|
||||||
|
* @param is_collection Whether the resource is a collection (directory)
|
||||||
|
* @param request The original PROPFIND request specifying which properties to include
|
||||||
|
* @return Result indicating success or containing an error
|
||||||
|
*/
|
||||||
|
fn write_resource_properties<W: Write>(
|
||||||
|
xml_writer: &mut Writer<W>,
|
||||||
|
href: &str,
|
||||||
|
last_modified: DateTime<Utc>,
|
||||||
|
size: u64,
|
||||||
|
is_collection: bool,
|
||||||
|
request: &PropFindRequest,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Start response element
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:response", "d:response".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write response start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Write href
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:href", "d:href".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write href start: {}", e))
|
||||||
|
})?;
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str(href))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write href text: {}", e))
|
||||||
|
})?;
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:href"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write href end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Start propstat
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:propstat", "d:propstat".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write propstat start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Start prop
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:prop", "d:prop".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write prop start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Determine which properties to include based on the request
|
||||||
|
match request {
|
||||||
|
PropFindRequest::AllProps => {
|
||||||
|
// Include standard properties
|
||||||
|
Self::write_standard_properties(xml_writer, last_modified, size, is_collection)?;
|
||||||
|
},
|
||||||
|
PropFindRequest::PropNames(props) => {
|
||||||
|
// Include only the requested properties
|
||||||
|
for prop_name in props {
|
||||||
|
if prop_name.namespace == Self::DAV_NS {
|
||||||
|
match prop_name.name.as_str() {
|
||||||
|
"resourcetype" => Self::write_resourcetype(xml_writer, is_collection)?,
|
||||||
|
"getcontentlength" => {
|
||||||
|
if !is_collection {
|
||||||
|
Self::write_getcontentlength(xml_writer, size)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"getlastmodified" => Self::write_getlastmodified(xml_writer, last_modified)?,
|
||||||
|
"creationdate" => Self::write_creationdate(xml_writer, last_modified)?,
|
||||||
|
"displayname" => {
|
||||||
|
// Extract displayname from href
|
||||||
|
let display_name = href.split('/').last().unwrap_or(href);
|
||||||
|
Self::write_displayname(xml_writer, display_name)?;
|
||||||
|
},
|
||||||
|
"getcontenttype" => {
|
||||||
|
if !is_collection {
|
||||||
|
// For files, try to determine MIME type
|
||||||
|
let content_type = if is_collection {
|
||||||
|
"httpd/unix-directory"
|
||||||
|
} else {
|
||||||
|
mime_guess::from_path(href)
|
||||||
|
.first_or_octet_stream()
|
||||||
|
.as_ref()
|
||||||
|
};
|
||||||
|
Self::write_getcontenttype(xml_writer, content_type)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Add other standard properties as needed
|
||||||
|
_ => {
|
||||||
|
// Unknown property - return empty element
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(
|
||||||
|
format!("d:{}", prop_name.name).as_bytes(),
|
||||||
|
prop_name.name.len() + 2,
|
||||||
|
))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write property: {}", e))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PropFindRequest::PropNameOnly => {
|
||||||
|
// Just include empty property elements
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:resourcetype", "d:resourcetype".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write resourcetype: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !is_collection {
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:getcontentlength", "d:getcontentlength".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontentlength: {}", e))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:getlastmodified", "d:getlastmodified".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getlastmodified: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:creationdate", "d:creationdate".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write creationdate: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:displayname", "d:displayname".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write displayname: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !is_collection {
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:getcontenttype", "d:getcontenttype".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontenttype: {}", e))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End prop
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:prop"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write prop end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Write status
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:status", "d:status".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write status start: {}", e))
|
||||||
|
})?;
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str("HTTP/1.1 200 OK"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write status text: {}", e))
|
||||||
|
})?;
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:status"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write status end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// End propstat
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:propstat"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write propstat end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// End response
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:response"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write response end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes all standard WebDAV properties for a resource.
|
||||||
|
*
|
||||||
|
* Helper method to write the core set of WebDAV properties that
|
||||||
|
* most clients expect.
|
||||||
|
*
|
||||||
|
* @param writer The XML writer to output to
|
||||||
|
* @param last_modified Last modification timestamp of the resource
|
||||||
|
* @param size Size of the resource in bytes
|
||||||
|
* @param is_collection Whether the resource is a collection (directory)
|
||||||
|
* @return Result indicating success or containing an error
|
||||||
|
*/
|
||||||
|
fn write_standard_properties<W: Write>(
|
||||||
|
xml_writer: &mut Writer<W>,
|
||||||
|
last_modified: DateTime<Utc>,
|
||||||
|
size: u64,
|
||||||
|
is_collection: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Write resourcetype (collection or not)
|
||||||
|
Self::write_resourcetype(xml_writer, is_collection)?;
|
||||||
|
|
||||||
|
// Write content length for files
|
||||||
|
if !is_collection {
|
||||||
|
Self::write_getcontentlength(xml_writer, size)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write last modified date
|
||||||
|
Self::write_getlastmodified(xml_writer, last_modified)?;
|
||||||
|
|
||||||
|
// Write creation date (using last modified as fallback)
|
||||||
|
Self::write_creationdate(xml_writer, last_modified)?;
|
||||||
|
|
||||||
|
// Add other standard properties as needed
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for writing specific properties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the resourcetype property.
|
||||||
|
* Indicates whether the resource is a collection (directory) or regular resource.
|
||||||
|
*/
|
||||||
|
fn write_resourcetype<W: Write>(xml_writer: &mut Writer<W>, is_collection: bool) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:resourcetype", "d:resourcetype".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write resourcetype start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if is_collection {
|
||||||
|
xml_writer.write_event(Event::Empty(BytesStart::borrowed(b"d:collection", "d:collection".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write collection: {}", e))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:resourcetype"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write resourcetype end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the getcontentlength property.
|
||||||
|
* Contains the size of the resource in bytes.
|
||||||
|
*/
|
||||||
|
fn write_getcontentlength<W: Write>(xml_writer: &mut Writer<W>, size: u64) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:getcontentlength", "d:getcontentlength".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontentlength start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str(&size.to_string()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontentlength text: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:getcontentlength"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontentlength end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the getlastmodified property.
|
||||||
|
* Contains the last modification date in RFC 822 format.
|
||||||
|
*/
|
||||||
|
fn write_getlastmodified<W: Write>(xml_writer: &mut Writer<W>, last_modified: DateTime<Utc>) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:getlastmodified", "d:getlastmodified".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getlastmodified start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Format as RFC 822 date as required by WebDAV
|
||||||
|
let formatted_date = last_modified.to_rfc2822();
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str(&formatted_date))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getlastmodified text: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:getlastmodified"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getlastmodified end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the creationdate property.
|
||||||
|
* Contains the creation date in ISO 8601 format.
|
||||||
|
*/
|
||||||
|
fn write_creationdate<W: Write>(xml_writer: &mut Writer<W>, creation_date: DateTime<Utc>) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:creationdate", "d:creationdate".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write creationdate start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Format as ISO 8601 date as required by WebDAV
|
||||||
|
let formatted_date = creation_date.to_rfc3339();
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str(&formatted_date))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write creationdate text: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:creationdate"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write creationdate end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the displayname property.
|
||||||
|
* Contains the human-readable name of the resource.
|
||||||
|
*/
|
||||||
|
fn write_displayname<W: Write>(xml_writer: &mut Writer<W>, display_name: &str) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:displayname", "d:displayname".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write displayname start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str(display_name))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write displayname text: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:displayname"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write displayname end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the getcontenttype property.
|
||||||
|
* Contains the MIME type of the resource.
|
||||||
|
*/
|
||||||
|
fn write_getcontenttype<W: Write>(xml_writer: &mut Writer<W>, content_type: &str) -> Result<()> {
|
||||||
|
xml_writer.write_event(Event::Start(BytesStart::borrowed(b"d:getcontenttype", "d:getcontenttype".len()))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontenttype start: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::Text(BytesText::from_plain_str(content_type))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontenttype text: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
xml_writer.write_event(Event::End(BytesEnd::borrowed(b"d:getcontenttype"))).map_err(|e| {
|
||||||
|
WebDavError::XmlError(format!("Failed to write getcontenttype end: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional helper methods for other WebDAV operations
|
||||||
|
|
||||||
|
// ... (PROPPATCH, LOCK, UNLOCK, etc. implementations would go here)
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* Calendar Entity
|
||||||
|
*
|
||||||
|
* This module defines the Calendar entity, which represents a calendar in the CalDAV
|
||||||
|
* implementation. Calendars contain calendar events and are owned by users.
|
||||||
|
*
|
||||||
|
* Calendars have properties such as name, color, and description, and they serve as
|
||||||
|
* containers for calendar events. Each calendar belongs to a specific user and can
|
||||||
|
* have custom properties.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error types specific to calendar operations.
|
||||||
|
*/
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CalendarError {
|
||||||
|
/// Error when calendar name is invalid
|
||||||
|
#[error("Invalid calendar name: {0}")]
|
||||||
|
InvalidName(String),
|
||||||
|
|
||||||
|
/// Error when color code is invalid
|
||||||
|
#[error("Invalid color code: {0}")]
|
||||||
|
InvalidColor(String),
|
||||||
|
|
||||||
|
/// Error when owner ID is invalid
|
||||||
|
#[error("Invalid owner ID: {0}")]
|
||||||
|
InvalidOwnerId(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calendar entity.
|
||||||
|
*
|
||||||
|
* Represents a calendar container that can hold multiple calendar events.
|
||||||
|
* Each calendar is owned by a user and has properties like name, color, and description.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Calendar {
|
||||||
|
/// Unique identifier for the calendar
|
||||||
|
id: Uuid,
|
||||||
|
|
||||||
|
/// Display name of the calendar
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// ID of the user who owns this calendar
|
||||||
|
owner_id: String,
|
||||||
|
|
||||||
|
/// Optional description of the calendar
|
||||||
|
description: Option<String>,
|
||||||
|
|
||||||
|
/// Optional color code for UI display (hex format #RRGGBB)
|
||||||
|
color: Option<String>,
|
||||||
|
|
||||||
|
/// Time when the calendar was created
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Time when the calendar was last modified
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Optional list of custom properties (for extended CalDAV support)
|
||||||
|
custom_properties: std::collections::HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Calendar {
|
||||||
|
/**
|
||||||
|
* Creates a new calendar with the given properties.
|
||||||
|
*
|
||||||
|
* @param name Display name of the calendar
|
||||||
|
* @param owner_id ID of the user who owns this calendar
|
||||||
|
* @param description Optional description of the calendar
|
||||||
|
* @param color Optional color code for UI display (#RRGGBB format)
|
||||||
|
* @return Result containing the new Calendar or a domain error
|
||||||
|
*/
|
||||||
|
pub fn new(
|
||||||
|
name: String,
|
||||||
|
owner_id: String,
|
||||||
|
description: Option<String>,
|
||||||
|
color: Option<String>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// Validate inputs
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Calendar name cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner_id.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Owner ID cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate color format if provided (#RRGGBB)
|
||||||
|
if let Some(ref color_str) = color {
|
||||||
|
if !color_str.starts_with('#') || color_str.len() != 7 {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Color must be in #RRGGBB format",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if remaining characters are valid hex
|
||||||
|
if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Color must be in #RRGGBB format with valid hex digits",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
name,
|
||||||
|
owner_id,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
custom_properties: std::collections::HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a calendar with specific ID and timestamps.
|
||||||
|
* Typically used when reconstructing from storage.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier for the calendar
|
||||||
|
* @param name Display name of the calendar
|
||||||
|
* @param owner_id ID of the user who owns this calendar
|
||||||
|
* @param description Optional description of the calendar
|
||||||
|
* @param color Optional color code for UI display
|
||||||
|
* @param created_at Time when the calendar was created
|
||||||
|
* @param updated_at Time when the calendar was last modified
|
||||||
|
* @return Result containing the new Calendar or a domain error
|
||||||
|
*/
|
||||||
|
pub fn with_id(
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
owner_id: String,
|
||||||
|
description: Option<String>,
|
||||||
|
color: Option<String>,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// Basic validation
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Calendar name cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner_id.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Owner ID cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
owner_id,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
custom_properties: std::collections::HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
/// Returns the calendar's unique identifier
|
||||||
|
pub fn id(&self) -> &Uuid {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the calendar's display name
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the ID of the user who owns this calendar
|
||||||
|
pub fn owner_id(&self) -> &str {
|
||||||
|
&self.owner_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the calendar's description, if any
|
||||||
|
pub fn description(&self) -> Option<&str> {
|
||||||
|
self.description.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the calendar's color code, if any
|
||||||
|
pub fn color(&self) -> Option<&str> {
|
||||||
|
self.color.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the time when the calendar was created
|
||||||
|
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||||
|
&self.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the time when the calendar was last modified
|
||||||
|
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||||
|
&self.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a custom property value by name, if it exists
|
||||||
|
pub fn custom_property(&self, name: &str) -> Option<&str> {
|
||||||
|
self.custom_properties.get(name).map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all custom properties
|
||||||
|
pub fn custom_properties(&self) -> &std::collections::HashMap<String, String> {
|
||||||
|
&self.custom_properties
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setters and Mutators
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the calendar's name.
|
||||||
|
*
|
||||||
|
* @param name New display name for the calendar
|
||||||
|
* @return Result indicating success or containing a domain error
|
||||||
|
*/
|
||||||
|
pub fn update_name(&mut self, name: String) -> Result<()> {
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Calendar name cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.name = name;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the calendar's description.
|
||||||
|
*
|
||||||
|
* @param description New description for the calendar
|
||||||
|
*/
|
||||||
|
pub fn update_description(&mut self, description: Option<String>) {
|
||||||
|
self.description = description;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the calendar's color.
|
||||||
|
*
|
||||||
|
* @param color New color code for the calendar
|
||||||
|
* @return Result indicating success or containing a domain error
|
||||||
|
*/
|
||||||
|
pub fn update_color(&mut self, color: Option<String>) -> Result<()> {
|
||||||
|
// Validate color format if provided (#RRGGBB)
|
||||||
|
if let Some(ref color_str) = color {
|
||||||
|
if !color_str.starts_with('#') || color_str.len() != 7 {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Color must be in #RRGGBB format",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if remaining characters are valid hex
|
||||||
|
if color_str[1..].chars().any(|c| !c.is_ascii_hexdigit()) {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Calendar",
|
||||||
|
"Color must be in #RRGGBB format with valid hex digits",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.color = color;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom property for extended CalDAV support.
|
||||||
|
*
|
||||||
|
* @param name Name of the property
|
||||||
|
* @param value Value of the property
|
||||||
|
*/
|
||||||
|
pub fn set_custom_property(&mut self, name: String, value: String) {
|
||||||
|
self.custom_properties.insert(name, value);
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a custom property.
|
||||||
|
*
|
||||||
|
* @param name Name of the property to remove
|
||||||
|
* @return true if the property was removed, false if it didn't exist
|
||||||
|
*/
|
||||||
|
pub fn remove_custom_property(&mut self, name: &str) -> bool {
|
||||||
|
let result = self.custom_properties.remove(name).is_some();
|
||||||
|
if result {
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this calendar belongs to the specified user.
|
||||||
|
*
|
||||||
|
* @param user_id ID of the user to check ownership against
|
||||||
|
* @return true if the calendar belongs to the user, false otherwise
|
||||||
|
*/
|
||||||
|
pub fn belongs_to(&self, user_id: &str) -> bool {
|
||||||
|
self.owner_id == user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the last modification time of the calendar to now.
|
||||||
|
* Called when calendar events are added, modified, or removed.
|
||||||
|
*/
|
||||||
|
pub fn touch(&mut self) {
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,810 @@
|
|||||||
|
/**
|
||||||
|
* Calendar Event Entity
|
||||||
|
*
|
||||||
|
* This module defines the CalendarEvent entity, which represents an event or
|
||||||
|
* appointment in a calendar, following the iCalendar (RFC 5545) specification.
|
||||||
|
*
|
||||||
|
* Calendar events have properties like summary, description, location, start/end times,
|
||||||
|
* and can include recurrence rules for repeating events. Each event belongs to a
|
||||||
|
* specific calendar and stores its complete iCalendar representation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::{DateTime, Utc, Duration};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error types specific to calendar event operations.
|
||||||
|
*/
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CalendarEventError {
|
||||||
|
/// Error when event summary/title is invalid
|
||||||
|
#[error("Invalid event summary: {0}")]
|
||||||
|
InvalidSummary(String),
|
||||||
|
|
||||||
|
/// Error when event dates are invalid
|
||||||
|
#[error("Invalid event dates: {0}")]
|
||||||
|
InvalidDates(String),
|
||||||
|
|
||||||
|
/// Error when recurrence rule is invalid
|
||||||
|
#[error("Invalid recurrence rule: {0}")]
|
||||||
|
InvalidRecurrence(String),
|
||||||
|
|
||||||
|
/// Error when iCalendar data is invalid
|
||||||
|
#[error("Invalid iCalendar data: {0}")]
|
||||||
|
InvalidICalData(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CalendarEvent entity.
|
||||||
|
*
|
||||||
|
* Represents a calendar event or appointment that can be synced via CalDAV.
|
||||||
|
* Follows the iCalendar format (RFC 5545) for compatibility with CalDAV clients.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CalendarEvent {
|
||||||
|
/// Unique identifier for the event
|
||||||
|
id: Uuid,
|
||||||
|
|
||||||
|
/// ID of the calendar this event belongs to
|
||||||
|
calendar_id: Uuid,
|
||||||
|
|
||||||
|
/// Short summary/title of the event
|
||||||
|
summary: String,
|
||||||
|
|
||||||
|
/// Detailed description of the event (optional)
|
||||||
|
description: Option<String>,
|
||||||
|
|
||||||
|
/// Location of the event (optional)
|
||||||
|
location: Option<String>,
|
||||||
|
|
||||||
|
/// Start time of the event
|
||||||
|
start_time: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// End time of the event
|
||||||
|
end_time: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Whether this is an all-day event
|
||||||
|
all_day: bool,
|
||||||
|
|
||||||
|
/// Recurrence rule in iCalendar RRULE format (optional)
|
||||||
|
rrule: Option<String>,
|
||||||
|
|
||||||
|
/// Unique identifier in iCalendar format (used for CalDAV sync)
|
||||||
|
ical_uid: String,
|
||||||
|
|
||||||
|
/// Complete iCalendar data (VEVENT component)
|
||||||
|
ical_data: String,
|
||||||
|
|
||||||
|
/// Time when the event was created
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Time when the event was last modified
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CalendarEvent {
|
||||||
|
/**
|
||||||
|
* Creates a new calendar event with the given properties.
|
||||||
|
*
|
||||||
|
* @param calendar_id ID of the calendar this event belongs to
|
||||||
|
* @param summary Short summary/title of the event
|
||||||
|
* @param description Detailed description of the event (optional)
|
||||||
|
* @param location Location of the event (optional)
|
||||||
|
* @param start_time Start time of the event
|
||||||
|
* @param end_time End time of the event
|
||||||
|
* @param all_day Whether this is an all-day event
|
||||||
|
* @param rrule Recurrence rule in iCalendar RRULE format (optional)
|
||||||
|
* @param ical_data Complete iCalendar data (VEVENT component)
|
||||||
|
* @return Result containing the new CalendarEvent or a domain error
|
||||||
|
*/
|
||||||
|
pub fn new(
|
||||||
|
calendar_id: Uuid,
|
||||||
|
summary: String,
|
||||||
|
description: Option<String>,
|
||||||
|
location: Option<String>,
|
||||||
|
start_time: DateTime<Utc>,
|
||||||
|
end_time: DateTime<Utc>,
|
||||||
|
all_day: bool,
|
||||||
|
rrule: Option<String>,
|
||||||
|
ical_data: String,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// Validate inputs
|
||||||
|
if summary.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Event summary cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_time < start_time {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"End time cannot be before start time",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate RRULE if provided (basic validation)
|
||||||
|
if let Some(ref rule) = rrule {
|
||||||
|
if !rule.starts_with("FREQ=") {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Recurrence rule must start with FREQ=",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate iCalendar data (basic validation)
|
||||||
|
if !ical_data.contains("BEGIN:VEVENT") || !ical_data.contains("END:VEVENT") {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"iCalendar data must contain a VEVENT component",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
calendar_id,
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
location,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
all_day,
|
||||||
|
rrule,
|
||||||
|
ical_uid: Uuid::new_v4().to_string(),
|
||||||
|
ical_data,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a calendar event with specific ID and timestamps.
|
||||||
|
* Typically used when reconstructing from storage.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier for the event
|
||||||
|
* @param calendar_id ID of the calendar this event belongs to
|
||||||
|
* @param summary Short summary/title of the event
|
||||||
|
* @param description Detailed description of the event (optional)
|
||||||
|
* @param location Location of the event (optional)
|
||||||
|
* @param start_time Start time of the event
|
||||||
|
* @param end_time End time of the event
|
||||||
|
* @param all_day Whether this is an all-day event
|
||||||
|
* @param rrule Recurrence rule in iCalendar RRULE format (optional)
|
||||||
|
* @param ical_uid Unique identifier in iCalendar format
|
||||||
|
* @param ical_data Complete iCalendar data (VEVENT component)
|
||||||
|
* @param created_at Time when the event was created
|
||||||
|
* @param updated_at Time when the event was last modified
|
||||||
|
* @return Result containing the new CalendarEvent or a domain error
|
||||||
|
*/
|
||||||
|
pub fn with_id(
|
||||||
|
id: Uuid,
|
||||||
|
calendar_id: Uuid,
|
||||||
|
summary: String,
|
||||||
|
description: Option<String>,
|
||||||
|
location: Option<String>,
|
||||||
|
start_time: DateTime<Utc>,
|
||||||
|
end_time: DateTime<Utc>,
|
||||||
|
all_day: bool,
|
||||||
|
rrule: Option<String>,
|
||||||
|
ical_uid: String,
|
||||||
|
ical_data: String,
|
||||||
|
created_at: DateTime<Utc>,
|
||||||
|
updated_at: DateTime<Utc>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
// Basic validation
|
||||||
|
if summary.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Event summary cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if end_time < start_time {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"End time cannot be before start time",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
calendar_id,
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
location,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
all_day,
|
||||||
|
rrule,
|
||||||
|
ical_uid,
|
||||||
|
ical_data,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a calendar event from an iCalendar VEVENT component.
|
||||||
|
* Parses the iCalendar data to extract event properties.
|
||||||
|
*
|
||||||
|
* @param calendar_id ID of the calendar this event belongs to
|
||||||
|
* @param ical_data Complete iCalendar data (VEVENT component)
|
||||||
|
* @return Result containing the new CalendarEvent or a domain error
|
||||||
|
*/
|
||||||
|
pub fn from_ical(calendar_id: Uuid, ical_data: String) -> Result<Self> {
|
||||||
|
// This implementation would require a proper iCalendar parser
|
||||||
|
// For brevity, we're using a simplified version here
|
||||||
|
|
||||||
|
// Extract required fields from iCalendar data
|
||||||
|
let summary = Self::extract_ical_property(&ical_data, "SUMMARY")
|
||||||
|
.ok_or_else(|| DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Missing SUMMARY in iCalendar data",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let dtstart = Self::extract_ical_property(&ical_data, "DTSTART")
|
||||||
|
.ok_or_else(|| DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Missing DTSTART in iCalendar data",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let dtend = Self::extract_ical_property(&ical_data, "DTEND")
|
||||||
|
.ok_or_else(|| DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Missing DTEND in iCalendar data",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// Parse dates (simplified)
|
||||||
|
let start_time = Self::parse_ical_datetime(&dtstart)
|
||||||
|
.map_err(|e| DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
format!("Invalid DTSTART: {}", e),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let end_time = Self::parse_ical_datetime(&dtend)
|
||||||
|
.map_err(|e| DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
format!("Invalid DTEND: {}", e),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
// Determine if all-day event (simplified check)
|
||||||
|
let all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
|
||||||
|
|
||||||
|
// Extract optional fields
|
||||||
|
let description = Self::extract_ical_property(&ical_data, "DESCRIPTION");
|
||||||
|
let location = Self::extract_ical_property(&ical_data, "LOCATION");
|
||||||
|
let rrule = Self::extract_ical_property(&ical_data, "RRULE");
|
||||||
|
|
||||||
|
// Extract UID or generate a new one
|
||||||
|
let ical_uid = Self::extract_ical_property(&ical_data, "UID")
|
||||||
|
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
calendar_id,
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
location,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
all_day,
|
||||||
|
rrule,
|
||||||
|
ical_uid,
|
||||||
|
ical_data,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
/// Returns the event's unique identifier
|
||||||
|
pub fn id(&self) -> &Uuid {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the ID of the calendar this event belongs to
|
||||||
|
pub fn calendar_id(&self) -> &Uuid {
|
||||||
|
&self.calendar_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's summary/title
|
||||||
|
pub fn summary(&self) -> &str {
|
||||||
|
&self.summary
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's description, if any
|
||||||
|
pub fn description(&self) -> Option<&str> {
|
||||||
|
self.description.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's location, if any
|
||||||
|
pub fn location(&self) -> Option<&str> {
|
||||||
|
self.location.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's start time
|
||||||
|
pub fn start_time(&self) -> &DateTime<Utc> {
|
||||||
|
&self.start_time
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's end time
|
||||||
|
pub fn end_time(&self) -> &DateTime<Utc> {
|
||||||
|
&self.end_time
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this is an all-day event
|
||||||
|
pub fn all_day(&self) -> bool {
|
||||||
|
self.all_day
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's recurrence rule, if any
|
||||||
|
pub fn rrule(&self) -> Option<&str> {
|
||||||
|
self.rrule.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event's iCalendar UID
|
||||||
|
pub fn ical_uid(&self) -> &str {
|
||||||
|
&self.ical_uid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complete iCalendar data for the event
|
||||||
|
pub fn ical_data(&self) -> &str {
|
||||||
|
&self.ical_data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the time when the event was created
|
||||||
|
pub fn created_at(&self) -> &DateTime<Utc> {
|
||||||
|
&self.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the time when the event was last modified
|
||||||
|
pub fn updated_at(&self) -> &DateTime<Utc> {
|
||||||
|
&self.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration of the event
|
||||||
|
pub fn duration(&self) -> Duration {
|
||||||
|
self.end_time - self.start_time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setters and Mutators
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the event's summary/title.
|
||||||
|
*
|
||||||
|
* @param summary New summary/title for the event
|
||||||
|
* @return Result indicating success or containing a domain error
|
||||||
|
*/
|
||||||
|
pub fn update_summary(&mut self, summary: String) -> Result<()> {
|
||||||
|
if summary.is_empty() {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Event summary cannot be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.summary = summary;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// Update iCalendar data
|
||||||
|
self.update_ical_property("SUMMARY", &self.summary);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the event's description.
|
||||||
|
*
|
||||||
|
* @param description New description for the event
|
||||||
|
*/
|
||||||
|
pub fn update_description(&mut self, description: Option<String>) {
|
||||||
|
self.description = description.clone();
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// Update iCalendar data
|
||||||
|
match description {
|
||||||
|
Some(desc) => self.update_ical_property("DESCRIPTION", &desc),
|
||||||
|
None => self.remove_ical_property("DESCRIPTION"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the event's location.
|
||||||
|
*
|
||||||
|
* @param location New location for the event
|
||||||
|
*/
|
||||||
|
pub fn update_location(&mut self, location: Option<String>) {
|
||||||
|
self.location = location.clone();
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// Update iCalendar data
|
||||||
|
match location {
|
||||||
|
Some(loc) => self.update_ical_property("LOCATION", &loc),
|
||||||
|
None => self.remove_ical_property("LOCATION"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the event's start and end times.
|
||||||
|
*
|
||||||
|
* @param start_time New start time for the event
|
||||||
|
* @param end_time New end time for the event
|
||||||
|
* @return Result indicating success or containing a domain error
|
||||||
|
*/
|
||||||
|
pub fn update_time_range(&mut self, start_time: DateTime<Utc>, end_time: DateTime<Utc>) -> Result<()> {
|
||||||
|
if end_time < start_time {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"End time cannot be before start time",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.start_time = start_time;
|
||||||
|
self.end_time = end_time;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// Update iCalendar data
|
||||||
|
let start_str = if self.all_day {
|
||||||
|
format!("{}T000000Z", start_time.format("%Y%m%d"))
|
||||||
|
} else {
|
||||||
|
format!("{}", start_time.format("%Y%m%dT%H%M%SZ"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_str = if self.all_day {
|
||||||
|
format!("{}T000000Z", end_time.format("%Y%m%d"))
|
||||||
|
} else {
|
||||||
|
format!("{}", end_time.format("%Y%m%dT%H%M%SZ"))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_ical_property("DTSTART", &start_str);
|
||||||
|
self.update_ical_property("DTEND", &end_str);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates whether this is an all-day event.
|
||||||
|
*
|
||||||
|
* @param all_day Whether this is an all-day event
|
||||||
|
*/
|
||||||
|
pub fn update_all_day(&mut self, all_day: bool) {
|
||||||
|
self.all_day = all_day;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// Update iCalendar data
|
||||||
|
let start_str = if all_day {
|
||||||
|
format!("VALUE=DATE:{}", self.start_time.format("%Y%m%d"))
|
||||||
|
} else {
|
||||||
|
format!("{}", self.start_time.format("%Y%m%dT%H%M%SZ"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_str = if all_day {
|
||||||
|
format!("VALUE=DATE:{}", self.end_time.format("%Y%m%d"))
|
||||||
|
} else {
|
||||||
|
format!("{}", self.end_time.format("%Y%m%dT%H%M%SZ"))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_ical_property("DTSTART", &start_str);
|
||||||
|
self.update_ical_property("DTEND", &end_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the event's recurrence rule.
|
||||||
|
*
|
||||||
|
* @param rrule New recurrence rule for the event
|
||||||
|
* @return Result indicating success or containing a domain error
|
||||||
|
*/
|
||||||
|
pub fn update_rrule(&mut self, rrule: Option<String>) -> Result<()> {
|
||||||
|
// Validate RRULE if provided (basic validation)
|
||||||
|
if let Some(ref rule) = rrule {
|
||||||
|
if !rule.starts_with("FREQ=") {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"Recurrence rule must start with FREQ=",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rrule = rrule.clone();
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
// Update iCalendar data
|
||||||
|
match rrule {
|
||||||
|
Some(rule) => self.update_ical_property("RRULE", &rule),
|
||||||
|
None => self.remove_ical_property("RRULE"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the complete iCalendar data for the event.
|
||||||
|
* Also updates the event properties based on the new iCalendar data.
|
||||||
|
*
|
||||||
|
* @param ical_data New iCalendar data for the event
|
||||||
|
* @return Result indicating success or containing a domain error
|
||||||
|
*/
|
||||||
|
pub fn update_ical_data(&mut self, ical_data: String) -> Result<()> {
|
||||||
|
// Validate iCalendar data (basic validation)
|
||||||
|
if !ical_data.contains("BEGIN:VEVENT") || !ical_data.contains("END:VEVENT") {
|
||||||
|
return Err(DomainError::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"CalendarEvent",
|
||||||
|
"iCalendar data must contain a VEVENT component",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and update properties from iCalendar data
|
||||||
|
if let Some(summary) = Self::extract_ical_property(&ical_data, "SUMMARY") {
|
||||||
|
self.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.description = Self::extract_ical_property(&ical_data, "DESCRIPTION");
|
||||||
|
self.location = Self::extract_ical_property(&ical_data, "LOCATION");
|
||||||
|
|
||||||
|
if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART") {
|
||||||
|
if let Ok(start_time) = Self::parse_ical_datetime(&dtstart) {
|
||||||
|
self.start_time = start_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dtend) = Self::extract_ical_property(&ical_data, "DTEND") {
|
||||||
|
if let Ok(end_time) = Self::parse_ical_datetime(&dtend) {
|
||||||
|
self.end_time = end_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all-day status based on DTSTART
|
||||||
|
if let Some(dtstart) = Self::extract_ical_property(&ical_data, "DTSTART") {
|
||||||
|
self.all_day = dtstart.contains("VALUE=DATE") && !dtstart.contains("T");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rrule = Self::extract_ical_property(&ical_data, "RRULE");
|
||||||
|
|
||||||
|
if let Some(uid) = Self::extract_ical_property(&ical_data, "UID") {
|
||||||
|
self.ical_uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ical_data = ical_data;
|
||||||
|
self.updated_at = Utc::now();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this event belongs to the specified calendar.
|
||||||
|
*
|
||||||
|
* @param calendar_id ID of the calendar to check against
|
||||||
|
* @return true if the event belongs to the calendar, false otherwise
|
||||||
|
*/
|
||||||
|
pub fn belongs_to_calendar(&self, calendar_id: &Uuid) -> bool {
|
||||||
|
self.calendar_id == *calendar_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this event occurs within the specified time range.
|
||||||
|
*
|
||||||
|
* @param start Start of the time range to check
|
||||||
|
* @param end End of the time range to check
|
||||||
|
* @return true if the event occurs within the range, false otherwise
|
||||||
|
*/
|
||||||
|
pub fn occurs_in_range(&self, start: &DateTime<Utc>, end: &DateTime<Utc>) -> bool {
|
||||||
|
// Basic case: event directly overlaps with range
|
||||||
|
if (self.start_time <= *end && self.end_time >= *start) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If event has recurrence, check if any recurrence occurs in range
|
||||||
|
// Note: A full implementation would need a proper recurrence rule parser
|
||||||
|
if let Some(rrule) = &self.rrule {
|
||||||
|
// Simplified check for demonstration
|
||||||
|
// A real implementation would need to generate recurrence instances
|
||||||
|
// and check if any fall within the range
|
||||||
|
|
||||||
|
// For now, we'll just check if the recurrence hasn't ended
|
||||||
|
// or if it ended after the start of our range
|
||||||
|
if let Some(until_pos) = rrule.find("UNTIL=") {
|
||||||
|
let until_start = until_pos + 6; // "UNTIL=" is 6 chars
|
||||||
|
if let Some(until_end) = rrule[until_start..].find(';') {
|
||||||
|
let until_str = &rrule[until_start..until_start+until_end];
|
||||||
|
if let Ok(until_date) = Self::parse_ical_datetime(&until_str) {
|
||||||
|
return until_date >= *start;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// UNTIL is the last part of the rule
|
||||||
|
let until_str = &rrule[until_start..];
|
||||||
|
if let Ok(until_date) = Self::parse_ical_datetime(&until_str) {
|
||||||
|
return until_date >= *start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No UNTIL specified, so recurrence continues indefinitely
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for iCalendar operations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a property value from iCalendar data.
|
||||||
|
*
|
||||||
|
* @param ical_data The iCalendar data to search in
|
||||||
|
* @param property_name The name of the property to extract
|
||||||
|
* @return Option containing the property value if found
|
||||||
|
*/
|
||||||
|
fn extract_ical_property(ical_data: &str, property_name: &str) -> Option<String> {
|
||||||
|
// Find the property in the iCalendar data
|
||||||
|
let search_str = format!("\n{}:", property_name);
|
||||||
|
let search_str_alt = format!("\r\n{}:", property_name);
|
||||||
|
|
||||||
|
let pos = ical_data.find(&search_str)
|
||||||
|
.or_else(|| ical_data.find(&search_str_alt));
|
||||||
|
|
||||||
|
if let Some(pos) = pos {
|
||||||
|
// Find the start of the value
|
||||||
|
let value_start = pos + search_str.len();
|
||||||
|
|
||||||
|
// Find the end of the value (next line or end of string)
|
||||||
|
let value_end = ical_data[value_start..]
|
||||||
|
.find('\n')
|
||||||
|
.map(|p| value_start + p)
|
||||||
|
.unwrap_or_else(|| ical_data.len());
|
||||||
|
|
||||||
|
// Extract and return the value
|
||||||
|
let value = ical_data[value_start..value_end].trim();
|
||||||
|
if !value.is_empty() {
|
||||||
|
return Some(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an iCalendar datetime string into a DateTime object.
|
||||||
|
*
|
||||||
|
* @param datetime The iCalendar datetime string to parse
|
||||||
|
* @return Result containing the parsed DateTime or an error
|
||||||
|
*/
|
||||||
|
fn parse_ical_datetime(datetime: &str) -> std::result::Result<DateTime<Utc>, String> {
|
||||||
|
// Handle VALUE=DATE format
|
||||||
|
if datetime.contains("VALUE=DATE") {
|
||||||
|
let date_str = datetime.split(':').last().unwrap_or("");
|
||||||
|
if date_str.len() != 8 {
|
||||||
|
return Err("Invalid date format".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = date_str[0..4].parse::<i32>()
|
||||||
|
.map_err(|_| "Invalid year".to_string())?;
|
||||||
|
let month = date_str[4..6].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid month".to_string())?;
|
||||||
|
let day = date_str[6..8].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid day".to_string())?;
|
||||||
|
|
||||||
|
return match chrono::NaiveDate::from_ymd_opt(year, month, day) {
|
||||||
|
Some(date) => Ok(DateTime::<Utc>::from_utc(date.and_hms_opt(0, 0, 0).unwrap(), Utc)),
|
||||||
|
None => Err("Invalid date components".to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle standard UTC format (20230101T120000Z)
|
||||||
|
let datetime_str = datetime.split(':').last().unwrap_or(datetime);
|
||||||
|
if datetime_str.len() < 15 || !datetime_str.ends_with('Z') {
|
||||||
|
return Err("Invalid datetime format".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = datetime_str[0..4].parse::<i32>()
|
||||||
|
.map_err(|_| "Invalid year".to_string())?;
|
||||||
|
let month = datetime_str[4..6].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid month".to_string())?;
|
||||||
|
let day = datetime_str[6..8].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid day".to_string())?;
|
||||||
|
|
||||||
|
let hour = datetime_str[9..11].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid hour".to_string())?;
|
||||||
|
let minute = datetime_str[11..13].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid minute".to_string())?;
|
||||||
|
let second = datetime_str[13..15].parse::<u32>()
|
||||||
|
.map_err(|_| "Invalid second".to_string())?;
|
||||||
|
|
||||||
|
match chrono::NaiveDate::from_ymd_opt(year, month, day) {
|
||||||
|
Some(date) => match date.and_hms_opt(hour, minute, second) {
|
||||||
|
Some(datetime) => Ok(DateTime::<Utc>::from_utc(datetime, Utc)),
|
||||||
|
None => Err("Invalid time components".to_string()),
|
||||||
|
},
|
||||||
|
None => Err("Invalid date components".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an iCalendar property in the event's iCalendar data.
|
||||||
|
*
|
||||||
|
* @param property_name The name of the property to update
|
||||||
|
* @param value The new value for the property
|
||||||
|
*/
|
||||||
|
fn update_ical_property(&mut self, property_name: &str, value: &str) {
|
||||||
|
let search_str = format!("\n{}:", property_name);
|
||||||
|
let search_str_alt = format!("\r\n{}:", property_name);
|
||||||
|
|
||||||
|
// Check if property exists
|
||||||
|
let pos = self.ical_data.find(&search_str)
|
||||||
|
.or_else(|| self.ical_data.find(&search_str_alt));
|
||||||
|
|
||||||
|
if let Some(pos) = pos {
|
||||||
|
// Find the start of the value
|
||||||
|
let value_start = pos + search_str.len();
|
||||||
|
|
||||||
|
// Find the end of the value (next line or end of string)
|
||||||
|
let value_end = self.ical_data[value_start..]
|
||||||
|
.find('\n')
|
||||||
|
.map(|p| value_start + p)
|
||||||
|
.unwrap_or_else(|| self.ical_data.len());
|
||||||
|
|
||||||
|
// Replace the value
|
||||||
|
let before = &self.ical_data[..value_start];
|
||||||
|
let after = &self.ical_data[value_end..];
|
||||||
|
self.ical_data = format!("{}{}{}", before, value, after);
|
||||||
|
} else {
|
||||||
|
// Property doesn't exist, add it before END:VEVENT
|
||||||
|
let end_pos = self.ical_data.find("END:VEVENT")
|
||||||
|
.unwrap_or(self.ical_data.len());
|
||||||
|
|
||||||
|
let before = &self.ical_data[..end_pos];
|
||||||
|
let after = &self.ical_data[end_pos..];
|
||||||
|
self.ical_data = format!("{}{}:{}\n{}", before, property_name, value, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an iCalendar property from the event's iCalendar data.
|
||||||
|
*
|
||||||
|
* @param property_name The name of the property to remove
|
||||||
|
*/
|
||||||
|
fn remove_ical_property(&mut self, property_name: &str) {
|
||||||
|
let search_str = format!("\n{}:", property_name);
|
||||||
|
let search_str_alt = format!("\r\n{}:", property_name);
|
||||||
|
|
||||||
|
// Check if property exists
|
||||||
|
let pos = self.ical_data.find(&search_str)
|
||||||
|
.or_else(|| self.ical_data.find(&search_str_alt));
|
||||||
|
|
||||||
|
if let Some(pos) = pos {
|
||||||
|
// Find the end of the value (next line or end of string)
|
||||||
|
let value_end = self.ical_data[pos + 1..]
|
||||||
|
.find('\n')
|
||||||
|
.map(|p| pos + 1 + p)
|
||||||
|
.unwrap_or_else(|| self.ical_data.len());
|
||||||
|
|
||||||
|
// Remove the property
|
||||||
|
let before = &self.ical_data[..pos];
|
||||||
|
let after = &self.ical_data[value_end..];
|
||||||
|
self.ical_data = format!("{}{}", before, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* WebDAV Handler Module
|
||||||
|
*
|
||||||
|
* This module implements the WebDAV protocol (RFC 4918) endpoints for OxiCloud.
|
||||||
|
* It provides a complete WebDAV server implementation that allows clients to
|
||||||
|
* perform file operations over HTTP, including reading, writing, and manipulating
|
||||||
|
* files and directories.
|
||||||
|
*
|
||||||
|
* The WebDAV protocol extends HTTP to provide file system-like functionality, enabling:
|
||||||
|
* - File/folder listing (PROPFIND)
|
||||||
|
* - Creation of collections/directories (MKCOL)
|
||||||
|
* - Retrieving and updating resources (GET, PUT)
|
||||||
|
* - Moving and copying resources (MOVE, COPY)
|
||||||
|
* - Resource locking for concurrency control (LOCK, UNLOCK)
|
||||||
|
*
|
||||||
|
* This implementation leverages OxiCloud's existing file and folder services
|
||||||
|
* through the application's port interfaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::get,
|
||||||
|
extract::{Path, State, Request, Extension},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use http::{Method, header};
|
||||||
|
|
||||||
|
use crate::common::di::AppState;
|
||||||
|
use crate::interfaces::middleware::auth::CurrentUser;
|
||||||
|
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase};
|
||||||
|
use crate::application::ports::folder_ports::FolderUseCase;
|
||||||
|
use crate::common::errors::AppError;
|
||||||
|
use crate::application::adapters::webdav_adapter::WebDavAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns the WebDAV router with all required endpoints.
|
||||||
|
*
|
||||||
|
* This function sets up all WebDAV method handlers following RFC 4918,
|
||||||
|
* mapping HTTP methods to appropriate WebDAV operations.
|
||||||
|
*
|
||||||
|
* @return Router configured with WebDAV endpoints
|
||||||
|
*/
|
||||||
|
pub fn webdav_routes() -> Router<Arc<AppState>> {
|
||||||
|
Router::new()
|
||||||
|
// Standard HTTP methods used in WebDAV
|
||||||
|
.route("/webdav/*path", get(handle_get))
|
||||||
|
.route("/webdav/*path", axum::routing::head(handle_head))
|
||||||
|
.route("/webdav/*path", axum::routing::put(handle_put))
|
||||||
|
.route("/webdav/*path", axum::routing::delete(handle_delete))
|
||||||
|
|
||||||
|
// WebDAV-specific methods
|
||||||
|
.route_with_tsr("/webdav/*path", axum::routing::on(
|
||||||
|
Method::OPTIONS, handle_options,
|
||||||
|
Method::PROPFIND, handle_propfind,
|
||||||
|
Method::PROPPATCH, handle_proppatch,
|
||||||
|
Method::MKCOL, handle_mkcol,
|
||||||
|
Method::COPY, handle_copy,
|
||||||
|
Method::MOVE, handle_move,
|
||||||
|
Method::LOCK, handle_lock,
|
||||||
|
Method::UNLOCK, handle_unlock,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles OPTIONS requests to advertise WebDAV capabilities.
|
||||||
|
*
|
||||||
|
* This handler responds with the DAV header indicating WebDAV compliance
|
||||||
|
* level and the methods supported by this WebDAV server.
|
||||||
|
*
|
||||||
|
* @param state The application state containing service dependencies
|
||||||
|
* @param path The requested resource path
|
||||||
|
* @return HTTP response with appropriate WebDAV headers
|
||||||
|
*/
|
||||||
|
async fn handle_options(
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
Path(_path): Path<String>,
|
||||||
|
) -> Response {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::DAV, "1, 2") // Class 1 and 2 WebDAV support
|
||||||
|
.header(header::ALLOW, "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK")
|
||||||
|
.body(axum::body::Body::empty())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles PROPFIND requests to retrieve resource properties.
|
||||||
|
*
|
||||||
|
* This handler processes WebDAV PROPFIND requests, which are used to retrieve
|
||||||
|
* properties for one or more resources. It supports different depths (0, 1, infinity)
|
||||||
|
* and can return all properties or a specified subset based on the request.
|
||||||
|
*
|
||||||
|
* @param state The application state containing service dependencies
|
||||||
|
* @param user The authenticated user making the request
|
||||||
|
* @param path The requested resource path
|
||||||
|
* @param request The full HTTP request containing headers and body
|
||||||
|
* @return XML response with requested properties
|
||||||
|
*/
|
||||||
|
async fn handle_propfind(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Extension(user): Extension<CurrentUser>,
|
||||||
|
Path(path): Path<String>,
|
||||||
|
request: Request<axum::body::Body>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
// Extract depth header (0, 1, or infinity)
|
||||||
|
let depth = request
|
||||||
|
.headers()
|
||||||
|
.get(header::from_str("Depth").unwrap())
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("infinity");
|
||||||
|
|
||||||
|
// Read request body to determine which properties are requested
|
||||||
|
let body_bytes = hyper::body::to_bytes(request.into_body()).await
|
||||||
|
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
|
||||||
|
|
||||||
|
// Use the adapter to parse the PROPFIND request
|
||||||
|
let prop_find_request = WebDavAdapter::parse_propfind(&body_bytes[..])
|
||||||
|
.map_err(|e| AppError::bad_request(format!("Invalid PROPFIND request: {}", e)))?;
|
||||||
|
|
||||||
|
// Determine if the path is a file or directory
|
||||||
|
let is_file = if path.ends_with('/') {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// Check if path exists as a file
|
||||||
|
let file_service = state.file_service.as_ref()
|
||||||
|
.ok_or_else(|| AppError::internal_error("File service not configured"))?;
|
||||||
|
|
||||||
|
match file_service.file_retrieval_service.get_file_by_path(&path, &user.id).await {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_file {
|
||||||
|
// Handle PROPFIND for a file
|
||||||
|
let file_service = state.file_service.as_ref()
|
||||||
|
.ok_or_else(|| AppError::internal_error("File service not configured"))?;
|
||||||
|
|
||||||
|
let file = file_service.file_retrieval_service.get_file_by_path(&path, &user.id).await?;
|
||||||
|
|
||||||
|
// Generate XML response
|
||||||
|
let mut xml_buffer = Vec::new();
|
||||||
|
WebDavAdapter::generate_propfind_response_for_file(
|
||||||
|
&mut xml_buffer,
|
||||||
|
&file,
|
||||||
|
&prop_find_request,
|
||||||
|
depth,
|
||||||
|
&format!("/webdav/{}", path),
|
||||||
|
).map_err(|e| AppError::internal_error(format!("Failed to generate XML response: {}", e)))?;
|
||||||
|
|
||||||
|
// Return response with appropriate headers
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::MULTI_STATUS)
|
||||||
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||||
|
.body(axum::body::Body::from(xml_buffer))
|
||||||
|
.unwrap())
|
||||||
|
} else {
|
||||||
|
// Handle PROPFIND for a directory
|
||||||
|
let folder_service = state.folder_service.as_ref()
|
||||||
|
.ok_or_else(|| AppError::internal_error("Folder service not configured"))?;
|
||||||
|
|
||||||
|
let folder_path = if path.ends_with('/') { path.clone() } else { format!("{}/", path) };
|
||||||
|
let folder = folder_service.get_folder_by_path(&folder_path, &user.id).await?;
|
||||||
|
|
||||||
|
// Fetch children if depth > 0
|
||||||
|
let (files, folders) = if depth == "0" {
|
||||||
|
(Vec::new(), Vec::new())
|
||||||
|
} else {
|
||||||
|
let files = file_service.file_retrieval_service.get_files_in_folder(
|
||||||
|
Some(&folder.id.to_string()),
|
||||||
|
&user.id,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
let folders = folder_service.get_subfolders(
|
||||||
|
Some(&folder.id.to_string()),
|
||||||
|
&user.id,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
(files, folders)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate XML response
|
||||||
|
let mut xml_buffer = Vec::new();
|
||||||
|
WebDavAdapter::generate_propfind_response(
|
||||||
|
&mut xml_buffer,
|
||||||
|
Some(&folder),
|
||||||
|
&files,
|
||||||
|
&folders,
|
||||||
|
&prop_find_request,
|
||||||
|
depth,
|
||||||
|
&format!("/webdav/{}", folder_path),
|
||||||
|
).map_err(|e| AppError::internal_error(format!("Failed to generate XML response: {}", e)))?;
|
||||||
|
|
||||||
|
// Return response with appropriate headers
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::MULTI_STATUS)
|
||||||
|
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||||
|
.body(axum::body::Body::from(xml_buffer))
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles GET requests to retrieve file contents.
|
||||||
|
*
|
||||||
|
* This handler streams file contents to the client with appropriate
|
||||||
|
* content type and other metadata headers.
|
||||||
|
*
|
||||||
|
* @param state The application state containing service dependencies
|
||||||
|
* @param user The authenticated user making the request
|
||||||
|
* @param path The requested file path
|
||||||
|
* @return Streaming response with file contents
|
||||||
|
*/
|
||||||
|
async fn handle_get(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Extension(user): Extension<CurrentUser>,
|
||||||
|
Path(path): Path<String>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
// Ensure this is a file request (not a directory)
|
||||||
|
if path.ends_with('/') {
|
||||||
|
return Err(AppError::bad_request("Cannot GET a directory"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_service = state.file_service.as_ref()
|
||||||
|
.ok_or_else(|| AppError::internal_error("File service not configured"))?;
|
||||||
|
|
||||||
|
// Get file metadata
|
||||||
|
let file = file_service.file_retrieval_service.get_file_by_path(&path, &user.id).await?;
|
||||||
|
|
||||||
|
// Stream file content
|
||||||
|
let stream = file_service.file_retrieval_service.get_file_stream(&file.id, &user.id).await?;
|
||||||
|
|
||||||
|
// Return streamed response with appropriate headers
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(header::CONTENT_TYPE, file.mime_type)
|
||||||
|
.header(header::CONTENT_LENGTH, file.size.to_string())
|
||||||
|
.header(header::ETAG, format!("\"{}\"", file.id))
|
||||||
|
.header(header::LAST_MODIFIED, file.updated_at.to_rfc2822())
|
||||||
|
.body(axum::body::Body::from_stream(stream))
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement remaining WebDAV method handlers...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles HEAD requests to retrieve file metadata without content.
|
||||||
|
* Similar to GET but without returning the file body.
|
||||||
|
*/
|
||||||
|
async fn handle_head(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Extension(user): Extension<CurrentUser>,
|
||||||
|
Path(path): Path<String>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
// Implementation similar to handle_get but without body
|
||||||
|
// ...
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles PUT requests to create or update files.
|
||||||
|
* Streams the request body to create or replace a file.
|
||||||
|
*/
|
||||||
|
async fn handle_put(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles PROPPATCH requests to update resource properties.
|
||||||
|
* Processes property updates and removals.
|
||||||
|
*/
|
||||||
|
async fn handle_proppatch(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles MKCOL requests to create directories.
|
||||||
|
* Creates a new collection (directory) at the specified path.
|
||||||
|
*/
|
||||||
|
async fn handle_mkcol(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles DELETE requests to remove resources.
|
||||||
|
* Deletes the specified file or recursively deletes a directory.
|
||||||
|
*/
|
||||||
|
async fn handle_delete(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles COPY requests to duplicate resources.
|
||||||
|
* Copies a file or recursively copies a directory.
|
||||||
|
*/
|
||||||
|
async fn handle_copy(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles MOVE requests to relocate resources.
|
||||||
|
* Moves or renames a file or directory.
|
||||||
|
*/
|
||||||
|
async fn handle_move(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles LOCK requests for concurrency control.
|
||||||
|
* Locks a resource for exclusive access by a client.
|
||||||
|
*/
|
||||||
|
async fn handle_lock(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles UNLOCK requests to release locks.
|
||||||
|
* Releases a previously acquired lock on a resource.
|
||||||
|
*/
|
||||||
|
async fn handle_unlock(
|
||||||
|
// ...
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user