mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
416 lines
11 KiB
Plaintext
416 lines
11 KiB
Plaintext
---
|
|
title: Devices
|
|
sidebarTitle: Devices
|
|
---
|
|
|
|
Devices are machines running Spacedrive. Each device has a unique identity, can pair with others, and participates in library synchronization.
|
|
|
|
## Architecture
|
|
|
|
Devices operate across three layers:
|
|
|
|
### Identity Layer
|
|
|
|
Manages device configuration and cryptographic keys.
|
|
|
|
```rust
|
|
// Device initialization
|
|
let device_manager = DeviceManager::init()?;
|
|
let device_id = device_manager.device_id()?;
|
|
```
|
|
|
|
**Storage locations**:
|
|
|
|
- macOS: `~/Library/Application Support/com.spacedrive/device.json`
|
|
- Linux: `~/.config/spacedrive/device.json`
|
|
- Windows: `%APPDATA%/Spacedrive/device.json`
|
|
|
|
### Domain Layer
|
|
|
|
Provides the rich device model used throughout the application.
|
|
|
|
```rust
|
|
pub struct Device {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub slug: String, // URL-safe identifier for addressing
|
|
pub os: OperatingSystem,
|
|
pub hardware_model: Option<String>,
|
|
pub network_addresses: Vec<String>,
|
|
pub is_online: bool,
|
|
pub last_seen_at: DateTime<Utc>,
|
|
}
|
|
```
|
|
|
|
Devices are stored per library, not globally. Each library database contains device records for all participating devices.
|
|
|
|
<Info>
|
|
The `devices` table is the **source of truth** for sync state within a
|
|
library. It tracks which devices are online, when they were last seen, and
|
|
their sync watermarks.
|
|
</Info>
|
|
|
|
### Device Slugs for Unified Addressing
|
|
|
|
Each device has a unique slug used in Spacedrive's unified addressing scheme. The slug is a URL-safe identifier generated from the device name:
|
|
|
|
```rust
|
|
// Slug generation
|
|
let name = "Jamie's MacBook Pro";
|
|
let slug = name.to_lowercase()
|
|
.chars()
|
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
|
.collect::<String>()
|
|
.trim_matches('-');
|
|
// Result: "jamies-macbook-pro"
|
|
```
|
|
|
|
Slugs enable human-readable local file URIs:
|
|
|
|
```
|
|
local://jamies-macbook/Users/james/Documents/report.pdf
|
|
local://home-server/mnt/storage/media/movies/Inception.mkv
|
|
local://work-desktop/C:/Projects/spacedrive/README.md
|
|
```
|
|
|
|
The database enforces slug uniqueness with a UNIQUE constraint. If two devices would have the same slug (e.g., both named "MacBook Pro"), one must be renamed before they can sync.
|
|
|
|
See [Unified Addressing](/docs/core/addressing) for complete details on URI formats and slug resolution.
|
|
|
|
### Network Layer
|
|
|
|
Handles P2P connections and device pairing through Iroh.
|
|
|
|
<Note>
|
|
The network layer uses mDNS for local discovery and QUIC for encrypted
|
|
communication.
|
|
</Note>
|
|
|
|
## Device Pairing
|
|
|
|
Pairing establishes trust between devices using a cryptographic handshake.
|
|
|
|
### Pairing Flow
|
|
|
|
<Steps>
|
|
<Step title="Generate Code">
|
|
Device A generates a pairing code valid for 5 minutes.
|
|
```typescript
|
|
const pairing = await client.action("network.pair.generate", {});
|
|
console.log(`Pairing code: ${pairing.code}`); // "ABCD-1234-EFGH"
|
|
```
|
|
</Step>
|
|
|
|
<Step title="Enter Code">
|
|
Device B joins using the pairing code.
|
|
```typescript
|
|
await client.action("network.pair.join", {
|
|
code: "ABCD-1234-EFGH"
|
|
});
|
|
```
|
|
</Step>
|
|
|
|
<Step title="Establish Trust">
|
|
Devices exchange cryptographic signatures and derive session keys for future communication.
|
|
</Step>
|
|
</Steps>
|
|
|
|
### Paired Device States
|
|
|
|
- **Discovered**: Found via mDNS but not paired
|
|
- **Pairing**: Authentication in progress
|
|
- **Paired**: Trusted and persisted
|
|
- **Connected**: Active P2P connection
|
|
- **Disconnected**: Paired but offline
|
|
|
|
## Library Participation
|
|
|
|
After pairing, devices must register with libraries to enable sync.
|
|
|
|
### Registration Process
|
|
|
|
1. Paired devices discover each other's libraries
|
|
2. User selects which libraries to join
|
|
3. Device records are created in each library's database
|
|
4. Sync begins automatically
|
|
|
|
### Device-Owned Data
|
|
|
|
Each device owns specific data that only it can modify:
|
|
|
|
- **Locations**: Filesystem paths on that device
|
|
- **Entries**: Files and folders within those locations
|
|
- **Volumes**: Physical drives attached to the device
|
|
|
|
This ownership model is fundamental to Spacedrive's conflict-free sync design.
|
|
|
|
## Sync Participation
|
|
|
|
<Info>
|
|
Spacedrive uses a leaderless sync model. All devices are peers with no central
|
|
authority.
|
|
</Info>
|
|
|
|
The `devices` table is the **source of truth** for all sync state within a library:
|
|
|
|
- **Connection State**: `is_online`, `last_seen_at` track real-time availability
|
|
- **Sync Enablement**: `sync_enabled` controls whether a device participates
|
|
- **Watermarks**: Track what data has been synchronized
|
|
- `last_state_watermark`: Timestamp for device-owned data (locations, entries)
|
|
- `last_shared_watermark`: HLC for shared resources (tags, albums)
|
|
|
|
### How Devices Participate in Sync
|
|
|
|
Devices sync data using two protocols based on ownership:
|
|
|
|
- **Device-owned data** (locations, entries): Owner broadcasts state, peers apply. Tracked via `last_state_watermark`.
|
|
- **Shared resources** (tags, collections): Any device can modify. Changes ordered via HLC. Tracked via `last_shared_watermark`.
|
|
|
|
For detailed protocol documentation, see [Library Sync](/docs/core/library-sync).
|
|
|
|
### Sync State Management
|
|
|
|
Query sync-enabled devices in a library:
|
|
|
|
```rust
|
|
// Get all devices that can sync
|
|
let sync_devices = entities::device::Entity::find()
|
|
.filter(entities::device::Column::SyncEnabled.eq(true))
|
|
.all(db)
|
|
.await?;
|
|
|
|
// Get only online devices ready for immediate sync
|
|
let online_devices = entities::device::Entity::find()
|
|
.filter(entities::device::Column::SyncEnabled.eq(true))
|
|
.filter(entities::device::Column::IsOnline.eq(true))
|
|
.all(db)
|
|
.await?;
|
|
```
|
|
|
|
### Watermark Tracking
|
|
|
|
Watermarks enable incremental sync by tracking the last successfully synchronized state:
|
|
|
|
```rust
|
|
// Get device's sync watermarks
|
|
let device = entities::device::Entity::find()
|
|
.filter(entities::device::Column::Uuid.eq(device_id))
|
|
.one(db)
|
|
.await?;
|
|
|
|
if let Some(d) = device {
|
|
// State watermark: last timestamp synced for device-owned data
|
|
let state_watermark: Option<DateTime<Utc>> = d.last_state_watermark;
|
|
|
|
// Shared watermark: last HLC synced for shared resources
|
|
let shared_watermark: Option<HLC> = d.last_shared_watermark
|
|
.as_ref()
|
|
.and_then(|s| serde_json::from_str(s).ok());
|
|
}
|
|
```
|
|
|
|
When devices reconnect after being offline, they compare watermarks to determine what data needs to be synchronized incrementally, avoiding full re-sync.
|
|
|
|
## API Reference
|
|
|
|
### List Paired Devices
|
|
|
|
```typescript
|
|
const devices = await client.query("network.devices.list", {
|
|
connectedOnly: false
|
|
});
|
|
|
|
// Response
|
|
{
|
|
devices: [
|
|
{
|
|
id: "device-uuid",
|
|
name: "Jamie's MacBook",
|
|
deviceType: "Laptop",
|
|
isConnected: true,
|
|
lastSeen: "2024-10-12T..."
|
|
}
|
|
],
|
|
total: 3,
|
|
connected: 2
|
|
}
|
|
```
|
|
|
|
### Device Events
|
|
|
|
Devices emit events when their state changes:
|
|
|
|
```typescript
|
|
// Device comes online
|
|
{
|
|
kind: "ResourceChanged",
|
|
resourceType: "device",
|
|
resource: { id: "...", isOnline: true }
|
|
}
|
|
|
|
// Device goes offline
|
|
{
|
|
kind: "ResourceChanged",
|
|
resourceType: "device",
|
|
resource: { id: "...", isOnline: false }
|
|
}
|
|
```
|
|
|
|
## Platform Considerations
|
|
|
|
### Mobile Devices
|
|
|
|
iOS and Android require special initialization:
|
|
|
|
```swift
|
|
// iOS: Pass UIDevice name to Rust
|
|
let deviceName = UIDevice.current.name
|
|
core.initializeDevice(withName: deviceName)
|
|
```
|
|
|
|
### Device Types
|
|
|
|
- **Desktop**: Windows, Linux desktops
|
|
- **Laptop**: MacBooks, portable computers
|
|
- **Mobile**: iOS, Android devices
|
|
- **Server**: Headless installations
|
|
|
|
## Security
|
|
|
|
Each device maintains:
|
|
|
|
- **Ed25519 key pair**: Unique cryptographic identity
|
|
- **Session keys**: Derived after pairing for encrypted communication
|
|
- **Trust levels**: Verified (paired) or blocked
|
|
|
|
<Warning>
|
|
Never share device keys or pairing codes over insecure channels.
|
|
</Warning>
|
|
|
|
## Troubleshooting
|
|
|
|
### Pairing Issues
|
|
|
|
If devices won't pair:
|
|
|
|
- Verify both devices are on the same network
|
|
- Check firewall settings for mDNS (port 5353)
|
|
- Ensure pairing code hasn't expired (5 minute timeout)
|
|
- Restart the application on both devices
|
|
|
|
### Sync Not Working
|
|
|
|
If changes aren't syncing between devices:
|
|
|
|
- Confirm devices are paired (check paired device list)
|
|
- Verify both devices have joined the library
|
|
- Check `devices.is_online` status in the library database
|
|
- Verify `devices.sync_enabled = 1` for both devices
|
|
- Compare watermarks to check if sync is progressing
|
|
- Check network connectivity between devices
|
|
- Review sync status in the UI
|
|
|
|
### Debug Commands
|
|
|
|
```bash
|
|
# Check device registration and sync state in library
|
|
sqlite3 ~/Spacedrive/Libraries/My\ Library.sdlibrary/database.db \
|
|
"SELECT uuid, name, is_online, sync_enabled, last_sync_at,
|
|
last_state_watermark, last_shared_watermark
|
|
FROM devices;"
|
|
|
|
# Check which devices are online and sync-enabled
|
|
sqlite3 ~/Spacedrive/Libraries/My\ Library.sdlibrary/database.db \
|
|
"SELECT uuid, name, is_online, last_seen_at
|
|
FROM devices
|
|
WHERE sync_enabled = 1;"
|
|
|
|
# View watermarks for a specific device
|
|
sqlite3 ~/Spacedrive/Libraries/My\ Library.sdlibrary/database.db \
|
|
"SELECT name, last_state_watermark, last_shared_watermark
|
|
FROM devices
|
|
WHERE uuid='device-uuid';"
|
|
|
|
# Monitor sync activity
|
|
RUST_LOG=sd_core::sync=debug,sd_core::service::sync=debug cargo run
|
|
```
|
|
|
|
## Implementation Details
|
|
|
|
### Global Device ID Cache
|
|
|
|
For performance, the current device ID is cached globally:
|
|
|
|
```rust
|
|
use sd_core::device::get_current_device_id;
|
|
|
|
let device_id = get_current_device_id(); // Fast lookup
|
|
```
|
|
|
|
### Device Registration Query
|
|
|
|
```sql
|
|
-- Each library database contains
|
|
CREATE TABLE devices (
|
|
id INTEGER PRIMARY KEY,
|
|
uuid TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
slug TEXT UNIQUE NOT NULL, -- URL-safe identifier for unified addressing
|
|
os TEXT NOT NULL,
|
|
os_version TEXT,
|
|
hardware_model TEXT,
|
|
network_addresses TEXT, -- JSON array
|
|
is_online BOOLEAN DEFAULT 0,
|
|
last_seen_at TEXT NOT NULL,
|
|
|
|
-- Sync management fields
|
|
sync_enabled BOOLEAN DEFAULT 1,
|
|
last_sync_at TEXT,
|
|
last_state_watermark TEXT, -- Timestamp for device-owned data
|
|
last_shared_watermark TEXT, -- HLC for shared resources (JSON)
|
|
|
|
capabilities TEXT, -- JSON object
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
### Connection State Management
|
|
|
|
The sync system subscribes to network connection events and updates the `devices` table automatically:
|
|
|
|
```rust
|
|
// When a device connects (network event)
|
|
entities::device::Entity::update_many()
|
|
.col_expr(entities::device::Column::IsOnline, Expr::value(true))
|
|
.col_expr(entities::device::Column::LastSeenAt, Expr::value(Utc::now()))
|
|
.filter(entities::device::Column::Uuid.eq(peer_id))
|
|
.exec(db)
|
|
.await?;
|
|
|
|
// When a device disconnects (network event)
|
|
entities::device::Entity::update_many()
|
|
.col_expr(entities::device::Column::IsOnline, Expr::value(false))
|
|
.col_expr(entities::device::Column::LastSeenAt, Expr::value(Utc::now()))
|
|
.filter(entities::device::Column::Uuid.eq(peer_id))
|
|
.exec(db)
|
|
.await?;
|
|
```
|
|
|
|
The `devices` table is the single source of truth - the network layer updates it, and the sync layer queries it to determine which peers to sync with.
|
|
|
|
You can also check real-time connection status via the DeviceRegistry:
|
|
|
|
```rust
|
|
// Get current connection status (in-memory state)
|
|
let networking = context.get_networking().await?;
|
|
let registry = networking.device_registry();
|
|
let is_connected = registry.is_device_connected(device_id);
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Sync System](/docs/core/sync) - How devices synchronize data
|
|
- [Libraries](/docs/core/libraries) - Multi-device library management
|
|
- [Networking](/docs/core/networking) - P2P connection details
|