NetMount is a unified cloud storage management and mounting application built with:
- Frontend: React + TypeScript + Vite
- Backend: Tauri (Rust)
- Dependencies: Rclone, OpenList
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (React Components in src/page) │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Controller Layer │
│ (Business logic in src/controller/*) │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ (Core business services in src/services/*) │
│ - storage/: Storage management, file operations, transfers │
│ - ConfigService.ts: Configuration management │
│ - hook/: Event hooks │
│ - rclone/: Rclone integration │
│ - openlist/: OpenList integration │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Repository Layer │
│ (Data access abstraction - Future) │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ - utils/rclone/: Rclone API calls │
│ - utils/openlist/: OpenList API calls │
│ - utils/: Utility functions │
│ - type/: TypeScript type definitions │
│ - constants/: Application constants │
└─────────────────────────────────────────────────────────────┘
The storage module has been refactored from a monolithic file into focused sub-modules:
Core storage management operations:
reupStorage()- Refresh storage list from rclone/OpenListdelStorage()- Delete a storagegetStorageParams()- Get storage configurationsearchStorage()- Find storage by namefilterHideStorage()- Filter out hidden storagesconvertStoragePath()- Convert paths between formatsgetStorageSpace()- Get storage capacity infoformatPathRclone()- Format paths for rclonegetFileName()- Extract filename from path
File operations:
getFileList()- List files in a directorydelFile()- Delete a filedelDir()- Delete a directorymkDir()- Create a directoryuploadFileRequest()- Upload files with progress
Transfer operations between storages:
copyFile()- Copy filescopyDir()- Copy directoriesmoveFile()- Move filesmoveDir()- Move directoriessync()- Sync directories (with bisync option)
Callback registration pattern to avoid circular dependencies:
registerDeleteStorage()- Register delete implementationdeleteStorage()- Call registered delete function
Encapsulates global state (nmConfig, osInfo) with:
- Controlled access via getter/setter methods
- Subscription pattern for config changes
- Type-safe configuration updates
- Callback registration pattern to avoid circular dependencies
- Persistence (load/save to disk)
Backward Compatibility: Original exports (nmConfig, osInfo) still work via getters.
The Repository layer abstracts data access operations, decoupling business logic from data sources:
- Encapsulate all Tauri invoke calls
- Provide type-safe data access
- Enable caching and retry mechanisms
- Support testing with mock repositories
- Emit data change events
Abstract base class providing:
invokeCommand<R>()- Unified Tauri invoke wrapper with retry and loggingclearCache()- Cache managementaddChangeListener()- Data change subscriptionvalidate()- Entity validation helper
Configuration data access:
getConfig()- Load full configurationsaveConfig()- Persist configurationupdatePartialConfig()- Merge partial updatesgetConfigPath()/setConfigPath()- Path-based accessgetOsInfo()- Operating system info
Storage data access:
getAll()- List all storagesgetById()- Get specific storagecreate()/update()/delete()- CRUD operationsgetStorageSpace()- Storage capacity infogetVisibleStorages()- Filter hidden storagesrefreshStorageList()- Sync from Rclone/OpenList
import { configRepository, storageRepository } from '@/repositories'
// Get configuration
const config = await configRepository.getConfig()
// Update configuration path
await configRepository.setConfigPath('settings.themeMode', 'dark')
// List storages
const storages = await storageRepository.getAll()
// Create storage
await storageRepository.create({
name: 'my-storage',
type: 's3',
framework: 'rclone'
})
// Subscribe to data changes
const unsubscribe = storageRepository.addChangeListener((event) => {
console.log(`Storage ${event.id} ${event.type}`)
})- Testability: Mock repositories in tests without touching Tauri
- Decoupling: Business logic isolated from data sources
- Caching: Automatic caching with configurable timeout
- Logging: Unified logging via LoggerService
- Retry: Automatic retry on transient failures
- Events: React to data changes in real-time
Refactored into focused sub-modules:
formatSize()- Format file sizesformatETA()- Format time estimatesformatPath()- Normalize paths
randomString()- Generate random stringstakeMidStr()/takeRightStr()- String extractioncompareVersions()- Version comparison
downloadFile()- File downloadsgetWinFspInstallState()/installWinFsp()/openWinFspInstaller()- WinFsp managementopenUrlInBrowser()- Open external URLsshowPathInExplorer()- Open file explorerfs_exist_dir()/fs_make_dir()- Directory operations
set_devtools_state()- Toggle devtoolsrestartSelf()- Restart applicationsleep()- Async delaygetAvailablePorts()- Port availability
isEmptyObject()- Object emptiness checkgetURLSearchParam()- URL parsinggetProperties()- Object property extractionmergeObjects()- Deep object merging
Centralized application constants:
THEME_MODES/LANGUAGE_OPTIONS- UI optionsAPI_TIMEOUT- Request timeout settingsRETRY_CONFIG- Retry logic configurationWATCHDOG_CONFIG- Health check settingsSTATS_CONFIG- Statistics refresh intervalsUPDATE_CONFIG- Update check intervalsDEFAULT_LANGUAGE/FALLBACK_LANGUAGE- i18n defaults
Problem: utils/rclone/process.ts imported delStorage from controller/storage/storage.ts, creating a circular dependency.
Solution: Callback registration pattern via StorageService.ts:
// utils/rclone/process.ts
import { deleteStorage } from '@/services/storage/StorageService'
await deleteStorage(name) // calls registered implementation
// controller/storage/storage.ts
import { registerDeleteStorage } from '@/services/storage/StorageService'
registerDeleteStorage(delStorage)Problem: nmConfig, rcloneInfo, openlistInfo were exported as mutable objects, allowing any module to modify them.
Solution: ConfigService class with subscription pattern:
// New pattern (recommended)
import { configService } from '@/services/ConfigService'
configService.updateConfig({ settings: { themeMode: 'dark' }})
const unsubscribe = configService.subscribeConfig((new, old) => { ... })
// Backward compatible (still works)
import { nmConfig } from '@/services/config'
nmConfig.settings.themeMode = 'dark' // via getterPattern: New modules export from index, old files re-export for backward compatibility:
// src/services/storage/index.ts - New structure
export { reupStorage, delStorage, ... } from './StorageManager'
export { getFileList, delFile, ... } from './FileManager'
export { copyFile, moveFile, ... } from './TransferService'
// src/controller/storage/storage.ts - Backward compatibility
export * from '../../services/storage'Use direct imports to avoid circular dependency warnings:
// ✅ Good - Direct import from specific module
import { filterHideStorage } from '@/services/storage/StorageManager'
import { getFileList } from '@/services/storage/FileManager'
import { copyFile } from '@/services/storage/TransferService'
// ⚠️ Avoid - Index imports can cause circular dependency warnings in some cases
import { filterHideStorage, getFileList } from '@/services/storage'// Old path - maintained for backward compatibility
import { filterHideStorage } from '@/controller/storage/storage'- ✅ Deleted backup file (
storage.ts.bak) - ✅ Fixed circular dependencies (utils/rclone/process.ts ↔ controller/storage)
- ✅ Merged duplicate constants files
- ✅ Extracted magic numbers to named constants
- ✅ Created ConfigService for state encapsulation
- ✅ Split storage.ts into 3 focused modules
- ✅ Split utils.ts into 6 focused modules
- ✅ Updated imports to use new modular paths
- ✅ Migrated all console.log/warn/error to unified LoggerService
- ✅ Repository layer fully implemented (BaseRepository, ConfigRepository, StorageRepository, MountRepository, TaskRepository)
- ✅ Repository layer for data access abstraction (已完成)
- ✅ Migrate console logs to unified LoggerService (已完成)
- ✅ Update remaining code to use new patterns (已完成)
- ✅ Added comprehensive unit tests for StorageManager
- ✅ Added comprehensive unit tests for FileManager
- ✅ Added comprehensive unit tests for TransferService
- ✅ Added unit tests for StorageRepository
- ✅ Added unit tests for ConfigRepository
- ✅ Added unit tests for TaskRepository
- ✅ Added unit tests for MountRepository
- ✅ Added unit tests for ErrorService
- ✅ Added integration tests for storage operations
- ✅ Performance optimization guidance documented
- ✅ Code quality checks implemented
- ✅ Module size validation (<300 lines)
- ✅ Use named constants instead of magic numbers
- ✅ Import from specific modules for new code
- ✅ Use ConfigService for configuration access
- ✅ Keep modules focused and under 300 lines
- ✅ Use TypeScript strict mode
- ✅ Add JSDoc comments for public APIs
- ❌ Import mutable state objects directly
- ❌ Create new circular dependencies
- ❌ Mix concerns in a single module
- ❌ Use
anytype - ❌ Suppress TypeScript errors with
@ts-ignore
- Test each repository method in isolation
- Mock API calls
- Test edge cases and error handling
- Test component interactions
- Test storage operations end-to-end
- Test configuration persistence
- Import from
@/services/storage/*directly - Use
ConfigServicefor configuration - Follow the module boundaries
- No immediate changes required
- Gradual migration encouraged
- Old imports continue to work