.tasks/core/FSYNC-004-service-integration.md
Wire FileSyncService into Spacedrive infrastructure - add service to CoreContext, implement API routes for UI integration, and set up event handling for real-time updates.
Goal: Make File Sync accessible from UI with full CRUD, sync triggering, and status monitoring.
// Location: core/src/service/mod.rs
pub struct Services {
// ... existing services
pub file_sync: Arc<FileSyncService>,
}
impl Services {
pub fn new(context: Arc<CoreContext>) -> Self {
// ... initialize other services
let file_sync = Arc::new(FileSyncService::new(
context.library_db(),
context.job_manager.clone(),
context.clone(),
));
Self {
// ... other services
file_sync,
}
}
}
// Location: core/src/api/sync.rs
pub fn mount() -> Router {
Router::new()
// Conduit Management
.query("listConduits", |ctx, _: ()| async move { ... })
.mutation("createConduit", |ctx, input: CreateConduitInput| async move { ... })
.mutation("updateConduit", |ctx, input: UpdateConduitInput| async move { ... })
.mutation("deleteConduit", |ctx, conduit_id: i32| async move { ... })
// Sync Operations
.mutation("syncNow", |ctx, conduit_id: i32| async move { ... })
.mutation("pauseSync", |ctx, conduit_id: i32| async move { ... })
.mutation("resumeSync", |ctx, conduit_id: i32| async move { ... })
// Status & History
.query("getSyncStatus", |ctx, conduit_id: i32| async move { ... })
.query("getSyncProgress", |ctx, conduit_id: i32| async move { ... })
.query("getSyncHistory", |ctx, input: HistoryInput| async move { ... })
.query("getConflicts", |ctx, conduit_id: i32| async move { ... })
}
API Types:
// Request/Response types
#[derive(Type, Deserialize)]
pub struct CreateConduitInput {
pub source_entry_id: i32,
pub target_entry_id: i32,
pub sync_mode: String, // "mirror" | "bidirectional" | "selective"
pub schedule: String, // "instant" | "interval:5m" | "manual"
}
#[derive(Type, Serialize)]
pub struct SyncConduitResponse {
pub id: i32,
pub uuid: String,
pub source_entry_id: i32,
pub target_entry_id: i32,
pub sync_mode: String,
pub enabled: bool,
pub last_sync_completed_at: Option<String>,
pub total_syncs: i64,
pub files_synced: i64,
pub bytes_transferred: i64,
}
#[derive(Type, Serialize)]
pub struct SyncStatusResponse {
pub conduit_id: i32,
pub is_syncing: bool,
pub current_phase: Option<String>, // "copying" | "deleting" | "verifying"
pub last_completed: Option<String>,
pub last_error: Option<String>,
}
#[derive(Type, Serialize)]
pub struct SyncProgressResponse {
pub conduit_id: i32,
pub phase: String,
pub total_files: usize,
pub completed_files: usize,
pub total_bytes: u64,
pub completed_bytes: u64,
pub copy_progress: Option<CopyProgress>,
pub delete_progress: Option<DeleteProgress>,
}
// Location: core/src/service/file_sync/events.rs
pub enum FileSyncEvent {
ConduitCreated { conduit_id: i32, uuid: Uuid },
ConduitUpdated { conduit_id: i32 },
ConduitDeleted { conduit_id: i32 },
SyncStarted { conduit_id: i32, generation: i64 },
SyncProgress { conduit_id: i32, progress: SyncProgress },
SyncCompleted { conduit_id: i32, generation: i64, stats: SyncStats },
SyncFailed { conduit_id: i32, error: String },
ConflictDetected { conduit_id: i32, conflict: SyncConflict },
VerificationComplete { conduit_id: i32, verified: bool },
}
impl FileSyncService {
fn emit_event(&self, event: FileSyncEvent) {
if let Some(event_bus) = &self.context.event_bus {
event_bus.emit("file_sync", serde_json::to_value(event).unwrap());
}
}
}
Event Emission Points:
// Location: core/src/api/mod.rs
pub fn mount() -> Router {
Router::new()
// ... existing routes
.merge("sync.", crate::api::sync::mount())
}
API Implementation:
core/src/api/sync.rs - API routes and typesEvent Integration:
core/src/service/file_sync/events.rs - Event types and emissionService Registration:
core/src/service/mod.rs - Add file_sync to Services structcore/src/api/mod.rs - Register sync routerconst conduit = await core.sync.createConduit({
sourceEntryId: 123,
targetEntryId: 456,
syncMode: "mirror",
schedule: "manual",
});
const handle = await core.sync.syncNow(conduit.id);
// Subscribe to progress
core.events.on("file_sync", (event) => {
if (event.type === "SyncProgress" && event.conduit_id === conduit.id) {
console.log(
`Progress: ${event.progress.completed_files}/${event.progress.total_files}`,
);
}
});
const status = await core.sync.getSyncStatus(conduit.id);
console.log(status.is_syncing, status.last_completed);
const progress = await core.sync.getSyncProgress(conduit.id);
console.log(progress.phase, progress.completed_bytes, progress.total_bytes);
Location Context Menu:
Sync Status Panel:
Settings: