11 Commits

Author SHA1 Message Date
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
13 changed files with 136 additions and 18 deletions

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.0"
dependencies = [
"anyhow",
"async-trait",
@@ -607,7 +607,7 @@ dependencies = [
[[package]]
name = "furumi-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"prost",
"protobuf-src",
@@ -617,7 +617,7 @@ dependencies = [
[[package]]
name = "furumi-mount-linux"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"anyhow",
"clap",
@@ -634,7 +634,7 @@ dependencies = [
[[package]]
name = "furumi-mount-macos"
version = "0.1.0"
version = "0.2.0"
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,12 +27,18 @@ cargo build --release --workspace
--token mysecrettoken \
--tls-cert-out /tmp/furumi-ca.pem
# Client — automatically uses TLS, trusts server certificate
# Client (Linux) — automatically uses TLS, trusts server certificate
./target/release/furumi-mount-linux \
--server server-ip:50051 \
--token mysecrettoken \
--mount /mnt/remote
# Client (macOS)
./target/release/furumi-mount-macos \
--server server-ip:50051 \
--token mysecrettoken \
--mount /Volumes/remote
# Use it
ls /mnt/remote
mpv /mnt/remote/video.mkv
@@ -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]

View File

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

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))
}