Compare commits
12 Commits
v0.1.0
...
f2d42751fd
| Author | SHA1 | Date | |
|---|---|---|---|
| f2d42751fd | |||
| bc34b6bc41 | |||
| 3ee7235b51 | |||
| 1f85d9c435 | |||
| 773e9c1ee7 | |||
| 0242376a65 | |||
|
|
64f292c7b1 | ||
| 73b6d7483e | |||
|
|
64b59ba72d | ||
|
|
c54af23845 | ||
|
|
dd3f3721b2 | ||
|
|
dc77933c9e |
63
.github/workflows/deb-publish.yml
vendored
Normal file
63
.github/workflows/deb-publish.yml
vendored
Normal 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"
|
||||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -585,7 +585,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumi-client-core"
|
name = "furumi-client-core"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -607,7 +607,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumi-common"
|
name = "furumi-common"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
"protobuf-src",
|
"protobuf-src",
|
||||||
@@ -617,7 +617,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumi-mount-linux"
|
name = "furumi-mount-linux"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -634,7 +634,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumi-mount-macos"
|
name = "furumi-mount-macos"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -652,7 +652,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumi-server"
|
name = "furumi-server"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -27,13 +27,7 @@ cargo build --release --workspace
|
|||||||
--token mysecrettoken \
|
--token mysecrettoken \
|
||||||
--tls-cert-out /tmp/furumi-ca.pem
|
--tls-cert-out /tmp/furumi-ca.pem
|
||||||
|
|
||||||
# Client (Linux) — automatically uses TLS, trusts server certificate
|
# Client
|
||||||
./target/release/furumi-mount-linux \
|
|
||||||
--server server-ip:50051 \
|
|
||||||
--token mysecrettoken \
|
|
||||||
--mount /mnt/remote
|
|
||||||
|
|
||||||
# Client (macOS)
|
|
||||||
./target/release/furumi-mount-macos \
|
./target/release/furumi-mount-macos \
|
||||||
--server server-ip:50051 \
|
--server server-ip:50051 \
|
||||||
--token mysecrettoken \
|
--token mysecrettoken \
|
||||||
@@ -44,6 +38,12 @@ ls /mnt/remote
|
|||||||
mpv /mnt/remote/video.mkv
|
mpv /mnt/remote/video.mkv
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Linux FUSE3
|
||||||
|
Linux client uses FUSE. Install with:
|
||||||
|
```
|
||||||
|
sudo add-apt-repository universe
|
||||||
|
sudo apt install libfuse3-dev
|
||||||
|
```
|
||||||
## Encryption
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumi-client-core"
|
name = "furumi-client-core"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ impl FurumiClient {
|
|||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.concurrency_limit(256)
|
.concurrency_limit(256)
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60)))
|
.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 {
|
let channel = if is_https {
|
||||||
info!("TLS enabled (encryption only, certificate verification disabled)");
|
info!("TLS enabled (encryption only, certificate verification disabled)");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumi-common"
|
name = "furumi-common"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumi-mount-linux"
|
name = "furumi-mount-linux"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -15,3 +15,16 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
|||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
tokio-stream = "0.1.18"
|
tokio-stream = "0.1.18"
|
||||||
ctrlc = "3.5.2"
|
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" },
|
||||||
|
]
|
||||||
|
|||||||
15
furumi-mount-linux/debian/furumi-mount.service
Normal file
15
furumi-mount-linux/debian/furumi-mount.service
Normal 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
|
||||||
36
furumi-mount-linux/debian/postinst
Normal file
36
furumi-mount-linux/debian/postinst
Normal 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
|
||||||
@@ -57,7 +57,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let client = rt.block_on(async {
|
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());
|
let fuse_fs = fs::FurumiFuse::new(client, rt.handle().clone());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumi-mount-macos"
|
name = "furumi-mount-macos"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -116,7 +116,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unmount
|
// 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();
|
handle.abort();
|
||||||
println!("Unmounted successfully.");
|
println!("Unmounted successfully.");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumi-server"
|
name = "furumi-server"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -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.
|
/// Render all registered metrics in Prometheus text format.
|
||||||
pub fn render_metrics() -> String {
|
pub fn render_metrics() -> String {
|
||||||
let encoder = TextEncoder::new();
|
let encoder = TextEncoder::new();
|
||||||
|
|||||||
@@ -60,15 +60,14 @@ impl<V: VirtualFileSystem> RemoteFileSystem for RemoteFileSystemImpl<V> {
|
|||||||
match self.vfs.read_dir(&safe_path).await {
|
match self.vfs.read_dir(&safe_path).await {
|
||||||
Ok(mut rx) => {
|
Ok(mut rx) => {
|
||||||
timer.finish_ok();
|
timer.finish_ok();
|
||||||
metrics::ACTIVE_STREAMS.inc();
|
|
||||||
let stream = async_stream::try_stream! {
|
let stream = async_stream::try_stream! {
|
||||||
|
let _guard = metrics::ActiveStreamGuard::new();
|
||||||
while let Some(result) = rx.recv().await {
|
while let Some(result) = rx.recv().await {
|
||||||
match result {
|
match result {
|
||||||
Ok(entry) => yield entry,
|
Ok(entry) => yield entry,
|
||||||
Err(e) => Err(Status::internal(e.to_string()))?,
|
Err(e) => Err(Status::internal(e.to_string()))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metrics::ACTIVE_STREAMS.dec();
|
|
||||||
};
|
};
|
||||||
Ok(Response::new(Box::pin(stream) as Self::ReadDirStream))
|
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 {
|
match self.vfs.read_file(sanitized_req).await {
|
||||||
Ok(mut rx) => {
|
Ok(mut rx) => {
|
||||||
timer.finish_ok();
|
timer.finish_ok();
|
||||||
metrics::ACTIVE_STREAMS.inc();
|
|
||||||
let stream = async_stream::try_stream! {
|
let stream = async_stream::try_stream! {
|
||||||
|
let _guard = metrics::ActiveStreamGuard::new();
|
||||||
while let Some(result) = rx.recv().await {
|
while let Some(result) = rx.recv().await {
|
||||||
match result {
|
match result {
|
||||||
Ok(chunk) => {
|
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))
|
Ok(Response::new(Box::pin(stream) as Self::ReadFileStream))
|
||||||
}
|
}
|
||||||
|
|||||||
56
windows-implementation-plan.md
Normal file
56
windows-implementation-plan.md
Normal 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.
|
||||||
Reference in New Issue
Block a user