mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-08-07 04:32:12 -04:00
Add Spacedrive server with embedded daemon
- Introduce an Axum-based HTTP server with an embedded daemon and a JSON-RPC proxy to the daemon via a Unix socket - Bundle web UI assets into the server with an assets feature and a build.rs that builds the frontend using pnpm - Add multi-stage Dockerfile, docker-compose.yml, and a Distroless runtime image - Provide TrueNAS deployment support with a build script and setup guide - Add a new web UI (apps/web) with a Vite-based dev/build flow and a web platform shim for the frontend - Implement server logic (apps/server/src/main.rs): health, auth, /rpc proxy and data-dir/socket-path wiring - Include server-specific Cargo.toml and a comprehensive server README - Add architecture and memory-focused docs to guide usage and design - Minor core tweak: simplify location/resource event emission in core/src/location/manager.rs to align with new flow - Tauri app: adjust menus to add an Edit submenu and remove unused items
This commit is contained in:
1 parent
8a3387ca69
commit
bcab31462e
29 files changed
+2693
-187
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Build artifacts
|
||||
target/
|
||||
**/target/
|
||||
dist/
|
||||
**/dist/
|
||||
build/
|
||||
**/build/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Test files
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Documentation (not needed in container)
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
|
||||
# Development files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Rust
|
||||
**/*.rs.bk
|
||||
Cargo.lock.orig
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Linux
|
||||
*~
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
@@ -0,0 +1,380 @@
|
||||
# Spacedrive Server Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The Spacedrive Server is a production-ready HTTP server that embeds the Spacedrive daemon and serves the web interface. It's designed for headless deployments, NAS systems, and container environments.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Embedded Daemon** - No separate process management needed
|
||||
2. **Single Binary** - Web assets bundled via `include_dir` when built with `--features assets`
|
||||
3. **Platform Abstraction** - Uses same `@sd/interface` as Tauri, with web-specific platform impl
|
||||
4. **Security First** - HTTP Basic Auth for all endpoints (except health check)
|
||||
5. **Container Native** - Docker-first design with distroless runtime image
|
||||
|
||||
## Components
|
||||
|
||||
### 1. HTTP Server (`apps/server/src/main.rs`)
|
||||
|
||||
Built with Axum, provides:
|
||||
- **`GET /health`** - Health check (no auth required)
|
||||
- **`POST /rpc`** - JSON-RPC proxy to daemon Unix socket
|
||||
- **`GET /*`** - Static asset serving (SPA fallback to index.html)
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
Browser → HTTP Request → Axum Router → Basic Auth Middleware → Handler
|
||||
↓
|
||||
┌─────────────────────────┴────────┐
|
||||
↓ ↓
|
||||
Static Assets RPC Proxy
|
||||
(serve from ↓
|
||||
ASSETS_DIR) Unix Socket → Daemon
|
||||
```
|
||||
|
||||
### 2. Embedded Daemon
|
||||
|
||||
Unlike Tauri (which spawns `sd-daemon` as a child process), the server runs the daemon in-process:
|
||||
|
||||
```rust
|
||||
tokio::spawn(async move {
|
||||
sd_core::infra::daemon::bootstrap::start_default_server(
|
||||
socket_path,
|
||||
data_dir,
|
||||
enable_p2p,
|
||||
).await
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Single container image
|
||||
- Simplified lifecycle management
|
||||
- Shared memory space (more efficient)
|
||||
- Graceful shutdown via `tokio::select!`
|
||||
|
||||
**Daemon lifecycle:**
|
||||
1. Check if socket already exists (reuse existing daemon)
|
||||
2. If not, spawn daemon in background task
|
||||
3. Wait for socket file to appear (max 3s)
|
||||
4. Return handle for graceful shutdown
|
||||
|
||||
### 3. Web Client (`apps/web/`)
|
||||
|
||||
Minimal React app using `@sd/interface`:
|
||||
|
||||
```tsx
|
||||
// apps/web/src/main.tsx
|
||||
<PlatformProvider platform={webPlatform}>
|
||||
<Explorer />
|
||||
</PlatformProvider>
|
||||
```
|
||||
|
||||
**Platform implementation:**
|
||||
```typescript
|
||||
// apps/web/src/platform.ts
|
||||
export const platform: Platform = {
|
||||
platform: "web",
|
||||
openLink(url) { window.open(url) },
|
||||
confirm(msg, cb) { cb(window.confirm(msg)) },
|
||||
// No native file pickers, daemon control, etc.
|
||||
};
|
||||
```
|
||||
|
||||
**Build process:**
|
||||
1. Vite bundles React app → `apps/web/dist/`
|
||||
2. `build.rs` runs `pnpm build` before compiling server
|
||||
3. `include_dir!` macro embeds `dist/` into binary at compile time
|
||||
4. Axum serves embedded files from memory
|
||||
|
||||
### 4. RPC Proxy
|
||||
|
||||
Browsers can't connect to Unix sockets, so the server proxies:
|
||||
|
||||
```
|
||||
Browser Server Daemon
|
||||
│ │ │
|
||||
│ POST /rpc │ │
|
||||
├────────────────────>│ │
|
||||
│ (JSON-RPC) │ Unix Socket Write │
|
||||
│ ├────────────────────────>│
|
||||
│ │ │
|
||||
│ │ Unix Socket Read │
|
||||
│ │<────────────────────────┤
|
||||
│ 200 OK │ │
|
||||
│<────────────────────┤ │
|
||||
│ (JSON-RPC result) │ │
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```rust
|
||||
async fn daemon_rpc(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let mut stream = UnixStream::connect(&state.socket_path).await?;
|
||||
stream.write_all(format!("{}\n", serde_json::to_string(&payload)?).as_bytes()).await?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response = String::new();
|
||||
reader.read_line(&mut response).await?;
|
||||
|
||||
Ok(Json(serde_json::from_str(&response)?))
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison: Server vs Tauri vs CLI
|
||||
|
||||
| Aspect | Server | Tauri | CLI |
|
||||
|--------|--------|-------|-----|
|
||||
| **Process Model** | Embedded daemon | Spawned daemon | Connects to daemon |
|
||||
| **UI** | Web (React in browser) | WebView (React) | Terminal (TUI) |
|
||||
| **Daemon Communication** | Unix socket (proxied) | Unix socket (direct) | Unix socket (direct) |
|
||||
| **Platform Abstraction** | `platform: "web"` | `platform: "tauri"` | N/A |
|
||||
| **Access Model** | Remote (HTTP) | Local only | Local only |
|
||||
| **Auth** | HTTP Basic Auth | Not needed | Not needed |
|
||||
| **Deployment** | Docker, systemd | App bundle | Binary |
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
```
|
||||
1. Browser makes request without auth
|
||||
↓
|
||||
2. basic_auth middleware checks state.auth
|
||||
↓
|
||||
3. If empty → allow (auth disabled)
|
||||
If populated → require Basic Auth header
|
||||
↓
|
||||
4. Extract credentials from Authorization header
|
||||
↓
|
||||
5. Compare with state.auth HashMap
|
||||
↓
|
||||
6. Match → proceed to handler
|
||||
No match → 401 Unauthorized
|
||||
```
|
||||
|
||||
**Security considerations:**
|
||||
- Credentials stored in memory as `SecStr` (zeroed on drop)
|
||||
- Basic Auth over HTTPS recommended for production
|
||||
- Socket file has filesystem permissions (only accessible to server user)
|
||||
|
||||
## Docker Architecture
|
||||
|
||||
### Multi-stage Build
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Builder (Debian + Rust + Node)
|
||||
FROM debian:bookworm-slim AS builder
|
||||
RUN install Rust, Node, pnpm
|
||||
COPY workspace
|
||||
RUN pnpm build (web)
|
||||
RUN cargo build --release --features assets
|
||||
|
||||
# Stage 2: Runtime (Distroless)
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
COPY --from=builder /build/target/release/sd-server
|
||||
ENTRYPOINT ["/usr/bin/sd-server"]
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Small image** - Distroless base (~50MB vs ~1GB for full Debian)
|
||||
- **Secure** - No shell, no package manager, minimal attack surface
|
||||
- **Fast** - Cached layers for dependencies
|
||||
- **Reproducible** - Locked versions via `Cargo.lock` and `pnpm-lock.yaml`
|
||||
|
||||
### Volume Mounts
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- spacedrive-data:/data # Persistent library data
|
||||
- /mnt/storage:/storage:ro # Optional: Read-only media access
|
||||
```
|
||||
|
||||
**Data layout:**
|
||||
```
|
||||
/data/
|
||||
├── daemon/
|
||||
│ └── daemon.sock # Unix socket for RPC
|
||||
├── libraries/
|
||||
│ └── *.sdlibrary/ # SQLite databases
|
||||
│ ├── library.db
|
||||
│ └── sidecars/ # Thumbnails, previews
|
||||
├── logs/
|
||||
│ ├── daemon.log
|
||||
│ └── indexing.log
|
||||
└── current_library_id.txt
|
||||
```
|
||||
|
||||
## Development vs Production
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
# Terminal 1: Web dev server (hot reload)
|
||||
cd apps/web
|
||||
pnpm dev # → http://localhost:3000
|
||||
|
||||
# Terminal 2: API server
|
||||
cargo run -p sd-server
|
||||
# → http://localhost:8080
|
||||
# Vite proxies /rpc to 8080
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
1. Edit React components → Vite hot reloads
|
||||
2. Edit server code → `cargo run` rebuilds
|
||||
3. No need to rebuild web assets during development
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
# Build with bundled assets
|
||||
cargo build --release -p sd-server --features assets
|
||||
|
||||
# Single binary contains:
|
||||
# - Axum HTTP server
|
||||
# - Embedded daemon
|
||||
# - Bundled web UI (React app)
|
||||
```
|
||||
|
||||
**Deployment:**
|
||||
```bash
|
||||
./target/release/sd-server \
|
||||
--data-dir /var/lib/spacedrive \
|
||||
--port 8080
|
||||
```
|
||||
|
||||
## Platform Abstraction
|
||||
|
||||
Both Tauri and Web use `@sd/interface`, but with different platform implementations:
|
||||
|
||||
### Tauri Platform (`apps/tauri/src/platform.ts`)
|
||||
|
||||
```typescript
|
||||
{
|
||||
platform: "tauri",
|
||||
openDirectoryPickerDialog: async () => open({ directory: true }),
|
||||
revealFile: async (path) => invoke("reveal_file", { path }),
|
||||
getCurrentLibraryId: async () => invoke("get_current_library_id"),
|
||||
getDaemonStatus: async () => invoke("get_daemon_status"),
|
||||
// Full native capabilities
|
||||
}
|
||||
```
|
||||
|
||||
### Web Platform (`apps/web/src/platform.ts`)
|
||||
|
||||
```typescript
|
||||
{
|
||||
platform: "web",
|
||||
openLink: (url) => window.open(url),
|
||||
confirm: (msg, cb) => cb(window.confirm(msg)),
|
||||
// Minimal browser-only capabilities
|
||||
}
|
||||
```
|
||||
|
||||
**Interface components adapt:**
|
||||
```tsx
|
||||
function FilePickerButton() {
|
||||
const platform = usePlatform();
|
||||
|
||||
if (platform.platform === "tauri") {
|
||||
// Show native picker button
|
||||
return <button onClick={platform.openDirectoryPickerDialog}>Pick</button>;
|
||||
} else {
|
||||
// Web: no native picker, show manual path input
|
||||
return <input type="text" placeholder="Enter path..." />;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Errors
|
||||
|
||||
```rust
|
||||
async fn daemon_rpc(...) -> Result<Json<Value>, (StatusCode, String)> {
|
||||
let stream = UnixStream::connect(&socket_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::SERVICE_UNAVAILABLE, format!("Daemon not available: {}", e)))?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Responses:**
|
||||
- `503 Service Unavailable` - Daemon not running
|
||||
- `400 Bad Request` - Invalid JSON
|
||||
- `500 Internal Server Error` - RPC failed
|
||||
- `401 Unauthorized` - Auth failed
|
||||
|
||||
### Daemon Errors
|
||||
|
||||
Daemon errors are passed through RPC response:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": "Library not found"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Static Assets** - Served from memory (embedded via `include_dir`)
|
||||
2. **Socket Pooling** - Each RPC request opens new socket (TODO: connection pool)
|
||||
3. **Async I/O** - Tokio runtime handles concurrent requests
|
||||
4. **Graceful Shutdown** - Waits for in-flight requests before terminating
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **WebSocket Support** - Real-time event streaming (vs polling)
|
||||
2. **HTTPS** - TLS termination (currently expects reverse proxy)
|
||||
3. **Connection Pool** - Reuse Unix sockets for RPC
|
||||
4. **Multi-tenancy** - Separate libraries per user
|
||||
5. **SSE Events** - Server-sent events for daemon notifications
|
||||
|
||||
## Security Model
|
||||
|
||||
**Trust Boundaries:**
|
||||
```
|
||||
Internet ←[TLS]→ Reverse Proxy ←[HTTP+Auth]→ Server ←[Unix Socket]→ Daemon
|
||||
(❌) (✅) (✅) (✅) (✅)
|
||||
```
|
||||
|
||||
**Assumptions:**
|
||||
- Server runs on trusted network OR behind reverse proxy with TLS
|
||||
- Unix socket accessible only to server process (filesystem permissions)
|
||||
- HTTP Basic Auth sufficient for home/NAS use
|
||||
- For public internet: Use nginx/Caddy with Let's Encrypt
|
||||
|
||||
## Monitoring
|
||||
|
||||
**Health Check:**
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
# → "OK"
|
||||
```
|
||||
|
||||
**Logs:**
|
||||
```bash
|
||||
# Docker
|
||||
docker logs spacedrive -f
|
||||
|
||||
# Systemd
|
||||
journalctl -u spacedrive -f
|
||||
|
||||
# Native
|
||||
RUST_LOG=debug ./sd-server
|
||||
```
|
||||
|
||||
**Metrics:** (TODO)
|
||||
- Request count/latency
|
||||
- Daemon socket errors
|
||||
- Active connections
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [README.md](./README.md) - Setup and usage
|
||||
- [../../docs/core/architecture.md](../../docs/core/architecture.md) - Core VDFS design
|
||||
- [../tauri/DAEMON_SETUP.md](../tauri/DAEMON_SETUP.md) - Tauri daemon integration
|
||||
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "sd-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Include bundled web assets (production builds)
|
||||
assets = []
|
||||
|
||||
[dependencies]
|
||||
# Spacedrive core
|
||||
sd-core = { path = "../../core", features = ["ffmpeg", "heif"] }
|
||||
|
||||
# HTTP server
|
||||
axum = "0.7"
|
||||
axum-extra = { version = "0.9", features = ["typed-header"] }
|
||||
http = "1.1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "signal", "sync", "io-util"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["fs", "cors"] }
|
||||
|
||||
# Auth
|
||||
secstr = "0.5"
|
||||
|
||||
# Static assets
|
||||
include_dir = "0.7"
|
||||
mime_guess = "2.0"
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
|
||||
# CLI
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# Dev dependencies
|
||||
tempfile = "3"
|
||||
|
||||
[[bin]]
|
||||
name = "sd-server"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,100 @@
|
||||
# Spacedrive Server Docker Image
|
||||
# Multi-stage build for minimal production image
|
||||
|
||||
ARG RUST_VERSION=1.83
|
||||
ARG NODE_VERSION=22
|
||||
|
||||
#--
|
||||
# Base image with common dependencies
|
||||
#--
|
||||
FROM debian:bookworm-slim AS base
|
||||
|
||||
# Configure apt for non-interactive use and caching
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
|
||||
apt-get update && apt-get upgrade -y
|
||||
|
||||
#--
|
||||
# Build environment
|
||||
#--
|
||||
FROM base AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
git \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
ca-certificates
|
||||
|
||||
# Install Rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
|
||||
# Install Node.js and pnpm
|
||||
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y nodejs
|
||||
RUN npm install -g pnpm@latest
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy workspace configuration
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY .cargo ./.cargo
|
||||
|
||||
# Copy source code
|
||||
COPY core ./core
|
||||
COPY crates ./crates
|
||||
COPY apps/server ./apps/server
|
||||
COPY apps/web ./apps/web
|
||||
COPY packages ./packages
|
||||
|
||||
# Build web assets first
|
||||
WORKDIR /build/apps/web
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
# Build server with embedded assets
|
||||
WORKDIR /build
|
||||
RUN --mount=type=cache,target=/root/.cargo/registry \
|
||||
--mount=type=cache,target=/root/.cargo/git \
|
||||
--mount=type=cache,target=/build/target \
|
||||
cargo build --release --features assets -p sd-server && \
|
||||
cp target/release/sd-server /usr/local/bin/sd-server
|
||||
|
||||
#--
|
||||
# Runtime image
|
||||
#--
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
|
||||
# Copy binary
|
||||
COPY --from=builder /usr/local/bin/sd-server /usr/bin/sd-server
|
||||
|
||||
# Environment
|
||||
ENV DATA_DIR=/data \
|
||||
PORT=8080 \
|
||||
RUST_LOG=info,sd_core=debug
|
||||
|
||||
# Expose HTTP port
|
||||
EXPOSE 8080
|
||||
|
||||
# Expose P2P port
|
||||
EXPOSE 7373
|
||||
|
||||
# Volume for persistent data
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Run as non-root user
|
||||
USER nonroot:nonroot
|
||||
|
||||
# Start server
|
||||
ENTRYPOINT ["/usr/bin/sd-server"]
|
||||
CMD ["--data-dir", "/data"]
|
||||
@@ -0,0 +1,319 @@
|
||||
# Spacedrive Server
|
||||
|
||||
HTTP server for Spacedrive with embedded daemon and web interface.
|
||||
|
||||
## Overview
|
||||
|
||||
`sd-server` runs the Spacedrive daemon and serves a web interface over HTTP. Perfect for:
|
||||
- **NAS deployments** (TrueNAS, Unraid, Synology, etc.)
|
||||
- **Headless servers**
|
||||
- **Remote access** to your Spacedrive libraries
|
||||
- **Docker/container environments**
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ sd-server (HTTP Server) │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Axum HTTP Server (Port 8080) │ │
|
||||
│ │ ├─ /health (healthcheck) │ │
|
||||
│ │ ├─ /rpc (proxy to daemon) │ │
|
||||
│ │ └─ /* (web UI assets) │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Embedded Daemon │ │
|
||||
│ │ (Unix socket: daemon.sock) │ │
|
||||
│ │ ├─ Core VDFS │ │
|
||||
│ │ ├─ Indexing │ │
|
||||
│ │ ├─ P2P Networking │ │
|
||||
│ │ └─ File Operations │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Unlike Tauri (desktop app), the server:
|
||||
- Embeds the daemon instead of spawning a separate process
|
||||
- Serves the web UI as static assets (when built with `--features assets`)
|
||||
- Proxies RPC requests from browser to daemon via Unix socket
|
||||
- Provides basic auth for security
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Development (without Docker)
|
||||
|
||||
1. **Build the server:**
|
||||
```bash
|
||||
# Without web assets (for API-only usage)
|
||||
cargo build -p sd-server
|
||||
|
||||
# With bundled web UI
|
||||
cargo build -p sd-server --features assets
|
||||
```
|
||||
|
||||
2. **Run the server:**
|
||||
```bash
|
||||
# Development mode (creates temp data dir)
|
||||
cargo run -p sd-server
|
||||
|
||||
# Production mode (requires DATA_DIR)
|
||||
DATA_DIR=/path/to/data cargo run -p sd-server --release --features assets
|
||||
```
|
||||
|
||||
3. **Access the web UI:**
|
||||
- Open http://localhost:8080
|
||||
- Default auth: disabled in dev mode
|
||||
|
||||
### Docker Deployment (Recommended)
|
||||
|
||||
Perfect for TrueNAS, Unraid, or any Docker-compatible NAS.
|
||||
|
||||
1. **Create a `.env` file:**
|
||||
```bash
|
||||
# REQUIRED: Set your credentials
|
||||
SD_AUTH=admin:your-secure-password
|
||||
|
||||
# Optional: Change port
|
||||
PORT=8080
|
||||
|
||||
# Optional: Disable auth (NOT RECOMMENDED)
|
||||
# SD_AUTH=disabled
|
||||
```
|
||||
|
||||
2. **Start with docker-compose:**
|
||||
```bash
|
||||
cd apps/server
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
3. **Access the server:**
|
||||
- Navigate to `http://your-nas-ip:8080`
|
||||
- Login with credentials from `.env`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `DATA_DIR` | Path to Spacedrive data directory | `/data` (in Docker) | Yes (production) |
|
||||
| `PORT` | HTTP server port | `8080` | No |
|
||||
| `SD_AUTH` | Authentication credentials (format: `user:pass,user2:pass2`) | None | Recommended |
|
||||
| `SD_P2P` | Enable P2P networking | `true` | No |
|
||||
| `RUST_LOG` | Log level | `info,sd_core=debug` | No |
|
||||
|
||||
### Authentication
|
||||
|
||||
**IMPORTANT:** Always set `SD_AUTH` in production!
|
||||
|
||||
```bash
|
||||
# Single user
|
||||
SD_AUTH=admin:securepassword123
|
||||
|
||||
# Multiple users
|
||||
SD_AUTH=admin:pass1,user:pass2,readonly:pass3
|
||||
|
||||
# Disable (NOT RECOMMENDED - only for trusted networks)
|
||||
SD_AUTH=disabled
|
||||
```
|
||||
|
||||
Uses HTTP Basic Authentication. The server will return `401 Unauthorized` if credentials don't match.
|
||||
|
||||
### Data Storage
|
||||
|
||||
The server stores all data in `DATA_DIR`:
|
||||
```
|
||||
$DATA_DIR/
|
||||
├── daemon/
|
||||
│ └── daemon.sock # Unix socket for RPC
|
||||
├── libraries/
|
||||
│ └── *.sdlibrary/ # Library databases
|
||||
├── logs/ # Application logs
|
||||
└── current_library_id.txt # Last opened library
|
||||
```
|
||||
|
||||
**Docker volumes:** Mounted at `/data` inside the container.
|
||||
|
||||
## TrueNAS Setup
|
||||
|
||||
### Using TrueNAS SCALE (Docker)
|
||||
|
||||
1. **Navigate to Apps** in TrueNAS web UI
|
||||
2. **Click "Launch Docker Image"**
|
||||
3. **Configure:**
|
||||
- **Image:** Build locally or use pre-built image
|
||||
- **Port:** Map `8080` to host
|
||||
- **Volume:** Mount `/mnt/pool/spacedrive` to `/data`
|
||||
- **Environment:**
|
||||
- `SD_AUTH=admin:yourpassword`
|
||||
- `TZ=America/New_York` (your timezone)
|
||||
|
||||
4. **Add storage pools** (optional):
|
||||
- Mount your datasets as read-only volumes
|
||||
- Example: `/mnt/tank/photos` → `/photos` in container
|
||||
|
||||
### Manual Docker Run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name spacedrive \
|
||||
-p 8080:8080 \
|
||||
-p 7373:7373 \
|
||||
-v /mnt/pool/spacedrive:/data \
|
||||
-v /mnt/pool/media:/media:ro \
|
||||
-e SD_AUTH=admin:password \
|
||||
-e TZ=UTC \
|
||||
--restart unless-stopped \
|
||||
spacedrive/server:latest
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### With Web Assets (Production)
|
||||
|
||||
```bash
|
||||
# Build everything (web UI + server)
|
||||
cargo build --release -p sd-server --features assets
|
||||
|
||||
# The binary includes bundled web assets
|
||||
./target/release/sd-server --data-dir /path/to/data
|
||||
```
|
||||
|
||||
### Without Assets (API Only)
|
||||
|
||||
```bash
|
||||
# Build server without web UI
|
||||
cargo build --release -p sd-server
|
||||
|
||||
# Serve API endpoints only
|
||||
./target/release/sd-server --data-dir /path/to/data
|
||||
```
|
||||
|
||||
In this mode, you can connect with:
|
||||
- `sd-cli` (CLI client)
|
||||
- Custom HTTP clients via `/rpc`
|
||||
- Tauri desktop app configured to connect to this server
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Run web dev server:**
|
||||
```bash
|
||||
cd apps/web
|
||||
pnpm dev
|
||||
```
|
||||
This starts Vite on http://localhost:3000 with hot reload.
|
||||
|
||||
2. **Run API server:**
|
||||
```bash
|
||||
cargo run -p sd-server
|
||||
```
|
||||
This starts the HTTP server on http://localhost:8080.
|
||||
|
||||
3. **Develop:**
|
||||
- Edit React components in `apps/web/src`
|
||||
- Edit server code in `apps/server/src`
|
||||
- Vite proxies `/rpc` requests to the server
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `GET /health`
|
||||
Health check endpoint.
|
||||
|
||||
**Response:** `200 OK` with body `"OK"`
|
||||
|
||||
### `POST /rpc`
|
||||
JSON-RPC proxy to daemon.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "query:libraries.list",
|
||||
"params": { "include_stats": false }
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /*` (with `--features assets`)
|
||||
Serves the bundled web UI. All non-API routes fallback to `index.html` for SPA routing.
|
||||
|
||||
## Comparison: Server vs Tauri
|
||||
|
||||
| Feature | Server | Tauri |
|
||||
|---------|--------|-------|
|
||||
| **Platform** | Linux/Docker | macOS/Windows/Linux |
|
||||
| **UI** | Web (React in browser) | Native webview |
|
||||
| **Daemon** | Embedded in process | Spawned as child process |
|
||||
| **Access** | Remote over HTTP | Local only |
|
||||
| **Auth** | HTTP Basic Auth | Not needed (local) |
|
||||
| **Use Case** | NAS, headless servers | Desktop workstations |
|
||||
|
||||
Both use the same Spacedrive core and `@sd/interface` package!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server won't start
|
||||
- Check `DATA_DIR` exists and is writable
|
||||
- Verify port 8080 is not in use: `lsof -i :8080`
|
||||
- Check logs: `RUST_LOG=debug cargo run -p sd-server`
|
||||
|
||||
### Can't connect to daemon
|
||||
- Ensure `daemon.sock` exists in `$DATA_DIR/daemon/`
|
||||
- Check daemon logs in `$DATA_DIR/logs/`
|
||||
- Try removing stale socket: `rm $DATA_DIR/daemon/daemon.sock`
|
||||
|
||||
### Authentication failing
|
||||
- Verify `SD_AUTH` format: `username:password`
|
||||
- Check browser is sending Basic Auth header
|
||||
- Test with curl:
|
||||
```bash
|
||||
curl -u admin:password http://localhost:8080/health
|
||||
```
|
||||
|
||||
### Docker build failing
|
||||
- Ensure you're building from repository root:
|
||||
```bash
|
||||
docker build -f apps/server/Dockerfile .
|
||||
```
|
||||
- Check Docker has enough memory (4GB+ recommended)
|
||||
|
||||
## Contributing
|
||||
|
||||
The server app is part of the Spacedrive v2 monorepo.
|
||||
|
||||
**Project structure:**
|
||||
```
|
||||
apps/server/
|
||||
├── src/
|
||||
│ └── main.rs # Server implementation
|
||||
├── Cargo.toml # Dependencies
|
||||
├── build.rs # Web bundling script
|
||||
├── Dockerfile # Container image
|
||||
└── docker-compose.yml # Docker setup
|
||||
|
||||
apps/web/
|
||||
├── src/
|
||||
│ ├── main.tsx # Web entry point
|
||||
│ └── platform.ts # Web platform implementation
|
||||
├── package.json
|
||||
└── vite.config.ts
|
||||
```
|
||||
|
||||
**Making changes:**
|
||||
1. Server code: Edit `apps/server/src/main.rs`
|
||||
2. Web UI: Edit `apps/web/src/*` (uses `@sd/interface`)
|
||||
3. Platform integration: Edit `apps/web/src/platform.ts`
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0 - See LICENSE file in repository root.
|
||||
@@ -0,0 +1,406 @@
|
||||
# TrueNAS SCALE Deployment Guide
|
||||
|
||||
Quick reference for deploying Spacedrive Server on TrueNAS SCALE using the GUI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build and Transfer (on your Mac)
|
||||
|
||||
```bash
|
||||
cd ~/Projects/spacedrive/apps/server
|
||||
|
||||
# Build for TrueNAS
|
||||
./build-for-truenas.sh
|
||||
|
||||
# Transfer to TrueNAS (replace with your IP)
|
||||
scp spacedrive-server-*.tar.gz root@192.168.1.100:/tmp/
|
||||
```
|
||||
|
||||
### 2. Load Image (on TrueNAS)
|
||||
|
||||
```bash
|
||||
# SSH into TrueNAS
|
||||
ssh root@192.168.1.100
|
||||
|
||||
# Load the image
|
||||
gunzip -c /tmp/spacedrive-server-*.tar.gz | docker load
|
||||
|
||||
# Verify
|
||||
docker images | grep spacedrive
|
||||
# Should show: spacedrive-server latest ...
|
||||
```
|
||||
|
||||
### 3. Deploy via GUI
|
||||
|
||||
Go to: **Apps** → **Discover Apps** → **Launch Docker Image**
|
||||
|
||||
---
|
||||
|
||||
## GUI Configuration
|
||||
|
||||
### Container Images
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Image Repository | `spacedrive-server` |
|
||||
| Image Tag | `latest` |
|
||||
| **Image Pull Policy** | **Never** ⚠️ (Use local image!) |
|
||||
|
||||
### Container Settings
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Container Name | `spacedrive` |
|
||||
| Restart Policy | Unless Stopped |
|
||||
|
||||
### Networking
|
||||
|
||||
#### Port Forwarding
|
||||
|
||||
| Container Port | Protocol | Node Port | Description |
|
||||
|---------------|----------|-----------|-------------|
|
||||
| `8080` | TCP | `8080` | Web UI & API |
|
||||
| `7373` | TCP | `7373` | P2P Networking |
|
||||
|
||||
**Access your server at:** `http://TRUENAS-IP:8080`
|
||||
|
||||
### Storage
|
||||
|
||||
#### Primary Data Volume
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Type | Host Path (or ixVolume) |
|
||||
| Host Path | `/mnt/your-pool/spacedrive` |
|
||||
| Mount Path | `/data` |
|
||||
| Read Only | ❌ (needs write access) |
|
||||
|
||||
**This stores:**
|
||||
- Library databases (`.sdlibrary/`)
|
||||
- Daemon socket
|
||||
- Logs
|
||||
- Thumbnails/sidecars
|
||||
|
||||
#### Optional: Media Access
|
||||
|
||||
Mount your existing media as read-only:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Type | Host Path |
|
||||
| Host Path | `/mnt/your-pool/media` |
|
||||
| Mount Path | `/media` |
|
||||
| Read Only | ✅ |
|
||||
|
||||
Repeat for other datasets:
|
||||
- `/mnt/your-pool/photos` → `/photos`
|
||||
- `/mnt/your-pool/documents` → `/documents`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Required:**
|
||||
|
||||
| Name | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| `SD_AUTH` | `admin:CHANGE_THIS_PASSWORD` | Authentication (username:password) |
|
||||
|
||||
**Recommended:**
|
||||
|
||||
| Name | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| `TZ` | `America/New_York` | Your timezone |
|
||||
| `RUST_LOG` | `info,sd_core=debug` | Log level |
|
||||
|
||||
**Optional:**
|
||||
|
||||
| Name | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| `PORT` | `8080` | HTTP port (if you want to change it) |
|
||||
| `SD_P2P` | `true` | Enable P2P (default: true) |
|
||||
|
||||
### Health Check (Optional)
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Type | HTTP |
|
||||
| Port | `8080` |
|
||||
| Path | `/health` |
|
||||
| Initial Delay | `30` seconds |
|
||||
| Timeout | `10` seconds |
|
||||
| Period | `30` seconds |
|
||||
|
||||
### Resource Limits (Optional)
|
||||
|
||||
**Recommended for NAS stability:**
|
||||
|
||||
| Resource | Limit | Reservation |
|
||||
|----------|-------|-------------|
|
||||
| Memory | `2 GB` | `512 MB` |
|
||||
| CPU | - | - |
|
||||
|
||||
---
|
||||
|
||||
## Post-Installation
|
||||
|
||||
### 1. Verify Container is Running
|
||||
|
||||
In TrueNAS GUI:
|
||||
- **Apps** → **Installed** → Should see **spacedrive** with green status
|
||||
|
||||
Or via shell:
|
||||
```bash
|
||||
docker ps | grep spacedrive
|
||||
```
|
||||
|
||||
### 2. Check Logs
|
||||
|
||||
In GUI: Click **spacedrive** → **Logs**
|
||||
|
||||
Or via shell:
|
||||
```bash
|
||||
docker logs spacedrive
|
||||
```
|
||||
|
||||
Should see:
|
||||
```
|
||||
🚀 Spacedrive Server listening on http://localhost:8080
|
||||
✓ Daemon started successfully
|
||||
```
|
||||
|
||||
### 3. Access Web UI
|
||||
|
||||
Open browser: `http://YOUR-TRUENAS-IP:8080`
|
||||
|
||||
Login with credentials from `SD_AUTH`:
|
||||
- Username: `admin`
|
||||
- Password: (whatever you set)
|
||||
|
||||
---
|
||||
|
||||
## Updating the Server
|
||||
|
||||
When you rebuild on your Mac:
|
||||
|
||||
### 1. Build new image
|
||||
|
||||
```bash
|
||||
cd ~/Projects/spacedrive/apps/server
|
||||
./build-for-truenas.sh
|
||||
```
|
||||
|
||||
### 2. Transfer and load
|
||||
|
||||
```bash
|
||||
# Transfer new tar
|
||||
scp spacedrive-server-*.tar.gz root@TRUENAS-IP:/tmp/
|
||||
|
||||
# SSH and load
|
||||
ssh root@TRUENAS-IP
|
||||
gunzip -c /tmp/spacedrive-server-*.tar.gz | docker load
|
||||
```
|
||||
|
||||
### 3. Restart container in GUI
|
||||
|
||||
**Apps** → **Installed** → **spacedrive** → **Stop** → **Start**
|
||||
|
||||
Or via shell:
|
||||
```bash
|
||||
docker restart spacedrive
|
||||
```
|
||||
|
||||
The container will use the updated `spacedrive-server:latest` image.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container won't start
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
docker logs spacedrive
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Permission denied on `/data` → Check host path exists and is writable
|
||||
- Port already in use → Change `8080` to something else
|
||||
- Auth error → Verify `SD_AUTH` format is `username:password`
|
||||
|
||||
### Can't connect to web UI
|
||||
|
||||
1. Verify container is running: `docker ps | grep spacedrive`
|
||||
2. Check port mapping: Should show `0.0.0.0:8080->8080/tcp`
|
||||
3. Test from TrueNAS shell: `curl http://localhost:8080/health`
|
||||
4. Check firewall rules (TrueNAS should allow by default)
|
||||
|
||||
### Daemon not starting
|
||||
|
||||
**Check socket:**
|
||||
```bash
|
||||
docker exec spacedrive ls -la /data/daemon/
|
||||
```
|
||||
|
||||
Should see `daemon.sock`
|
||||
|
||||
**Check daemon logs:**
|
||||
```bash
|
||||
docker exec spacedrive cat /data/logs/daemon.log
|
||||
```
|
||||
|
||||
### Wrong architecture error
|
||||
|
||||
Make sure you built with `--platform linux/amd64`:
|
||||
```bash
|
||||
docker inspect spacedrive-server:latest | grep Architecture
|
||||
# Should show: "Architecture": "amd64"
|
||||
```
|
||||
|
||||
If not, rebuild:
|
||||
```bash
|
||||
./build-for-truenas.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
**On TrueNAS host:**
|
||||
```
|
||||
/mnt/your-pool/spacedrive/
|
||||
├── daemon/
|
||||
│ └── daemon.sock # Unix socket
|
||||
├── libraries/
|
||||
│ └── My Library.sdlibrary/
|
||||
│ ├── library.db # SQLite database
|
||||
│ └── sidecars/ # Thumbnails
|
||||
└── logs/
|
||||
└── daemon.log
|
||||
```
|
||||
|
||||
**Inside container:**
|
||||
```
|
||||
/data/ # Maps to host path above
|
||||
/media/ # Your media mounts (if configured)
|
||||
/photos/
|
||||
/documents/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Port
|
||||
|
||||
If port 8080 is taken:
|
||||
|
||||
1. Change environment variable: `PORT=9000`
|
||||
2. Update port forwarding: `9000 → 9000`
|
||||
3. Access at: `http://TRUENAS-IP:9000`
|
||||
|
||||
### Multiple Instances
|
||||
|
||||
Run multiple Spacedrive instances with different data dirs:
|
||||
|
||||
**Instance 1 (Personal):**
|
||||
- Container name: `spacedrive-personal`
|
||||
- Host path: `/mnt/pool/spacedrive-personal`
|
||||
- Ports: `8080:8080`, `7373:7373`
|
||||
|
||||
**Instance 2 (Work):**
|
||||
- Container name: `spacedrive-work`
|
||||
- Host path: `/mnt/pool/spacedrive-work`
|
||||
- Ports: `8081:8080`, `7374:7373`
|
||||
- Env: `INSTANCE=work`
|
||||
|
||||
### Reverse Proxy (HTTPS)
|
||||
|
||||
If you want HTTPS access, put behind nginx/Caddy:
|
||||
|
||||
**Caddy example:**
|
||||
```
|
||||
spacedrive.yourdomain.com {
|
||||
reverse_proxy localhost:8080
|
||||
basicauth {
|
||||
admin $2a$14$... # hashed password
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
**⚠️ IMPORTANT:**
|
||||
- **Always set SD_AUTH** - never use `SD_AUTH=disabled` on a network-accessible server
|
||||
- **Use strong passwords** - not `admin:changeme`
|
||||
- Consider **firewall rules** if exposing to internet
|
||||
- Run behind **reverse proxy with HTTPS** for public access
|
||||
- **Read-only mounts** for media you don't want Spacedrive to modify
|
||||
|
||||
**Network access:**
|
||||
- `8080` → Web UI (needs auth)
|
||||
- `7373` → P2P (encrypted via QUIC/TLS)
|
||||
|
||||
---
|
||||
|
||||
## Backup
|
||||
|
||||
**What to backup:**
|
||||
```
|
||||
/mnt/your-pool/spacedrive/libraries/
|
||||
```
|
||||
|
||||
This contains your library databases and metadata.
|
||||
|
||||
**How:**
|
||||
- TrueNAS snapshots (recommended)
|
||||
- Or periodic `rsync`/`tar` backup
|
||||
|
||||
**Not needed:**
|
||||
- `daemon.sock` (recreated on start)
|
||||
- `logs/` (optional)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
**View logs:**
|
||||
```bash
|
||||
docker logs -f spacedrive
|
||||
```
|
||||
|
||||
**Shell access:**
|
||||
```bash
|
||||
docker exec -it spacedrive sh
|
||||
```
|
||||
|
||||
**Check daemon status:**
|
||||
```bash
|
||||
curl -u admin:yourpassword http://TRUENAS-IP:8080/health
|
||||
```
|
||||
|
||||
Should return: `OK`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TrueNAS SCALE: Spacedrive Server │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Image: spacedrive-server:latest │
|
||||
│ Pull: Never (use local image) │
|
||||
│ │
|
||||
│ Ports: 8080 (Web UI), 7373 (P2P) │
|
||||
│ │
|
||||
│ Volume: /mnt/pool/spacedrive → /data │
|
||||
│ │
|
||||
│ Env: SD_AUTH=admin:password (REQUIRED) │
|
||||
│ TZ=America/New_York │
|
||||
│ │
|
||||
│ Access: http://TRUENAS-IP:8080 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
# Build Spacedrive Server for TrueNAS deployment
|
||||
# Usage: ./build-for-truenas.sh [truenas-ip]
|
||||
|
||||
set -e
|
||||
|
||||
TRUENAS_IP="${1:-}"
|
||||
IMAGE_NAME="spacedrive-server"
|
||||
IMAGE_TAG="latest"
|
||||
TAR_FILE="spacedrive-server-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||
|
||||
echo "🏗️ Building Spacedrive Server for linux/amd64..."
|
||||
echo ""
|
||||
|
||||
# Check if buildx is available
|
||||
if ! docker buildx version &> /dev/null; then
|
||||
echo "❌ docker buildx not found. Installing..."
|
||||
docker buildx create --use
|
||||
fi
|
||||
|
||||
# Build the image for linux/amd64 (TrueNAS architecture)
|
||||
echo "📦 Building Docker image..."
|
||||
cd ../.. # Go to repository root
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-f apps/server/Dockerfile \
|
||||
-t ${IMAGE_NAME}:${IMAGE_TAG} \
|
||||
--load \
|
||||
.
|
||||
|
||||
echo ""
|
||||
echo "✅ Build complete!"
|
||||
echo ""
|
||||
|
||||
# Save the image to a tar file
|
||||
echo "💾 Saving image to ${TAR_FILE}..."
|
||||
docker save ${IMAGE_NAME}:${IMAGE_TAG} | gzip > "apps/server/${TAR_FILE}"
|
||||
|
||||
IMAGE_SIZE=$(du -h "apps/server/${TAR_FILE}" | cut -f1)
|
||||
echo "✅ Image saved: apps/server/${TAR_FILE} (${IMAGE_SIZE})"
|
||||
echo ""
|
||||
|
||||
# If TrueNAS IP provided, offer to transfer
|
||||
if [ -n "$TRUENAS_IP" ]; then
|
||||
echo "📤 Transfer to TrueNAS?"
|
||||
echo " Target: root@${TRUENAS_IP}"
|
||||
echo ""
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Transferring..."
|
||||
scp "apps/server/${TAR_FILE}" "root@${TRUENAS_IP}:/tmp/"
|
||||
|
||||
echo ""
|
||||
echo "✅ Transfer complete!"
|
||||
echo ""
|
||||
echo "📋 Next steps on TrueNAS:"
|
||||
echo " 1. SSH into TrueNAS: ssh root@${TRUENAS_IP}"
|
||||
echo " 2. Load the image: gunzip -c /tmp/${TAR_FILE} | docker load"
|
||||
echo " 3. Deploy via TrueNAS SCALE Apps UI (see instructions below)"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
echo "📋 Manual deployment steps:"
|
||||
echo ""
|
||||
echo "1. Transfer the image to TrueNAS:"
|
||||
echo " scp apps/server/${TAR_FILE} root@YOUR-TRUENAS-IP:/tmp/"
|
||||
echo ""
|
||||
echo "2. SSH into TrueNAS:"
|
||||
echo " ssh root@YOUR-TRUENAS-IP"
|
||||
echo ""
|
||||
echo "3. Load the image:"
|
||||
echo " gunzip -c /tmp/${TAR_FILE} | docker load"
|
||||
echo ""
|
||||
echo "4. Verify it loaded:"
|
||||
echo " docker images | grep spacedrive"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo "🎯 TrueNAS SCALE GUI Deployment Instructions"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "After loading the image on TrueNAS:"
|
||||
echo ""
|
||||
echo "1. Go to: Apps → Discover Apps → Launch Docker Image"
|
||||
echo ""
|
||||
echo "2. Container Settings:"
|
||||
echo " • Image Repository: ${IMAGE_NAME}"
|
||||
echo " • Image Tag: ${IMAGE_TAG}"
|
||||
echo " • Image Pull Policy: Never (important - use local image!)"
|
||||
echo " • Container Name: spacedrive"
|
||||
echo ""
|
||||
echo "3. Port Forwarding:"
|
||||
echo " • Container Port: 8080 → Node Port: 8080"
|
||||
echo " • Container Port: 7373 → Node Port: 7373"
|
||||
echo ""
|
||||
echo "4. Storage (Host Path Volumes):"
|
||||
echo " • Host Path: /mnt/YOUR-POOL/spacedrive"
|
||||
echo " Mount Path: /data"
|
||||
echo " Type: ixVolume (or Host Path)"
|
||||
echo ""
|
||||
echo " Optional - Mount your media:"
|
||||
echo " • Host Path: /mnt/YOUR-POOL/media"
|
||||
echo " Mount Path: /media"
|
||||
echo " Read Only: ✓"
|
||||
echo ""
|
||||
echo "5. Environment Variables:"
|
||||
echo " • SD_AUTH = admin:changeme (CHANGE THIS!)"
|
||||
echo " • TZ = America/New_York (your timezone)"
|
||||
echo " • RUST_LOG = info,sd_core=debug"
|
||||
echo ""
|
||||
echo "6. Health Check (optional but recommended):"
|
||||
echo " • Type: HTTP"
|
||||
echo " • Port: 8080"
|
||||
echo " • Path: /health"
|
||||
echo ""
|
||||
echo "7. Click 'Install'"
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "🌐 Access: http://YOUR-TRUENAS-IP:8080"
|
||||
echo "🔐 Login with credentials from SD_AUTH"
|
||||
echo ""
|
||||
@@ -0,0 +1,50 @@
|
||||
fn main() {
|
||||
// Only build web assets if the assets feature is enabled
|
||||
#[cfg(feature = "assets")]
|
||||
{
|
||||
use std::process::Command;
|
||||
println!("cargo:rerun-if-changed=../web/src");
|
||||
println!("cargo:rerun-if-changed=../web/index.html");
|
||||
println!("cargo:rerun-if-changed=../web/package.json");
|
||||
|
||||
// Build the web app
|
||||
let web_dir = std::env::current_dir()
|
||||
.expect("Failed to get current dir")
|
||||
.join("../web");
|
||||
|
||||
// Check if pnpm is available
|
||||
let pnpm_check = Command::new("pnpm")
|
||||
.arg("--version")
|
||||
.output();
|
||||
|
||||
if pnpm_check.is_err() {
|
||||
panic!("pnpm is required to build web assets. Install it with: npm install -g pnpm");
|
||||
}
|
||||
|
||||
// Install dependencies
|
||||
println!("Installing web dependencies...");
|
||||
let install = Command::new("pnpm")
|
||||
.arg("install")
|
||||
.current_dir(&web_dir)
|
||||
.status()
|
||||
.expect("Failed to run pnpm install");
|
||||
|
||||
if !install.success() {
|
||||
panic!("pnpm install failed");
|
||||
}
|
||||
|
||||
// Build the web app
|
||||
println!("Building web app...");
|
||||
let build = Command::new("pnpm")
|
||||
.arg("build")
|
||||
.current_dir(&web_dir)
|
||||
.status()
|
||||
.expect("Failed to run pnpm build");
|
||||
|
||||
if !build.success() {
|
||||
panic!("pnpm build failed");
|
||||
}
|
||||
|
||||
println!("Web assets built successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
services:
|
||||
spacedrive:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: apps/server/Dockerfile
|
||||
container_name: spacedrive-server
|
||||
restart: unless-stopped
|
||||
|
||||
# Ports
|
||||
ports:
|
||||
- "8080:8080" # HTTP server
|
||||
- "7373:7373" # P2P networking
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
- spacedrive-data:/data
|
||||
# Optional: Mount your file systems here
|
||||
# - /mnt/storage:/storage:ro
|
||||
|
||||
# Environment variables
|
||||
environment:
|
||||
# Data directory (inside container)
|
||||
- DATA_DIR=/data
|
||||
|
||||
# HTTP server port
|
||||
- PORT=8080
|
||||
|
||||
# Authentication (REQUIRED for security!)
|
||||
# Format: "username:password,username2:password2"
|
||||
# Or set to "disabled" to disable auth (NOT RECOMMENDED)
|
||||
- SD_AUTH=${SD_AUTH:-admin:changeme}
|
||||
|
||||
# Enable P2P networking
|
||||
- SD_P2P=true
|
||||
|
||||
# Logging
|
||||
- RUST_LOG=info,sd_core=debug
|
||||
|
||||
# Timezone
|
||||
- TZ=UTC
|
||||
|
||||
# Health check
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# Resource limits (adjust as needed)
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2G
|
||||
reservations:
|
||||
memory: 512M
|
||||
|
||||
volumes:
|
||||
spacedrive-data:
|
||||
driver: local
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Quick start script for running Spacedrive Server in Docker
|
||||
# Usage: ./docker-run.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Spacedrive Server with Docker Compose..."
|
||||
|
||||
# Check if .env exists
|
||||
if [ ! -f .env ]; then
|
||||
echo "⚠️ No .env file found. Creating from template..."
|
||||
cp .env.example .env
|
||||
echo ""
|
||||
echo "📝 IMPORTANT: Edit .env and set your SD_AUTH credentials!"
|
||||
echo " Default is 'admin:changeme' - please change this."
|
||||
echo ""
|
||||
read -p "Press enter to continue or Ctrl+C to abort..."
|
||||
fi
|
||||
|
||||
# Check if docker-compose is available
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker not found. Please install Docker first."
|
||||
exit 1
|
||||
fi
|
||||
# Try docker compose (newer syntax)
|
||||
COMPOSE_CMD="docker compose"
|
||||
else
|
||||
COMPOSE_CMD="docker-compose"
|
||||
fi
|
||||
|
||||
echo "🏗️ Building and starting container..."
|
||||
$COMPOSE_CMD up -d --build
|
||||
|
||||
echo ""
|
||||
echo "✅ Spacedrive Server is running!"
|
||||
echo ""
|
||||
echo "📍 Access your server at: http://localhost:8080"
|
||||
echo "🔐 Login credentials: Check your .env file (SD_AUTH)"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " - View logs: $COMPOSE_CMD logs -f spacedrive"
|
||||
echo " - Stop server: $COMPOSE_CMD down"
|
||||
echo " - Restart: $COMPOSE_CMD restart"
|
||||
echo " - Shell access: $COMPOSE_CMD exec spacedrive sh"
|
||||
echo ""
|
||||
@@ -0,0 +1,427 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{FromRequestParts, Request, State},
|
||||
http::{header, HeaderValue, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use axum_extra::{headers::authorization::Basic, headers::Authorization, TypedHeader};
|
||||
use clap::Parser;
|
||||
use secstr::SecStr;
|
||||
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::UnixStream,
|
||||
signal,
|
||||
sync::RwLock,
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[cfg(feature = "assets")]
|
||||
static ASSETS_DIR: include_dir::Dir<'static> =
|
||||
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../web/dist");
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
auth: HashMap<String, SecStr>,
|
||||
socket_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Basic auth middleware
|
||||
async fn basic_auth(State(state): State<AppState>, request: Request, next: Next) -> Response {
|
||||
let request = if !state.auth.is_empty() {
|
||||
let (mut parts, body) = request.into_parts();
|
||||
|
||||
let Ok(TypedHeader(Authorization(hdr))) =
|
||||
TypedHeader::<Authorization<Basic>>::from_request_parts(&mut parts, &()).await
|
||||
else {
|
||||
return Response::builder()
|
||||
.status(401)
|
||||
.header("WWW-Authenticate", "Basic realm=\"Spacedrive\"")
|
||||
.body("Unauthorized".into_response().into_body())
|
||||
.expect("hardcoded response will be valid");
|
||||
};
|
||||
let request = Request::from_parts(parts, body);
|
||||
|
||||
if state
|
||||
.auth
|
||||
.get(hdr.username())
|
||||
.map(|pass| *pass == SecStr::from(hdr.password()))
|
||||
!= Some(true)
|
||||
{
|
||||
return Response::builder()
|
||||
.status(401)
|
||||
.header("WWW-Authenticate", "Basic realm=\"Spacedrive\"")
|
||||
.body("Unauthorized".into_response().into_body())
|
||||
.expect("hardcoded response will be valid");
|
||||
}
|
||||
|
||||
request
|
||||
} else {
|
||||
request
|
||||
};
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
/// Health check endpoint
|
||||
async fn health() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
/// Proxy RPC requests to the daemon via Unix socket
|
||||
async fn daemon_rpc(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// Connect to daemon
|
||||
let mut stream = UnixStream::connect(&state.socket_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::SERVICE_UNAVAILABLE, format!("Daemon not available: {}", e)))?;
|
||||
|
||||
// Send request
|
||||
let request_line = serde_json::to_string(&payload)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)))?;
|
||||
|
||||
stream
|
||||
.write_all(format!("{}\n", request_line).as_bytes())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Write failed: {}", e)))?;
|
||||
|
||||
// Read response
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response_line = String::new();
|
||||
|
||||
reader
|
||||
.read_line(&mut response_line)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Read failed: {}", e)))?;
|
||||
|
||||
// Parse and return
|
||||
let response: serde_json::Value = serde_json::from_str(&response_line)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid response: {}", e)))?;
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[cfg(feature = "assets")]
|
||||
async fn serve_asset(path: String) -> Response {
|
||||
let path = path.trim_start_matches('/');
|
||||
|
||||
match ASSETS_DIR.get_file(path) {
|
||||
Some(file) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(
|
||||
mime_guess::from_path(path)
|
||||
.first_or_text_plain()
|
||||
.as_ref(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.body(Body::from(file.contents()))
|
||||
.unwrap(),
|
||||
None => {
|
||||
// Fallback to index.html for SPA routing
|
||||
match ASSETS_DIR.get_file("index.html") {
|
||||
Some(file) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str("text/html").unwrap(),
|
||||
)
|
||||
.body(Body::from(file.contents()))
|
||||
.unwrap(),
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "assets")]
|
||||
async fn serve_index() -> Response {
|
||||
match ASSETS_DIR.get_file("index.html") {
|
||||
Some(file) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_str("text/html").unwrap(),
|
||||
)
|
||||
.body(Body::from(file.contents()))
|
||||
.unwrap(),
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "spacedrive-server", about = "Spacedrive HTTP server")]
|
||||
struct Args {
|
||||
/// Path to spacedrive data directory
|
||||
#[arg(long, env = "DATA_DIR")]
|
||||
data_dir: Option<PathBuf>,
|
||||
|
||||
/// Port to bind HTTP server (default: 8080)
|
||||
#[arg(long, env = "PORT", default_value = "8080")]
|
||||
port: u16,
|
||||
|
||||
/// Authentication credentials (format: "username:password,username2:password2")
|
||||
/// Set to "disabled" to disable auth (not recommended in production)
|
||||
#[arg(long, env = "SD_AUTH")]
|
||||
auth: Option<String>,
|
||||
|
||||
/// Daemon instance name (for running multiple instances)
|
||||
#[arg(long)]
|
||||
instance: Option<String>,
|
||||
|
||||
/// Enable P2P networking
|
||||
#[arg(long, env = "SD_P2P", default_value = "true")]
|
||||
p2p: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info,sd_core=debug,sd_server=debug".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// Resolve data directory
|
||||
let base_data_dir = args
|
||||
.data_dir
|
||||
.unwrap_or_else(|| {
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
std::env::var("DATA_DIR")
|
||||
.expect("DATA_DIR must be set in production")
|
||||
.into()
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
std::env::var("DATA_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
let temp = tempfile::tempdir().expect("Failed to create temp dir");
|
||||
temp.path().to_path_buf()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate instance-specific paths
|
||||
let (data_dir, socket_path) = if let Some(instance) = &args.instance {
|
||||
let instance_data_dir = base_data_dir.join("instances").join(instance);
|
||||
let socket_path = base_data_dir
|
||||
.join("daemon")
|
||||
.join(format!("daemon-{}.sock", instance));
|
||||
(instance_data_dir, socket_path)
|
||||
} else {
|
||||
let socket_path = base_data_dir.join("daemon/daemon.sock");
|
||||
(base_data_dir.clone(), socket_path)
|
||||
};
|
||||
|
||||
info!("Data directory: {:?}", data_dir);
|
||||
info!("Socket path: {:?}", socket_path);
|
||||
|
||||
// Parse authentication
|
||||
let (auth, _disabled) = parse_auth(args.auth.as_deref());
|
||||
|
||||
// Require credentials in production builds (unless explicitly disabled)
|
||||
#[cfg(not(debug_assertions))]
|
||||
if auth.is_empty() && !_disabled {
|
||||
warn!("The 'SD_AUTH' environment variable is not set!");
|
||||
warn!("If you want to disable auth set 'SD_AUTH=disabled', or");
|
||||
warn!("Provide your credentials in the following format 'SD_AUTH=username:password,username2:password2'");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Ensure daemon directory exists
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Start the daemon if not already running
|
||||
let daemon_handle = start_daemon_if_needed(socket_path.clone(), data_dir.clone(), args.p2p).await?;
|
||||
|
||||
// Build HTTP router
|
||||
let state = AppState {
|
||||
auth,
|
||||
socket_path: socket_path.clone(),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/rpc", post(daemon_rpc));
|
||||
|
||||
// Add asset serving routes if assets feature is enabled
|
||||
#[cfg(feature = "assets")]
|
||||
let app = app
|
||||
.route("/", get(serve_index))
|
||||
.route("/*path", get(|axum::extract::Path(path): axum::extract::Path<String>| serve_asset(path)));
|
||||
|
||||
#[cfg(not(feature = "assets"))]
|
||||
let app = app.route("/", get(|| async { "Spacedrive Server - Use with web client" }));
|
||||
|
||||
let app = app
|
||||
.fallback(|| async {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404 Not Found: We're past the event horizon...",
|
||||
)
|
||||
})
|
||||
.layer(middleware::from_fn_with_state(state.clone(), basic_auth))
|
||||
.with_state(state);
|
||||
|
||||
// Bind server
|
||||
let mut addr = "[::]:8080".parse::<SocketAddr>().unwrap();
|
||||
addr.set_port(args.port);
|
||||
|
||||
info!("🚀 Spacedrive Server listening on http://localhost:{}", args.port);
|
||||
#[cfg(feature = "assets")]
|
||||
info!("📦 Serving bundled web assets");
|
||||
#[cfg(not(feature = "assets"))]
|
||||
info!("📦 Asset serving disabled (use --features assets)");
|
||||
|
||||
// Setup graceful shutdown
|
||||
let shutdown_signal = shutdown_signal(daemon_handle);
|
||||
|
||||
// Start server
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse authentication credentials from env var
|
||||
fn parse_auth(auth_str: Option<&str>) -> (HashMap<String, SecStr>, bool) {
|
||||
let Some(input) = auth_str else {
|
||||
return (HashMap::new(), false);
|
||||
};
|
||||
|
||||
if input == "disabled" {
|
||||
return (HashMap::new(), true);
|
||||
}
|
||||
|
||||
let credentials = input
|
||||
.split(',')
|
||||
.enumerate()
|
||||
.filter_map(|(i, s)| {
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut parts = s.split(':');
|
||||
let result = parts
|
||||
.next()
|
||||
.and_then(|user| parts.next().map(|pass| (user.to_string(), SecStr::from(pass))));
|
||||
|
||||
if result.is_none() {
|
||||
warn!("Found invalid credential {i}. Skipping...");
|
||||
}
|
||||
result
|
||||
})
|
||||
.collect();
|
||||
|
||||
(credentials, false)
|
||||
}
|
||||
|
||||
/// Start the daemon if it's not already running
|
||||
async fn start_daemon_if_needed(
|
||||
socket_path: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
enable_p2p: bool,
|
||||
) -> Result<Option<Arc<RwLock<tokio::task::JoinHandle<()>>>>, Box<dyn std::error::Error>> {
|
||||
// Check if daemon is already running
|
||||
if socket_path.exists() {
|
||||
match UnixStream::connect(&socket_path).await {
|
||||
Ok(_) => {
|
||||
info!("✓ Daemon already running");
|
||||
return Ok(None);
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Stale socket file found, removing...");
|
||||
std::fs::remove_file(&socket_path).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Starting embedded daemon...");
|
||||
|
||||
// Start daemon in background task
|
||||
let socket_path_clone = socket_path.clone();
|
||||
let data_dir_clone = data_dir.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = sd_core::infra::daemon::bootstrap::start_default_server(
|
||||
socket_path_clone,
|
||||
data_dir_clone,
|
||||
enable_p2p,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Daemon failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for socket to be created (daemon startup)
|
||||
for i in 0..30 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
if socket_path.exists() {
|
||||
info!("✓ Daemon started successfully");
|
||||
return Ok(Some(Arc::new(RwLock::new(handle))));
|
||||
}
|
||||
if i == 10 {
|
||||
warn!("Daemon taking longer than expected to start...");
|
||||
}
|
||||
}
|
||||
|
||||
Err("Daemon failed to start (socket not created after 3 seconds)".into())
|
||||
}
|
||||
|
||||
/// Graceful shutdown handler
|
||||
async fn shutdown_signal(daemon_handle: Option<Arc<RwLock<tokio::task::JoinHandle<()>>>>) {
|
||||
let ctrl_c = async {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
() = ctrl_c => {
|
||||
info!("Received Ctrl+C, shutting down gracefully...");
|
||||
}
|
||||
() = terminate => {
|
||||
info!("Received SIGTERM, shutting down gracefully...");
|
||||
}
|
||||
}
|
||||
|
||||
// Abort daemon task if we started it
|
||||
if let Some(handle) = daemon_handle {
|
||||
handle.write().await.abort();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user