FileStorageService
Protocol
public protocol FileStorageService: Sendable {
func loadTags() async throws -> [Tag]
func saveTags(_ tags: [Tag]) async throws
func loadRecords(from: Date, to: Date) async throws -> [TimeRecord]
func loadRunningRecords() async throws -> [TimeRecord]
func loadAllRecords() async throws -> [TimeRecord]
func saveRecord(_ record: TimeRecord) async throws
func updateRecord(_ record: TimeRecord) async throws
func deleteRecord(_ record: TimeRecord) async throws
// Image operations
func saveImage(_ data: Data, for record: TimeRecord, filename: String) async throws
func loadImage(filename: String, for record: TimeRecord) async throws -> Data
func deleteImage(filename: String, for record: TimeRecord) async throws
func imageURL(filename: String, for record: TimeRecord) -> URL
} Implementation: LocalFileStorageService
Actor-based implementation using local filesystem at ~/Documents/Minuta/.
public actor LocalFileStorageService: FileStorageService {
private let baseURL: URL
private var recordsCache: [UUID: TimeRecord] = [:]
private var cachePopulated: Bool = false
// ...
} Directory Management
- Creates
records/YYYY/MM/directories as needed - Tags stored in single
tags.jsonfile - Records stored as individual JSON files
Record Caching
The service maintains an in-memory cache for performance:
Cache Behavior
- First Load: Cache populated when
loadRecords()orloadRunningRecords()is first called - Queries: Subsequent queries filter from cache instead of disk
- Mutations: Cache invalidated on
saveRecord(),updateRecord(),deleteRecord()
Cache Invalidation
public func invalidateCache() {
recordsCache.removeAll()
cachePopulated = false
} Benefits
- Reduces disk I/O for repeated queries (e.g., filtering by date range)
- Actor isolation ensures thread-safe cache access
- Automatic invalidation maintains consistency
Image Storage
Images are stored alongside records in the same directory structure:
- Path:
records/YYYY/MM/{record-id}_{index}.{ext} - Images are associated with records via
TimeRecord.imagesarray (filenames only) - Automatic deletion when parent record is deleted
Error Handling
public enum FileStorageError: Error, LocalizedError {
case directoryCreationFailed(Error)
case fileWriteFailed(Error)
case fileReadFailed(Error)
case fileNotFound(String)
case decodingFailed(Error)
case encodingFailed(Error)
case imageWriteFailed(Error)
case imageNotFound(String)
} Test Coverage
55 tests covering:
- Directory creation
- Tags persistence (save/load/missing file/corrupt JSON)
- Record persistence (save/load/update/delete)
- Error handling (all FileStorageError cases)
- Edge cases (special chars, unicode, long comments)
- Date range queries
- Running records
- Concurrent access
- Cache behavior
Related
Models
- 201-tag - Tag data structure
- 202-time-record - TimeRecord with images array
Services
- 303-time-tracking - Business logic layer above storage
- 302-export - Uses storage for data retrieval
- 305-storage-location - Storage folder management
UI
- 403-app-state - Calls storage via TimeTrackingService
- 406-record-editor - Image add/remove operations
Architecture
- 101-overview - System architecture