20 Commits

Author SHA1 Message Date
f2d42751fd Added systemd unit
All checks were successful
Build and Publish Deb Package / build-deb (push) Successful in 33s
Publish Server Image / build-and-push-image (push) Successful in 9m16s
2026-03-13 16:42:44 +00:00
bc34b6bc41 Added systemd unit
All checks were successful
Build and Publish Deb Package / build-deb (push) Successful in 2m17s
Publish Server Image / build-and-push-image (push) Successful in 4m35s
2026-03-13 16:31:58 +00:00
3ee7235b51 Fixed deb CI
Some checks failed
Build and Publish Deb Package / build-deb (push) Successful in 1m19s
Publish Server Image / build-and-push-image (push) Has been cancelled
2026-03-13 16:18:08 +00:00
1f85d9c435 Fixed deb CI
Some checks failed
Build and Publish Deb Package / build-deb (push) Failing after 1m58s
Publish Server Image / build-and-push-image (push) Has been cancelled
2026-03-13 16:12:53 +00:00
773e9c1ee7 Added deb build
Some checks failed
Build and Publish Deb Package / build-deb (push) Failing after 9m5s
Publish Server Image / build-and-push-image (push) Has been cancelled
2026-03-13 15:54:08 +00:00
0242376a65 Added deb build
Some checks failed
Build and Publish Deb Package / build-deb (push) Has been cancelled
Publish Server Image / build-and-push-image (push) Has been cancelled
2026-03-13 15:52:17 +00:00
Ultradesu
64f292c7b1 Added win client plan 2026-03-11 14:28:10 +00:00
ab
73b6d7483e Update README.md 2026-03-11 14:17:07 +00:00
Ultradesu
64b59ba72d fix macos clinet unmounting 2026-03-11 10:42:19 +00:00
Ultradesu
c54af23845 Fix logging
Some checks failed
Publish Server Image / build-and-push-image (push) Has been cancelled
2026-03-11 10:29:31 +00:00
Ultradesu
dd3f3721b2 Adjust logging. Added tcp ping 2026-03-11 10:12:57 +00:00
Ultradesu
dc77933c9e Fix active streams metric
All checks were successful
Publish Server Image / build-and-push-image (push) Successful in 4m50s
2026-03-11 01:39:29 +00:00
Ultradesu
848d1643a5 Fix CI
All checks were successful
Publish Server Image / build-and-push-image (push) Successful in 5m3s
2026-03-11 00:56:10 +00:00
Ultradesu
8afd942aad Fix CI
Some checks failed
Publish Server Image / build-and-push-image (push) Failing after 3m18s
2026-03-11 00:28:15 +00:00
Ultradesu
7aee798a01 Fix CI
Some checks failed
Publish Server Image / build-and-push-image (push) Failing after 42s
2026-03-11 00:26:56 +00:00
Ultradesu
3a31173046 Fixed Dockerfile
Some checks failed
Publish Server Image / build-and-push-image (push) Failing after 34s
2026-03-11 00:25:21 +00:00
Ultradesu
b0fc1fa92c Added Dockerfile
Some checks failed
Publish Server Image / build-and-push-image (push) Failing after 25s
2026-03-11 00:23:04 +00:00
Ultradesu
5cd53f277b Added CI
Some checks failed
Publish Server Image / build-and-push-image (push) Failing after 2m21s
2026-03-11 00:19:13 +00:00
Ultradesu
80bfc69c0b Added macos client 2026-03-11 00:11:00 +00:00
Ultradesu
dc4fbd7407 Added macos client 2026-03-11 00:09:10 +00:00
19 changed files with 335 additions and 22 deletions

63
.github/workflows/deb-publish.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Build and Publish Deb Package
on:
push:
tags:
- 'v*.*.*'
jobs:
build-deb:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable universe and install dependencies
run: |
sudo add-apt-repository universe -y
sudo apt-get update
sudo apt-get install -y fuse3 libfuse3-dev pkg-config protobuf-compiler cmake
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Install cargo-deb
run: cargo install cargo-deb --locked
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Build deb package
run: |
cargo deb -p furumi-mount-linux \
--deb-version ${{ steps.version.outputs.version }}
- name: Locate deb file
id: deb
run: |
DEB=$(ls target/debian/furumi-mount-linux_*.deb | head -1)
echo "path=$DEB" >> "$GITHUB_OUTPUT"
echo "name=$(basename $DEB)" >> "$GITHUB_OUTPUT"
- name: Publish to Gitea APT registry
run: |
TARGET_URL="${{ secrets.PKG_REGISTRY_URL }}/api/packages/${{ secrets.PKG_OWNER }}/debian/pool/noble/main/upload"
echo "Uploading to: $TARGET_URL"
curl --fail-with-body \
--user "${{ secrets.PKG_USER }}:${{ secrets.PKG_TOKEN }}" \
--upload-file "${{ steps.deb.outputs.path }}" \
"$TARGET_URL"

50
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Publish Server Image
on:
push:
tags:
- 'v*.*.*'
env:
REGISTRY: docker.io
IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/furumi-server
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -35,7 +35,7 @@ A Rust library containing all OS-agnostic logic:
#### B. OS-Specific Mount Layer
Thin executable wrappers that consume the Shared Client Core and handle OS integration:
- **Linux:** Uses the `fuser` crate (binds to `libfuse`) to mount and handle events from `/dev/fuse`.
- **macOS:** Can utilize `macFUSE` via the `fuser` crate initially. Future iterations may explore native Apple `FileProvider` frameworks using FFI to bypass third-party kext requirements.
- **macOS:** Acts as a lightweight local NFSv3/v4 server (`nfssrv` or similar crate). The system natively mounts `localhost:/` via the built-in NFS client, avoiding any need for third-party kernel extensions (like `macFUSE`) or complex FileProvider bindings.
- **Windows (Future):** Will wrap libraries like `WinFSP` or `dokany` to integrate with the Windows internal VFS.
## API Design (Read-Only Foundation)

10
Cargo.lock generated
View File

@@ -585,7 +585,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "furumi-client-core"
version = "0.1.0"
version = "0.2.1"
dependencies = [
"anyhow",
"async-trait",
@@ -607,7 +607,7 @@ dependencies = [
[[package]]
name = "furumi-common"
version = "0.1.0"
version = "0.2.1"
dependencies = [
"prost",
"protobuf-src",
@@ -617,7 +617,7 @@ dependencies = [
[[package]]
name = "furumi-mount-linux"
version = "0.1.0"
version = "0.2.1"
dependencies = [
"anyhow",
"clap",
@@ -634,7 +634,7 @@ dependencies = [
[[package]]
name = "furumi-mount-macos"
version = "0.1.0"
version = "0.2.1"
dependencies = [
"anyhow",
"async-trait",
@@ -652,7 +652,7 @@ dependencies = [
[[package]]
name = "furumi-server"
version = "0.1.0"
version = "0.2.1"
dependencies = [
"anyhow",
"async-stream",

43
Dockerfile Normal file
View File

@@ -0,0 +1,43 @@
# Stage 1: Build the rust binary
FROM rust:1.88.0-bookworm AS builder
# Install supplementary dependencies that might be needed
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
protobuf-compiler \
cmake \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
# Option: Copy in root workspace files and source crates
COPY . .
# Build only the server for release
RUN cargo build --release --bin furumi-server
# Stage 2: Create the minimal runtime image
FROM debian:bookworm-slim
# Install system dependencies needed at runtime (like OpenSSL if dynamically linked)
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user
RUN useradd -ms /bin/bash appuser
WORKDIR /home/appuser
# Copy the binary from the builder stage
COPY --from=builder /usr/src/app/target/release/furumi-server /usr/local/bin/furumi-server
USER appuser
# Expose ports: 50051 (gRPC) and 9090 (Metrics)
EXPOSE 50051
EXPOSE 9090
# Command to run the server
ENTRYPOINT ["furumi-server"]

View File

@@ -7,12 +7,13 @@ Designed for streaming media (video, music) over the network.
## Architecture
```
furumi-server (gRPC + TLS) ←→ furumi-client-core ←→ furumi-mount-linux (FUSE)
furumi-server (gRPC + TLS) ←→ furumi-client-core ←→ furumi-mount-{linux,macos} (FUSE / NFS)
```
- **furumi-server** — exposes a directory over gRPC with auto-TLS, Bearer token auth, and Prometheus metrics
- **furumi-client-core** — cross-platform gRPC client library with attribute caching
- **furumi-mount-linux** — mounts the remote directory locally via FUSE (read-only)
- **furumi-mount-macos** — mounts the remote directory locally via a local NFS server (read-only)
## Quick Start
@@ -26,17 +27,23 @@ cargo build --release --workspace
--token mysecrettoken \
--tls-cert-out /tmp/furumi-ca.pem
# Client — automatically uses TLS, trusts server certificate
./target/release/furumi-mount-linux \
# Client
./target/release/furumi-mount-macos \
--server server-ip:50051 \
--token mysecrettoken \
--mount /mnt/remote
--mount /Volumes/remote
# Use it
ls /mnt/remote
mpv /mnt/remote/video.mkv
```
### Linux FUSE3
Linux client uses FUSE. Install with:
```
sudo add-apt-repository universe
sudo apt install libfuse3-dev
```
## Encryption
TLS is enabled by default. The server auto-generates a self-signed certificate on each start — no manual cert management required. The client automatically trusts the server's certificate for encryption.
@@ -80,6 +87,7 @@ Available at `http://<metrics-bind>/metrics`:
## Requirements
- Linux with `libfuse3-dev` and `pkg-config` (for client)
- macOS (uses built-in NFS client)
- Rust 2024 edition
## License

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-client-core"
version = "0.1.0"
version = "0.2.1"
edition = "2024"
[dependencies]

View File

@@ -155,7 +155,8 @@ impl FurumiClient {
.timeout(Duration::from_secs(30))
.concurrency_limit(256)
.tcp_keepalive(Some(Duration::from_secs(60)))
.http2_keep_alive_interval(Duration::from_secs(60));
.http2_keep_alive_interval(Duration::from_secs(60))
.keep_alive_while_idle(true);
let channel = if is_https {
info!("TLS enabled (encryption only, certificate verification disabled)");

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-common"
version = "0.1.0"
version = "0.2.1"
edition = "2024"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-mount-linux"
version = "0.1.0"
version = "0.2.1"
edition = "2024"
[dependencies]
@@ -15,3 +15,16 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tokio = { version = "1.50.0", features = ["full"] }
tokio-stream = "0.1.18"
ctrlc = "3.5.2"
[package.metadata.deb]
maintainer = "Furumi"
copyright = "Furumi contributors"
extended-description = "Furumi-ng: mount remote filesystem via encrypted gRPC + FUSE"
depends = "fuse3"
section = "utils"
priority = "optional"
maintainer-scripts = "debian/"
assets = [
{ source = "target/release/furumi-mount-linux", dest = "usr/bin/furumi-mount-linux", mode = "755" },
{ source = "debian/furumi-mount.service", dest = "usr/lib/systemd/user/furumi-mount.service", mode = "644" },
]

View File

@@ -0,0 +1,15 @@
[Unit]
Description=Furumi remote filesystem mount
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=%h/.config/furumi/config
ExecStart=/usr/bin/furumi-mount-linux
ExecStopPost=fusermount3 -uz ${FURUMI_MOUNT}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,36 @@
#!/bin/bash
set -e
if [ "$1" = "configure" ]; then
if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then
REAL_USER="$SUDO_USER"
REAL_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
CONFIG_DIR="$REAL_HOME/.config/furumi"
CONFIG_FILE="$CONFIG_DIR/config"
if [ ! -f "$CONFIG_FILE" ]; then
mkdir -p "$CONFIG_DIR"
cat > "$CONFIG_FILE" << 'EOF'
# Furumi mount configuration
# Edit the values below, then enable and start the service:
#
# systemctl --user enable --now furumi-mount.service
#
# To apply changes after editing this file:
#
# systemctl --user restart furumi-mount.service
FURUMI_SERVER=your-server:50051
FURUMI_TOKEN=your-token-here
FURUMI_MOUNT=/path/to/mountpoint
EOF
chown -R "$REAL_USER:$REAL_USER" "$CONFIG_DIR"
echo ""
echo "furumi-mount: config created at $CONFIG_FILE"
echo "furumi-mount: edit the file, then run:"
echo " systemctl --user enable --now furumi-mount.service"
echo ""
fi
fi
fi

View File

@@ -57,7 +57,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
let client = rt.block_on(async {
FurumiClient::connect(&full_addr, &args.token).await
let c = FurumiClient::connect(&full_addr, &args.token).await?;
// Ping the server to verify connection and authentication token
if let Err(e) = c.get_attr("/").await {
return Err(format!("Failed to authenticate or connect to server: {}", e).into());
}
Ok::<_, Box<dyn std::error::Error>>(c)
})?;
let fuse_fs = fs::FurumiFuse::new(client, rt.handle().clone());

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-mount-macos"
version = "0.1.0"
version = "0.2.1"
edition = "2024"
[dependencies]

View File

@@ -116,7 +116,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
// Unmount
let _ = Command::new("umount").arg(mount_point_umount.to_string_lossy().as_ref()).status();
let _ = Command::new("diskutil")
.arg("unmount")
.arg("force")
.arg(mount_point_umount.to_string_lossy().as_ref())
.status();
handle.abort();
println!("Unmounted successfully.");
});

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-server"
version = "0.1.0"
version = "0.2.1"
edition = "2024"
[dependencies]

View File

@@ -100,6 +100,24 @@ impl RequestTimer {
}
}
/// An RAII guard that increments the ACTIVE_STREAMS gauge when created
/// and decrements it when dropped. This ensures streams are correctly counted
/// even if they terminate abruptly.
pub struct ActiveStreamGuard;
impl ActiveStreamGuard {
pub fn new() -> Self {
ACTIVE_STREAMS.inc();
Self
}
}
impl Drop for ActiveStreamGuard {
fn drop(&mut self) {
ACTIVE_STREAMS.dec();
}
}
/// Render all registered metrics in Prometheus text format.
pub fn render_metrics() -> String {
let encoder = TextEncoder::new();

View File

@@ -60,15 +60,14 @@ impl<V: VirtualFileSystem> RemoteFileSystem for RemoteFileSystemImpl<V> {
match self.vfs.read_dir(&safe_path).await {
Ok(mut rx) => {
timer.finish_ok();
metrics::ACTIVE_STREAMS.inc();
let stream = async_stream::try_stream! {
let _guard = metrics::ActiveStreamGuard::new();
while let Some(result) = rx.recv().await {
match result {
Ok(entry) => yield entry,
Err(e) => Err(Status::internal(e.to_string()))?,
}
}
metrics::ACTIVE_STREAMS.dec();
};
Ok(Response::new(Box::pin(stream) as Self::ReadDirStream))
}
@@ -103,8 +102,8 @@ impl<V: VirtualFileSystem> RemoteFileSystem for RemoteFileSystemImpl<V> {
match self.vfs.read_file(sanitized_req).await {
Ok(mut rx) => {
timer.finish_ok();
metrics::ACTIVE_STREAMS.inc();
let stream = async_stream::try_stream! {
let _guard = metrics::ActiveStreamGuard::new();
while let Some(result) = rx.recv().await {
match result {
Ok(chunk) => {
@@ -117,7 +116,6 @@ impl<V: VirtualFileSystem> RemoteFileSystem for RemoteFileSystemImpl<V> {
}
}
}
metrics::ACTIVE_STREAMS.dec();
};
Ok(Response::new(Box::pin(stream) as Self::ReadFileStream))
}

View File

@@ -0,0 +1,56 @@
# Implementation Plan for `furumi-mount-windows` Client
## Architectural Decision
- **VFS Driver:** `WinFSP` (Windows File System Proxy).
- **Justification:** Excellent performance, perfect compatibility with the FUSE model, widely used in similar projects (e.g., rclone, sshfs-win).
- **Installation:** A unified installer (bundle) will be created (for example, using Inno Setup or WiX Toolkit), which will:
- Check if WinFSP is already installed.
- Automatically install the official `winfsp.msi` silently (using `/qn` flags) if the driver is missing.
- Install the `furumi-mount-windows.exe` client itself.
---
## Implementation Details
### 1. Application Scaffold
- Create a new binary crate `furumi-mount-windows` within the workspace.
- Add dependencies: `winfsp` (or `wfd`), `tokio`, `clap`, `tracing`, and an internal dependency on `furumi-client-core`.
### 2. Entry Point (CLI)
- In `main.rs`, configure parsing for command-line arguments and environment variables (`--server`, `--token`, `--mount`), similar to `furumi-mount-macos`.
- Initialize the gRPC connection to the server via `furumi-client-core`.
- Configure directory mounting:
- As a network drive (e.g., `Z:`).
- Or as a transparent folder within an existing NTFS filesystem (depending on driver support/flags).
### 3. VFS Implementation
- Create an `fs.rs` module.
- Implement the trait or callback structure required by WinFSP (e.g., the `WinFspFileSystem` structure).
- Action mapping:
- `GetFileInfo` / `GetSecurityByName` → gRPC `GetAttr` call.
- `ReadDirectory` → Streaming gRPC `ReadDir` call.
- `ReadFile``ReadFile` gRPC call (with support for stream chunking).
- **Crucial Part:** Translating Unix file attributes (from gRPC) into Windows File Attributes to ensure the system permits high-performance continuous stream reading (especially for media).
### 4. Installer Creation
- Write a configuration script for a Windows installer builder (e.g., `windows/setup.iss` for Inno Setup).
- Neatly bundle both `winfsp-x.y.z.msi` and `furumi-mount-windows.exe` together.
- Add Custom Actions / Logic to:
- Check the Windows Registry for an existing WinFSP installation.
- Trigger the `winfsp.msi` installation conditionally.
### 5. CI/CD Integration
- Update the GitHub Actions workflow (`docker-publish.yml` or create a dedicated release workflow).
- Add the target toolchain: `x86_64-pc-windows-msvc`.
- Add a step to compile: `cargo build --release --bin furumi-mount-windows`.
- Add a step to build the installer (e.g., `iscc setup.iss` or via `cargo-wix`).
- Output the final `setup.exe` as a GitHub Release artifact alongside other binaries.
### 6. Testing Strategy
- Write unit tests in Rust covering attribute translation and path mapping (mapping slashes `/` to backslashes `\`).
- Manual System Testing:
- Start `furumi-server` locally.
- Run the installer on a clean Windows machine (VM without pre-installed WinFSP).
- Verify that the drive mounts correctly and seamlessly.
- Launch media playback (e.g., via VLC/mpv) to ensure streaming stability over the VFS connection.