refactor(Docker): remove legacy Docker setup and streamline server deployment

This commit removes the outdated Dockerfile, docker-compose.yml, and related documentation for the Spacedrive server, consolidating the deployment process into a single Dockerfile located in the apps/server directory. The new setup supports multi-architecture builds and includes enhanced media processing capabilities. Additionally, a self-hosting guide is introduced to assist users in deploying the server on their infrastructure, ensuring a more efficient and user-friendly experience.
This commit is contained in:
Jamie Pine
2026-01-11 20:14:29 -08:00
parent df10b6cf72
commit 4db6a83a45
40 changed files with 791 additions and 332 deletions

View File

@@ -102,6 +102,71 @@ jobs:
# name: cli-${{ matrix.platform }}
# path: dist/*
# Server builds for self-hosting
server-build:
strategy:
matrix:
settings:
- host: ubuntu-22.04
target: x86_64-unknown-linux-gnu
platform: linux-x86_64
- host: ubuntu-22.04
target: aarch64-unknown-linux-gnu
platform: linux-aarch64
name: Server - ${{ matrix.settings.platform }}
runs-on: ${{ matrix.settings.host }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- name: Setup System and Rust
uses: ./.github/actions/setup-system
with:
token: ${{ secrets.GITHUB_TOKEN }}
target: ${{ matrix.settings.target }}
- name: Install cross-compilation tools (ARM)
if: matrix.settings.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Setup native dependencies
run: cargo run -p xtask -- setup
- name: Build server binary
run: |
cargo build --release --bin sd-server --features sd-core/heif,sd-core/ffmpeg --target ${{ matrix.settings.target }}
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Prepare server binary
run: |
mkdir -p dist
cp target/${{ matrix.settings.target }}/release/sd-server dist/sd-server-${{ matrix.settings.platform }}
chmod +x dist/sd-server-${{ matrix.settings.platform }}
- name: Generate checksum
run: |
cd dist
sha256sum sd-server-${{ matrix.settings.platform }} > sd-server-${{ matrix.settings.platform }}.sha256
- name: Create archive
run: |
cd dist
tar -czf sd-server-${{ matrix.settings.platform }}.tar.gz sd-server-${{ matrix.settings.platform }} sd-server-${{ matrix.settings.platform }}.sha256
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: server-${{ matrix.settings.platform }}
path: dist/sd-server-${{ matrix.settings.platform }}.tar.gz
# V2 Desktop builds
desktop-main:
strategy:
@@ -229,12 +294,12 @@ jobs:
if: always() && runner.os == 'macOS'
run: security delete-keychain signing_temp.keychain || true
# Create unified release with CLI and Desktop artifacts
# Create unified release with Server, CLI, and Desktop artifacts
release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: self-hosted
name: Create Release
needs: [desktop-main]
needs: [server-build, desktop-main]
permissions:
contents: write
steps:
@@ -247,8 +312,10 @@ jobs:
draft: true
files: |
cli-*/*
server-*/*
*/*.dmg
*/*.msi
*/*.deb
*/*.tar.xz
*/*.tar.gz
*/*.sig

View File

@@ -1,12 +1,13 @@
name: Server release
on:
release:
types: [published]
push:
tags:
- "v*"
pull_request:
paths:
- '.github/workflows/server.yml'
- 'apps/server/docker/*'
- 'apps/server/**'
workflow_dispatch:
jobs:
@@ -81,10 +82,12 @@ jobs:
shell: bash
run: |
set -euxo pipefail
if [ "$GITHUB_EVENT_NAME" == "release" ]; then
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
IMAGE_TAG="${GITHUB_REF##*/}"
EXTRA_TAG="latest"
else
IMAGE_TAG="$(git rev-parse --short "$GITHUB_SHA")"
EXTRA_TAG="staging"
fi
IMAGE_TAG="${IMAGE_TAG,,}"
IMAGE_NAME="${GITHUB_REPOSITORY,,}/server"
@@ -92,6 +95,7 @@ jobs:
echo "Building ${IMAGE_NAME}:${IMAGE_TAG}"
echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo "extra_tag=${EXTRA_TAG}" >> "$GITHUB_OUTPUT"
echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT"
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
echo "repo_ref=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
@@ -105,16 +109,16 @@ jobs:
id: build-image
uses: redhat-actions/buildah-build@v2
with:
tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'latest' || 'staging' }}
archs: amd64
tags: ${{ steps.image_info.outputs.tag }} ${{ steps.image_info.outputs.extra_tag }}
archs: amd64, arm64
image: ${{ steps.image_info.outputs.name }}
layers: 'false'
context: ./apps/server/docker
context: .
build-args: |
REPO=${{ steps.image_info.outputs.repo }}
REPO_REF=${{ steps.image_info.outputs.repo_ref }}
containerfiles: |
./apps/server/docker/Dockerfile
./apps/server/Dockerfile
- name: Push image to ghcr.io
uses: redhat-actions/push-to-registry@v2

158
DOCKER.md
View File

@@ -1,158 +0,0 @@
# Spacedrive Docker Deployment
Quick guide for running Spacedrive daemon in Docker.
## Quick Start
```bash
# Build the image
docker compose build
# Start daemon
docker compose up -d
# View logs
docker compose logs -f
# Check status
docker exec spacedrive-daemon sd-cli status
```
## Supported Platforms
- **x86_64** (amd64) - Servers, TrueNAS, Intel/AMD systems
- **ARM64** (aarch64) - Raspberry Pi 3/4/5, Apple Silicon (via emulation)
## Configuration
Edit `docker-compose.yml` to customize:
```yaml
volumes:
# Mount directories to index
- /path/to/your/photos:/mnt/photos:ro
- /path/to/your/documents:/mnt/docs:ro
environment:
# Optional: Set instance name
- SPACEDRIVE_INSTANCE=myserver
```
## CLI Access
```bash
# Run any CLI command
docker exec spacedrive-daemon sd-cli <command>
# Examples:
docker exec spacedrive-daemon sd-cli library list
docker exec spacedrive-daemon sd-cli location add /mnt/photos
docker exec spacedrive-daemon sd-cli search "vacation"
```
## Data Persistence
Data is stored in the `spacedrive-data` Docker volume. To backup:
```bash
# Backup volume
docker run --rm -v spacedrive-data:/data -v $(pwd):/backup \
alpine tar czf /backup/spacedrive-backup.tar.gz /data
# Restore volume
docker run --rm -v spacedrive-data:/data -v $(pwd):/backup \
alpine tar xzf /backup/spacedrive-backup.tar.gz -C /
```
## Building for Specific Platform
```bash
# Build for ARM64 (Raspberry Pi)
docker build --platform linux/arm64 -t spacedrive:arm64 .
# Build for x86_64
docker build --platform linux/amd64 -t spacedrive:amd64 .
```
## TrueNAS Deployment
1. Enable Apps in TrueNAS SCALE
2. Create custom app using the provided `docker-compose.yml`
3. Mount your pools as volumes:
```yaml
volumes:
- /mnt/pool1:/mnt/pool1:ro
- /mnt/pool2:/mnt/pool2:ro
```
## Troubleshooting
### Container exits immediately
Check logs:
```bash
docker compose logs
```
### Can't access daemon
Verify it's running:
```bash
docker ps
docker exec spacedrive-daemon sd-cli status
```
### Out of disk space
Check Docker disk usage:
```bash
docker system df
```
Clean up old data:
```bash
docker system prune
```
## Advanced
### Resource Limits
Add to `docker-compose.yml`:
```yaml
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
```
### Custom Data Directory
```yaml
volumes:
# Use host directory instead of volume
- /path/to/spacedrive/data:/data
```
### Network Access (Future API)
```yaml
ports:
- "8080:8080" # Expose API port
```
## Full Documentation
See [Linux Deployment Guide](./docs/cli/linux-deployment.mdx) for complete documentation including:
- Native binary installation
- Systemd service setup
- Raspberry Pi specific configuration
- TrueNAS integration
- Performance tuning
## Getting Help
- Documentation: https://docs.spacedrive.com
- GitHub Issues: https://github.com/spacedriveapp/spacedrive/issues

View File

@@ -1,76 +0,0 @@
# Multi-stage Dockerfile for Spacedrive CLI and Daemon
# Supports: x86_64 and aarch64 Linux
# ============================================================================
# Builder Stage - Compile Rust binaries
# ============================================================================
FROM rust:1.81-slim-bookworm AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
pkg-config \
libssl-dev \
git \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /build
# Copy workspace files
COPY Cargo.toml Cargo.lock ./
COPY apps/ apps/
COPY core/ core/
COPY crates/ crates/
COPY xtask/ xtask/
# Copy dependencies (specta, opendal)
COPY specta/ specta/
COPY opendal/ opendal/
# Build release binaries
# Note: We only build CLI features, no FFmpeg or AI models needed
RUN cargo build --release --bin sd-cli --bin sd-daemon
# ============================================================================
# Runtime Stage - Minimal image with only runtime dependencies
# ============================================================================
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -u 1000 spacedrive
# Create data directory
RUN mkdir -p /data && chown spacedrive:spacedrive /data
# Copy binaries from builder
COPY --from=builder /build/target/release/sd-cli /usr/local/bin/sd-cli
COPY --from=builder /build/target/release/sd-daemon /usr/local/bin/sd-daemon
# Set permissions
RUN chmod +x /usr/local/bin/sd-cli /usr/local/bin/sd-daemon
# Switch to non-root user
USER spacedrive
# Set data directory as volume
VOLUME /data
# Expose any ports if needed (future: add when API is enabled)
# EXPOSE 8080
# Set environment variables
ENV SPACEDRIVE_DATA_DIR=/data
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD sd-cli status || exit 1
# Default command: start daemon in foreground
CMD ["sd-cli", "--data-dir", "/data", "start", "--foreground"]

View File

@@ -135,7 +135,7 @@ Peer-to-peer synchronization without central coordinators. Device-specific data
**Apps**
- **CLI** - Command-line interface (available now)
- **Server** - Headless daemon for Docker deployment (available now)
- **Server** - Headless daemon for Docker deployment ([self-hosting guide](https://v2.spacedrive.com/overview/self-hosting))
- **Tauri** - Cross-platform desktop with React frontend (macOS and Linux now, Windows in alpha.2)
- **Web** - Web interface and shared UI components (available now)
- **Mobile** - React Native mobile app (iOS and Android coming soon)
@@ -331,6 +331,7 @@ Optional cloud integration (Spacedrive Cloud) is available for backup and remote
## Documentation
- **[v2 Documentation](https://v2.spacedrive.com)** - Complete guides and API reference
- **[Self-Hosting Guide](https://v2.spacedrive.com/overview/self-hosting)** - Deploy Spacedrive server
- **[Whitepaper](whitepaper/spacedrive.pdf)** - Technical architecture (work in progress)
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute
- **[Architecture Docs](docs/core/architecture.md)** - Detailed system design

302
SERVER_RELEASE_SETUP.md Normal file
View File

@@ -0,0 +1,302 @@
# Server Release Setup
This document explains the changes made to integrate Spacedrive server builds into the release workflow.
## Overview
The server app (`sd-server`) is now built and released in two formats:
1. **Static binaries** - For systemd, bare metal, and custom deployments
2. **Docker images** - For containerized deployments (Docker, Kubernetes, NAS systems)
Both are automatically built and published when a git tag is pushed (e.g., `v2.0.0-alpha.2`).
**Note:** The root `/Dockerfile` has been removed as it was redundant. The only Docker image for self-hosting is `apps/server/Dockerfile`, which builds the HTTP server with embedded daemon and full media processing support.
## Changes Made
### 1. Release Workflow (`.github/workflows/release.yml`)
**Added: `server-build` job**
Builds static server binaries for:
- `linux-x86_64` (Intel/AMD servers)
- `linux-aarch64` (ARM servers, Raspberry Pi, AWS Graviton)
**Features:**
- Full media processing support (`heif`, `ffmpeg` features enabled)
- Cross-compilation for ARM using `gcc-aarch64-linux-gnu`
- Checksums generated for each binary (SHA256)
- Archives created as `.tar.gz` for easy distribution
**Artifacts uploaded:**
- `sd-server-linux-x86_64.tar.gz`
- `sd-server-linux-aarch64.tar.gz`
**Updated: `release` job**
- Added `server-build` to dependencies
- Server artifacts now included in GitHub releases
- Pattern updated to include `.tar.gz` files
### 2. Server Docker Workflow (`.github/workflows/server.yml`)
**Changed trigger:**
- Old: `release: types: [published]`
- New: `push: tags: ["v*"]`
- Result: Docker images built at the same time as binaries
**Multi-arch support:**
- Old: `amd64` only
- New: `amd64` + `arm64`
- Uses QEMU for ARM cross-compilation
**Fixed paths:**
- Old: `context: ./apps/server/docker` (incorrect)
- New: `context: .` (repo root)
- Old: `containerfiles: ./apps/server/docker/Dockerfile` (incorrect)
- New: `containerfiles: ./apps/server/Dockerfile` (correct)
**Image tagging:**
- Git tags (e.g., `v2.0.0-alpha.2`) → `ghcr.io/spacedriveapp/spacedrive/server:v2.0.0-alpha.2` + `latest`
- Non-tagged commits → `ghcr.io/spacedriveapp/spacedrive/server:<commit-sha>` + `staging`
### 3. Server Dockerfile (`apps/server/Dockerfile`)
**Added media processing dependencies:**
Builder stage:
- `cmake`, `nasm` - Build tools for native dependencies
- `libavcodec-dev`, `libavformat-dev`, `libavutil-dev`, `libswscale-dev` - FFmpeg dev libraries
- `libheif-dev` - HEIF image format support
Runtime stage:
- Changed from `distroless/cc` to `debian:bookworm-slim`
- Installed runtime libraries: `libavcodec59`, `libavformat59`, `libavutil57`, `libswscale6`, `libheif1`
- Created `spacedrive` user (UID 1000) for security
**Enabled features in build:**
```dockerfile
cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg
```
This enables:
- Video thumbnail generation
- Audio transcription
- HEIF/HEIC image support
- All media processing capabilities
## Release Process
### Automated Release (Recommended)
1. **Tag a release:**
```bash
git tag v2.0.0-alpha.2
git push origin v2.0.0-alpha.2
```
2. **GitHub Actions automatically:**
- Builds server binaries (x86_64 + ARM)
- Builds desktop apps (macOS + Linux)
- Builds Docker images (amd64 + arm64)
- Creates draft GitHub release with all artifacts
3. **Review and publish:**
- Go to GitHub Releases
- Edit the draft release
- Add release notes
- Publish
### Manual Testing
**Test static binary build:**
```bash
# From project root
cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg
# Test locally
./target/release/sd-server --data-dir /tmp/sd-test
```
**Test Docker build:**
```bash
# From project root
docker build -f apps/server/Dockerfile -t sd-server-test .
# Run locally
docker run -p 8080:8080 -e SD_AUTH=admin:test sd-server-test
```
**Test multi-arch Docker build:**
```bash
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-f apps/server/Dockerfile \
-t sd-server-multiarch \
.
```
## Deployment Options
### Option 1: Static Binary (systemd)
```bash
# Download from GitHub release
wget https://github.com/spacedriveapp/spacedrive/releases/download/v2.0.0-alpha.2/sd-server-linux-x86_64.tar.gz
tar -xzf sd-server-linux-x86_64.tar.gz
# Verify checksum
sha256sum -c sd-server-linux-x86_64.sha256
# Install
sudo mv sd-server-linux-x86_64 /usr/local/bin/sd-server
sudo chmod +x /usr/local/bin/sd-server
# Create systemd service
sudo nano /etc/systemd/system/spacedrive.service
```
Example systemd unit:
```ini
[Unit]
Description=Spacedrive Server
After=network.target
[Service]
Type=simple
User=spacedrive
Environment="DATA_DIR=/var/lib/spacedrive"
Environment="SD_AUTH=admin:your-secure-password"
ExecStart=/usr/local/bin/sd-server --data-dir /var/lib/spacedrive
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
### Option 2: Docker
```bash
docker run -d \
--name spacedrive \
-p 8080:8080 \
-p 7373:7373 \
-v spacedrive-data:/data \
-e SD_AUTH=admin:password \
ghcr.io/spacedriveapp/spacedrive/server:latest
```
### Option 3: Docker Compose
Use the provided `docker-compose.yml` in `apps/server/`:
```bash
cd apps/server
docker-compose up -d
```
Or create your own:
```yaml
version: '3.8'
services:
spacedrive:
image: ghcr.io/spacedriveapp/spacedrive/server:latest
ports:
- "8080:8080"
- "7373:7373"
volumes:
- spacedrive-data:/data
- /mnt/storage:/storage:ro # Optional: mount storage
environment:
SD_AUTH: "admin:your-password"
TZ: "America/New_York"
restart: unless-stopped
volumes:
spacedrive-data:
```
## Architecture Support
| Platform | Binary | Docker |
|----------|--------|--------|
| Linux x86_64 (Intel/AMD) | ✅ | ✅ |
| Linux ARM64 (Raspberry Pi, AWS Graviton) | ✅ | ✅ |
| macOS | ❌ (desktop app only) | ❌ |
| Windows | ❌ (desktop app only) | ❌ |
## Verify Release Artifacts
After a release is created, verify these files exist:
**Server binaries:**
- `sd-server-linux-x86_64.tar.gz`
- `sd-server-linux-aarch64.tar.gz`
**Desktop apps:**
- `Spacedrive_<version>_aarch64.dmg` (macOS ARM)
- `Spacedrive_<version>_amd64.deb` (Linux)
- `dist.tar.xz` (frontend assets)
**Docker images:**
Check ghcr.io:
```bash
docker pull ghcr.io/spacedriveapp/spacedrive/server:v2.0.0-alpha.2
docker pull ghcr.io/spacedriveapp/spacedrive/server:latest
```
## Next Steps
### Potential Enhancements
1. **Add Windows server binary** - Build `sd-server.exe` for Windows Server deployments
2. **Package formats** - Create `.deb` and `.rpm` packages for easier installation
3. **ARM macOS** - Server binary for macOS (though desktop app is preferred)
4. **Static linking** - Fully static binaries using `musl` for maximum compatibility
5. **Checksums in release notes** - Auto-generate checksums table in release description
### Documentation Updates Needed
- [x] Add "Self-Hosting Guide" to docs (`docs/overview/self-hosting.mdx`)
- [x] Update main README with server deployment links
- [ ] Create TrueNAS app manifest for one-click install
- [ ] Write Unraid template
## Troubleshooting
### Build fails with "media features not found"
The workflow now automatically includes `sd-core/heif` and `sd-core/ffmpeg` features. If this fails:
- Check native dependencies are installed (cmake, nasm, FFmpeg dev packages)
- Verify setup-system action runs successfully
### Docker image size too large
Current runtime image uses `debian:bookworm-slim` (~80-100MB base) plus runtime libraries.
To reduce size:
- Consider Alpine Linux base (smaller but more complex dependencies)
- Use distroless and manually copy .so files (more brittle)
- Current approach prioritizes reliability over size
### Multi-arch build timeout
ARM builds can be slow via QEMU emulation. Options:
- Use native ARM runners (more expensive)
- Build in parallel jobs
- Cache build artifacts more aggressively
### Permission denied in container
The container runs as user `spacedrive` (UID 1000). If mounting volumes:
```bash
# Fix permissions
sudo chown -R 1000:1000 /path/to/data
```
Or run as root (not recommended):
```bash
docker run --user root ...
```

View File

@@ -11,7 +11,14 @@ RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/
git \
pkg-config \
libssl-dev \
ca-certificates
ca-certificates \
cmake \
nasm \
libavcodec-dev \
libavformat-dev \
libavutil-dev \
libswscale-dev \
libheif-dev
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
@@ -28,17 +35,32 @@ COPY core ./core
COPY crates ./crates
COPY apps/server ./apps/server
# Build server (RPC only, no assets)
# Build server with media processing features
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/root/.cargo/git \
--mount=type=cache,target=/build/target \
cargo build --release -p sd-server && \
cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg && \
cp target/release/sd-server /usr/local/bin/sd-server
#--
# Runtime image
#--
FROM gcr.io/distroless/cc-debian12:nonroot
FROM debian:bookworm-slim
# Install runtime dependencies only
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y \
libssl3 \
ca-certificates \
libavcodec59 \
libavformat59 \
libavutil57 \
libswscale6 \
libheif1 \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -u 1000 spacedrive
# Copy binary
COPY --from=builder /usr/local/bin/sd-server /usr/bin/sd-server
@@ -58,7 +80,7 @@ EXPOSE 7373
VOLUME ["/data"]
# Run as non-root user
USER nonroot:nonroot
USER spacedrive:spacedrive
# Start server
ENTRYPOINT ["/usr/bin/sd-server"]

View File

@@ -36,7 +36,7 @@ impl ActionManager {
// Check if confirmation is required
match validation_result {
super::ValidationResult::Success => {
super::ValidationResult::Success { .. } => {
// Proceed with execution
}
super::ValidationResult::RequiresConfirmation(_request) => {
@@ -95,7 +95,7 @@ impl ActionManager {
// Check if confirmation is required
match validation_result {
super::ValidationResult::Success => {
super::ValidationResult::Success { .. } => {
// Proceed with execution
}
super::ValidationResult::RequiresConfirmation(_request) => {

View File

@@ -22,7 +22,11 @@ pub mod receipt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationResult {
/// The action is valid and can proceed without user interaction.
Success,
Success {
/// Optional metadata for rich UI display (strategy info, file counts, etc.)
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
},
/// The action is valid, but requires user confirmation to proceed.
RequiresConfirmation(ConfirmationRequest),
}
@@ -64,7 +68,7 @@ pub trait CoreAction: Send + Sync + 'static {
) -> impl std::future::Future<
Output = Result<ValidationResult, crate::infra::action::error::ActionError>,
> + Send {
async { Ok(ValidationResult::Success) }
async { Ok(ValidationResult::Success { metadata: None }) }
}
/// Resolve a user confirmation choice (optional)
@@ -112,7 +116,7 @@ pub trait LibraryAction: Send + Sync + 'static {
) -> impl std::future::Future<
Output = Result<ValidationResult, crate::infra::action::error::ActionError>,
> + Send {
async { Ok(ValidationResult::Success) }
async { Ok(ValidationResult::Success { metadata: None }) }
}
/// Resolve a user confirmation choice (optional)

View File

@@ -321,10 +321,10 @@ impl LibraryAction for FileCopyAction {
"strategy": strategy_metadata,
"file_count": file_count,
"total_bytes": total_bytes,
"conflicts": conflicts.iter().map(|c| {
"conflicts": conflicts.iter().map(|(source, dest)| {
json!({
"source": c.to_string_lossy(),
"destination": c.to_string_lossy(),
"source": source.to_string_lossy(),
"destination": dest.to_string_lossy(),
})
}).collect::<Vec<_>>(),
"is_fast_operation": strategy_metadata.is_fast_operation,
@@ -357,7 +357,9 @@ impl LibraryAction for FileCopyAction {
// If it's a fast operation with no conflicts, return success with metadata
// Frontend can use this to decide whether to show a modal or auto-proceed
Ok(ValidationResult::Success)
Ok(ValidationResult::Success {
metadata: Some(metadata),
})
}
fn resolve_confirmation(&mut self, choice_index: usize) -> Result<(), ActionError> {
@@ -474,8 +476,8 @@ impl FileCopyAction {
Ok((count, size))
}
/// Check for all file conflicts and return list of conflicting paths
async fn check_for_conflicts_detailed(&self) -> Result<Vec<PathBuf>, ActionError> {
/// Check for all file conflicts and return list of conflicting (source, destination) pairs
async fn check_for_conflicts_detailed(&self) -> Result<Vec<(PathBuf, PathBuf)>, ActionError> {
let mut conflicts = Vec::new();
let dest_path = match self.destination.as_local_path() {
@@ -501,7 +503,7 @@ impl FileCopyAction {
// Check if this would conflict
if actual_dest.exists() {
conflicts.push(actual_dest);
conflicts.push((source_path.to_path_buf(), actual_dest));
}
}
}
@@ -512,7 +514,7 @@ impl FileCopyAction {
/// Check if any destination files would cause conflicts (legacy method)
async fn check_for_conflicts(&self) -> Result<Option<PathBuf>, ActionError> {
let conflicts = self.check_for_conflicts_detailed().await?;
Ok(conflicts.into_iter().next())
Ok(conflicts.into_iter().next().map(|(_, dest)| dest))
}
/// Generate a unique destination path by appending a number if the original exists

View File

@@ -915,6 +915,13 @@ impl ToGenericProgress for CopyProgress {
progress = progress.with_current_path(path.clone());
}
// Add strategy metadata for UI display
if let Some(ref strategy_metadata) = self.strategy_metadata {
progress = progress.with_metadata(serde_json::json!({
"strategy": strategy_metadata
}));
}
progress
}
}

View File

@@ -87,7 +87,7 @@ impl LibraryAction for CreateFolderAction {
}
}
Ok(ValidationResult::Success)
Ok(ValidationResult::Success { metadata: None })
}
async fn execute(

View File

@@ -85,7 +85,7 @@ impl LibraryAction for FileDeleteAction {
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -72,7 +72,7 @@ impl LibraryAction for FileRenameAction {
_ => {}
}
Ok(ValidationResult::Success)
Ok(ValidationResult::Success { metadata: None })
}
async fn execute(

View File

@@ -75,7 +75,7 @@ impl LibraryAction for IndexingAction {
message: errors.join("; "),
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
async fn execute(

View File

@@ -77,7 +77,7 @@ impl CoreAction for LibraryCreateAction {
message: "Library name cannot be empty".to_string(),
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -118,7 +118,7 @@ impl CoreAction for LibraryOpenAction {
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -87,7 +87,7 @@ impl LibraryAction for LibraryRenameAction {
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -191,7 +191,7 @@ impl LibraryAction for LocationAddAction {
// Check for duplicate locations
// TODO: Implement proper duplicate detection for both Physical and Cloud paths
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -191,7 +191,7 @@ impl LibraryAction for EnableIndexingAction {
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -58,7 +58,7 @@ impl LibraryAction for LocationImportAction {
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
async fn execute(

View File

@@ -235,7 +235,7 @@ impl LibraryAction for LocationTriggerJobAction {
return Err(ActionError::LocationNotFound(self.input.location_id));
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -115,7 +115,7 @@ impl LibraryAction for LocationUpdateAction {
return Err(ActionError::LocationNotFound(self.input.id));
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -109,7 +109,7 @@ impl CoreAction for LibrarySyncSetupAction {
});
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}
@@ -221,7 +221,7 @@ impl LibrarySyncSetupAction {
);
}
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
/// Execute ShareLocalLibrary action - share local library to remote device

View File

@@ -113,7 +113,7 @@ impl LibraryAction for AddGroupAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -148,7 +148,7 @@ impl LibraryAction for AddItemAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -110,7 +110,7 @@ impl LibraryAction for SpaceCreateAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -59,7 +59,7 @@ impl LibraryAction for SpaceDeleteAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -69,7 +69,7 @@ impl LibraryAction for DeleteGroupAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -68,7 +68,7 @@ impl LibraryAction for DeleteItemAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -59,7 +59,7 @@ impl LibraryAction for ReorderGroupsAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}
@@ -110,7 +110,7 @@ impl LibraryAction for ReorderItemsAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -108,7 +108,7 @@ impl LibraryAction for SpaceUpdateAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -107,7 +107,7 @@ impl LibraryAction for UpdateGroupAction {
_library: &std::sync::Arc<crate::library::Library>,
_context: std::sync::Arc<CoreContext>,
) -> Result<crate::infra::action::ValidationResult, ActionError> {
Ok(crate::infra::action::ValidationResult::Success)
Ok(crate::infra::action::ValidationResult::Success { metadata: None })
}
}

View File

@@ -1,38 +0,0 @@
version: '3.8'
services:
spacedrive:
build:
context: .
dockerfile: Dockerfile
image: spacedrive:latest
container_name: spacedrive-daemon
restart: unless-stopped
# Data persistence
volumes:
- spacedrive-data:/data
# Optional: mount host directories to index
# - /path/to/your/files:/mnt/files:ro
# Environment variables
environment:
- SPACEDRIVE_DATA_DIR=/data
# Optional: Set instance name
# - SPACEDRIVE_INSTANCE=default
# Optional: Expose ports when API is enabled
# ports:
# - "8080:8080"
# Health check
healthcheck:
test: ["CMD", "sd-cli", "status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
spacedrive-data:
driver: local

View File

@@ -57,6 +57,7 @@
"icon": "compass",
"pages": [
"overview/get-started",
"overview/self-hosting",
"overview/backup-photos-ios",
"overview/manage-libraries",
"overview/add-index-locations"

View File

@@ -0,0 +1,317 @@
---
title: Self-Hosting Spacedrive
description: Deploy Spacedrive server for remote access and headless operation
sidebarTitle: Self-Hosting
---
Self-hosting Spacedrive means running the server component on hardware you control. The server provides HTTP access to your libraries, enabling remote connections from desktop and mobile apps. Unlike the desktop app which requires a GUI, the server runs headless on Linux systems, NAS devices, or cloud instances.
The server embeds the full Spacedrive daemon with media processing capabilities. It exposes RPC endpoints over HTTP with optional basic authentication. Your data stays on your infrastructure while remaining accessible from any device with network access.
## Installation Methods
### Static Binary
Download the latest release binary for your platform. This method works on any Linux system and requires no container runtime.
```bash
# Download and extract
wget https://github.com/spacedriveapp/spacedrive/releases/latest/download/sd-server-linux-x86_64.tar.gz
tar -xzf sd-server-linux-x86_64.tar.gz
# Verify checksum
sha256sum -c sd-server-linux-x86_64.sha256
# Install to system
sudo mv sd-server-linux-x86_64 /usr/local/bin/sd-server
sudo chmod +x /usr/local/bin/sd-server
```
For ARM systems like Raspberry Pi, use `sd-server-linux-aarch64.tar.gz` instead.
### Docker Deployment
Docker images are available for both x86_64 and ARM64 architectures. The image includes all media processing dependencies and runs as a non-root user.
```bash
docker run -d \
--name spacedrive \
-p 8080:8080 \
-p 7373:7373 \
-v spacedrive-data:/data \
-e SD_AUTH=admin:your-secure-password \
ghcr.io/spacedriveapp/spacedrive/server:latest
```
The server listens on port 8080 for HTTP traffic and port 7373 for peer-to-peer connections. Mount additional volumes to index existing storage.
### Docker Compose
For production deployments, use the provided compose file in the repository at `apps/server/docker-compose.yml`. This includes health checks and restart policies.
```yaml
version: '3.8'
services:
spacedrive:
image: ghcr.io/spacedriveapp/spacedrive/server:latest
ports:
- "8080:8080"
- "7373:7373"
volumes:
- spacedrive-data:/data
- /mnt/media:/media:ro
environment:
SD_AUTH: "admin:your-password"
TZ: "America/New_York"
restart: unless-stopped
volumes:
spacedrive-data:
```
## System Service Setup
Running the server as a systemd service ensures it starts on boot and restarts on failure.
Create a service file at `/etc/systemd/system/spacedrive.service`:
```ini
[Unit]
Description=Spacedrive Server
After=network.target
[Service]
Type=simple
User=spacedrive
Group=spacedrive
Environment="DATA_DIR=/var/lib/spacedrive"
Environment="SD_AUTH=admin:your-secure-password"
ExecStart=/usr/local/bin/sd-server --data-dir /var/lib/spacedrive
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
Create the user and data directory, then enable the service:
```bash
sudo useradd -r -s /bin/false spacedrive
sudo mkdir -p /var/lib/spacedrive
sudo chown spacedrive:spacedrive /var/lib/spacedrive
sudo systemctl enable --now spacedrive
```
Check status with `sudo systemctl status spacedrive`.
## Configuration
The server accepts configuration via environment variables or command-line flags.
**Authentication** is controlled by `SD_AUTH`. The format supports multiple users separated by commas: `user1:pass1,user2:pass2`. Set to `disabled` to disable authentication, though this is not recommended for network-accessible deployments.
**Data directory** defaults to `/data` in Docker or a temporary directory in development. In production, always set `DATA_DIR` to a persistent location. The server creates library databases, thumbnails, and logs in this directory.
**Port configuration** defaults to 8080. Change with `--port` flag or `PORT` environment variable. The P2P port (7373) is fixed and required for device sync.
<Warning>
Production deployments must set `SD_AUTH`. The server refuses to start without authentication unless explicitly disabled. This prevents accidental exposure of your libraries.
</Warning>
## Network Access
The server binds to all interfaces by default, making it accessible on your local network. For internet access, place it behind a reverse proxy with TLS termination.
### Nginx Example
```nginx
server {
listen 443 ssl http2;
server_name spacedrive.example.com;
ssl_certificate /etc/letsencrypt/live/spacedrive.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/spacedrive.example.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### Caddy Example
Caddy handles TLS automatically with Let's Encrypt:
```
spacedrive.example.com {
reverse_proxy localhost:8080
}
```
## NAS Deployment
Network-attached storage devices are ideal for Spacedrive server. The server indexes existing media while providing remote access.
### TrueNAS SCALE
Install via the Apps interface using Docker. Map your pools as read-only volumes to prevent accidental modification during indexing.
```yaml
volumes:
- /mnt/pool1/photos:/photos:ro
- /mnt/pool1/videos:/videos:ro
- /mnt/pool2/documents:/documents:ro
```
After deployment, create locations in Spacedrive pointing to these mount points. The server indexes files without moving them.
### Synology NAS
Use Container Manager to deploy the Docker image. Configure port forwarding and volume mounts through the UI. Ensure the container has read access to your shared folders.
### Unraid
Add a custom container template with the image `ghcr.io/spacedriveapp/spacedrive/server:latest`. Map your shares as additional paths in the container configuration.
## Storage Considerations
The server creates three types of data in the data directory:
**Libraries** contain SQLite databases with file indexes, tags, and metadata. Each library is a separate `.sdlibrary` directory. These are small, typically under 100MB even for millions of files.
**Sidecars** include thumbnails, previews, and extracted text. Size depends on media processing settings. Budget 1-5% of your total media size for thumbnails.
**Logs** rotate automatically but can grow large during initial indexing. The server keeps the last 10 log files, typically under 100MB total.
Plan for library growth over time. A library indexing 1 million files uses approximately 500MB for the database and 10-50GB for sidecars, depending on media density.
<Info>
The server never modifies your original files. Indexing is read-only. File operations like copy and move require explicit user action through connected clients.
</Info>
## Security Best Practices
Self-hosted servers face different threats than desktop apps. Follow these guidelines for secure deployment.
**Always use authentication** when the server is network-accessible. HTTP basic auth is sufficient for home networks but consider stronger methods for internet-facing deployments.
**Enable TLS** for any internet access. Let's Encrypt provides free certificates through Caddy, Nginx, or Traefik. The server itself does not handle TLS, rely on reverse proxies.
**Limit exposure** with firewall rules. Only open port 8080 to trusted networks or VPN connections. The P2P port (7373) requires UDP ingress for device sync but can be restricted to known peers.
**Run as non-root** user. The Docker image and systemd example both use dedicated users with minimal permissions. Never run the server as root.
**Regular backups** of the data directory protect against corruption. The SQLite databases can be backed up while running using the `.backup` command or by copying the entire data directory when the server is stopped.
## Connecting Clients
Desktop and mobile apps can connect to self-hosted servers by configuring the server URL in settings.
The URL format is `http://your-server:8080` or `https://spacedrive.example.com` if using a reverse proxy. Clients authenticate using credentials set in `SD_AUTH`.
P2P sync works between the server and other devices without additional configuration. The server appears as a device in your library and participates in sync like any other device.
## Monitoring
The server provides a health check endpoint at `/health` that returns HTTP 200 when operational. Use this for monitoring tools or container orchestration health probes.
Logs are written to stdout and the data directory under `logs/`. Set `RUST_LOG` environment variable to `debug` for detailed output during troubleshooting.
**Key metrics to monitor:**
Disk usage in the data directory grows with library size. Alert when free space drops below 20% to prevent database corruption.
Memory usage typically ranges from 100-500MB depending on active jobs. Spike during intensive operations like thumbnail generation.
Network bandwidth peaks during initial sync with new devices. Ongoing sync uses minimal bandwidth after the first synchronization.
## Troubleshooting
**Server won't start:**
Check that the data directory exists and is writable by the server user. Verify no other process is using port 8080 with `lsof -i :8080`. Review logs for specific errors.
**Authentication failures:**
Ensure `SD_AUTH` format is correct. Test credentials with curl: `curl -u admin:password http://localhost:8080/health`. Browser authentication dialogs require exact username and password.
**Clients can't connect:**
Verify the server is accessible from the client network. Test with `curl http://server-ip:8080/health`. Check firewall rules on both server and client networks. Ensure reverse proxy configuration preserves authentication headers.
**Media processing fails:**
The server requires FFmpeg and libheif for video thumbnails and HEIF images. Docker images include these by default. Binary installations on minimal systems may need manual installation of these libraries.
**Sync not working:**
UDP port 7373 must be accessible for P2P connections. Check NAT and firewall rules. The server attempts hole-punching for NAT traversal but may fall back to relay servers if direct connection fails.
## Performance Tuning
The server handles multiple concurrent operations through the job system. Performance depends on storage speed, available memory, and indexing load.
**Indexing performance** is limited by storage I/O. SSDs provide 10-50x faster indexing than spinning disks. Network storage over gigabit ethernet typically indexes at 50-100MB/s for metadata reads.
**Thumbnail generation** is CPU-bound. Disable thumbnail generation for video files if CPU usage is a concern. Image thumbnails are fast but video processing can use significant resources.
**Database optimization** happens automatically through SQLite's auto-vacuum and WAL mode. Large libraries benefit from periodic `VACUUM` operations when the server is idle.
**Memory allocation** can be limited using systemd or Docker resource constraints. The server operates efficiently with 512MB minimum, though 2GB provides better performance for large libraries.
## Backup Strategy
Back up the data directory regularly to prevent data loss. The recommended approach depends on your infrastructure.
**Systemd deployments** can use rsync or similar tools while the server is running. SQLite WAL mode ensures consistent reads during backup.
**Docker deployments** should back up named volumes. Stop the container, copy the volume contents, then restart. Alternatively, use volume backup tools that handle running containers.
**Cloud backups** of library databases are small enough for frequent uploads. Sidecar data can be regenerated from source files if needed, reducing backup size.
Test restore procedures periodically. A backup is only useful if you can restore it successfully.
## Migration
Moving an existing library to a new server requires copying the data directory and adjusting location paths.
Copy the entire data directory to the new server. Start the server and verify the library loads. Update location paths if the mount points differ on the new system.
File content identity ensures files are recognized even if paths change. The server re-indexes locations with new paths but preserves tags, collections, and metadata.
## Updates
The server receives updates through new Docker image tags or binary releases. Always review release notes before updating to understand breaking changes.
**Docker updates:**
Pull the new image and recreate the container. Data persists in volumes across container replacements.
```bash
docker pull ghcr.io/spacedriveapp/spacedrive/server:latest
docker stop spacedrive
docker rm spacedrive
# Run docker run command with new image
```
**Binary updates:**
Download the new release, verify checksums, and replace the binary. Restart the systemd service.
```bash
sudo systemctl stop spacedrive
sudo mv sd-server-new /usr/local/bin/sd-server
sudo systemctl start spacedrive
```
Database migrations run automatically on first start after update. Check logs to confirm successful migration.
## Support
Report issues on [GitHub](https://github.com/spacedriveapp/spacedrive/issues) with server logs and deployment details. Join [Discord](https://discord.gg/gTaF2Z44f5) for community support and deployment advice.

View File

@@ -98,6 +98,7 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) {
pause={pause}
resume={resume}
cancel={cancel}
getSpeedHistory={getSpeedHistory}
/>
)}
</Popover>
@@ -111,6 +112,7 @@ function JobManagerPopoverContent({
pause,
resume,
cancel,
getSpeedHistory,
}: {
jobs: any[];
showOnlyRunning: boolean;
@@ -118,6 +120,7 @@ function JobManagerPopoverContent({
pause: (jobId: string) => Promise<void>;
resume: (jobId: string) => Promise<void>;
cancel: (jobId: string) => Promise<void>;
getSpeedHistory: (jobId: string) => import("./hooks/useJobs").SpeedSample[];
}) {
const filteredJobs = showOnlyRunning
? jobs.filter((job) => job.status === "running" || job.status === "paused")

View File

@@ -19,7 +19,7 @@ interface JobCardProps {
onPause?: (jobId: string) => void;
onResume?: (jobId: string) => void;
onCancel?: (jobId: string) => void;
getSpeedHistory: (jobId: string) => SpeedSample[];
getSpeedHistory?: (jobId: string) => SpeedSample[];
}
export function JobCard({ job, onPause, onResume, onCancel, getSpeedHistory }: JobCardProps) {
@@ -143,7 +143,7 @@ export function JobCard({ job, onPause, onResume, onCancel, getSpeedHistory }: J
{/* Expanded details section */}
<AnimatePresence>
{isExpanded && isCopyJob && (
{isExpanded && isCopyJob && getSpeedHistory && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useMemo } from "react";
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { useLibraryQuery, useLibraryMutation, useSpacedriveClient } from "../../../contexts/SpacedriveContext";
import type { JobListItem } from "../types";
import { sounds } from "@sd/assets/sounds";
@@ -211,10 +211,10 @@ export function useJobs() {
const runningCount = jobs.filter((j) => j.status === "running").length;
const pausedCount = jobs.filter((j) => j.status === "paused").length;
// Helper to get speed history for a job
const getSpeedHistory = (jobId: string): SpeedSample[] => {
// Helper to get speed history for a job (memoized to prevent re-creation)
const getSpeedHistory = useCallback((jobId: string): SpeedSample[] => {
return speedHistoryRef.current.get(jobId) || [];
};
}, []);
return {
jobs,

View File

@@ -286,7 +286,7 @@ function DeviceCard({
}: DeviceCardProps) {
const deviceName = device?.name || "Unknown Device";
const deviceIconSrc = device ? getDeviceIcon(device) : null;
const { pause, resume } = useJobs();
const { pause, resume, getSpeedHistory } = useJobs();
// Format hardware specs
const cpuInfo = device?.cpu_model
@@ -386,6 +386,7 @@ function DeviceCard({
job={job}
onPause={pause}
onResume={resume}
getSpeedHistory={getSpeedHistory}
/>
))}
</div>