Skip to content

Commit f1e075f

Browse files
author
FreeSynergy
committed
feat: Phase D — embedded S3 storage server (fsn-s3)
New crate `fsn-s3`: - D1: S3-compatible server via s3s 0.13 + s3s-fs (local filesystem backend) - D2: opendal sync backends (local, SFTP via backend-sftp feature, Hetzner via backend-hetzner) - D3: Bucket structure — profiles, backups, media, packages, shared - D4: NodeProfile + ProfileStore (JSON + avatar in profiles/ bucket) - D5: FederatedS3Client — inter-node S3 replication CLI: `fsn storage` command with status, init, profile (show/set/avatar), sync (pull/push/fetch-profile) `fsn serve` now starts the S3 server on port 9000 alongside the HTTP API Also fixes: lib path refs (Lib.Ext, Lib.UI) + chrono dep in fsn-node-cli
1 parent cf3b4c9 commit f1e075f

File tree

15 files changed

+2401
-90
lines changed

15 files changed

+2401
-90
lines changed

cli/Cargo.lock

Lines changed: 1062 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"crates/fsn-wizard",
88
"crates/fsn-node-cli",
99
"crates/fsn-installer",
10+
"crates/fsn-s3",
1011
]
1112
resolver = "2"
1213

@@ -19,27 +20,33 @@ repository = "https://github.com/FreeSynergy/Node"
1920

2021
# ── Shared dependency versions ────────────────────────────────────────────────
2122
[workspace.dependencies]
22-
# FreeSynergy.Lib — shared libraries
23-
fsn-types = { path = "../../FreeSynergy.Lib/fsn-types" }
24-
fsn-error = { path = "../../FreeSynergy.Lib/fsn-error" }
25-
fsn-config = { path = "../../FreeSynergy.Lib/fsn-config" }
26-
fsn-i18n = { path = "../../FreeSynergy.Lib/fsn-i18n" }
27-
fsn-theme = { path = "../../FreeSynergy.Lib/fsn-theme" }
28-
fsn-help = { path = "../../FreeSynergy.Lib/fsn-help" }
29-
fsn-health = { path = "../../FreeSynergy.Lib/fsn-health" }
30-
fsn-plugin-sdk = { path = "../../FreeSynergy.Lib/fsn-plugin-sdk" }
31-
fsn-plugin-runtime = { path = "../../FreeSynergy.Lib/fsn-plugin-runtime", features = ["wasm"] }
32-
fsn-container = { path = "../../FreeSynergy.Lib/fsn-container" }
33-
fsn-template = { path = "../../FreeSynergy.Lib/fsn-template" }
34-
fsn-db = { path = "../../FreeSynergy.Lib/fsn-db", features = ["sqlite"] }
35-
fsn-store = { path = "../../FreeSynergy.Lib/fsn-store" }
23+
# FreeSynergy.Lib (core)
24+
fsn-types = { path = "../../FreeSynergy.Lib/fsn-types" }
25+
fsn-error = { path = "../../FreeSynergy.Lib/fsn-error" }
26+
fsn-config = { path = "../../FreeSynergy.Lib/fsn-config" }
27+
fsn-health = { path = "../../FreeSynergy.Lib/fsn-health" }
28+
fsn-template = { path = "../../FreeSynergy.Lib/fsn-template" }
29+
fsn-db = { path = "../../FreeSynergy.Lib/fsn-db", features = ["sqlite"] }
30+
fsn-crypto = { path = "../../FreeSynergy.Lib/fsn-crypto" }
31+
32+
# FreeSynergy.Lib.UI
33+
fsn-i18n = { path = "../../FreeSynergy.Lib.UI/fsn-i18n" }
34+
fsn-theme = { path = "../../FreeSynergy.Lib.UI/fsn-theme" }
35+
fsn-help = { path = "../../FreeSynergy.Lib.UI/fsn-help" }
36+
37+
# FreeSynergy.Lib.Ext
38+
fsn-plugin-sdk = { path = "../../FreeSynergy.Lib.Ext/fsn-plugin-sdk" }
39+
fsn-plugin-runtime = { path = "../../FreeSynergy.Lib.Ext/fsn-plugin-runtime" }
40+
fsn-container = { path = "../../FreeSynergy.Lib.Ext/fsn-container" }
41+
fsn-store = { path = "../../FreeSynergy.Lib.Ext/fsn-store" }
3642

3743
# Internal crates
3844
fsn-node-core = { path = "crates/fsn-node-core" }
3945
fsn-deploy = { path = "crates/fsn-deploy" }
4046
fsn-dns = { path = "crates/fsn-dns" }
4147
fsn-host = { path = "crates/fsn-host" }
4248
fsn-wizard = { path = "crates/fsn-wizard" }
49+
fsn-s3 = { path = "crates/fsn-s3" }
4350

4451
# Serialization
4552
serde = { version = "1", features = ["derive"] }
@@ -92,6 +99,11 @@ russh-keys = { version = "0.45" }
9299
axum = { version = "0.8", features = ["json", "http1", "tokio"] }
93100
tower-http = { version = "0.6", features = ["cors"] }
94101

102+
# S3 server (fsn-s3)
103+
hyper = { version = "1", features = ["server", "http1", "http2"] }
104+
hyper-util = { version = "0.1", features = ["tokio", "server"] }
105+
opendal = { version = "0.51", features = ["services-fs"] }
106+
95107
# WebSocket (Desktop Live-Updates, fsn serve)
96108
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
97109

cli/crates/fsn-node-cli/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ fsn-host = { workspace = true }
1818
fsn-container = { workspace = true }
1919
fsn-store = { workspace = true }
2020
fsn-i18n = { workspace = true }
21+
fsn-s3 = { workspace = true }
2122
clap = { workspace = true }
2223
tokio = { workspace = true }
2324
anyhow = { workspace = true }
@@ -27,10 +28,11 @@ tracing = { workspace = true }
2728
tracing-subscriber = { workspace = true }
2829
libc = { workspace = true }
2930
rand = { workspace = true }
31+
chrono = { workspace = true }
3032
axum = { workspace = true }
3133
tower-http = { workspace = true }
3234
serde_json = { workspace = true }
33-
fsn-pkg = { path = "../../../../FreeSynergy.Lib/fsn-pkg" }
35+
fsn-pkg = { path = "../../../../FreeSynergy.Lib.Ext/fsn-pkg" }
3436

3537
[dev-dependencies]
3638
tempfile = { workspace = true }

cli/crates/fsn-node-cli/src/cli.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,120 @@ pub enum Command {
161161
/// Service instance name (as declared in the project config)
162162
service: String,
163163
},
164+
165+
/// Manage the embedded S3 storage server
166+
Storage {
167+
#[command(subcommand)]
168+
cmd: StorageCommand,
169+
},
170+
}
171+
172+
#[derive(Subcommand)]
173+
pub enum StorageCommand {
174+
/// Show bucket status (sizes, object counts)
175+
Status,
176+
177+
/// Initialize bucket directory structure
178+
Init,
179+
180+
/// Manage the local node's public profile
181+
Profile {
182+
#[command(subcommand)]
183+
cmd: ProfileCommand,
184+
},
185+
186+
/// Sync data with a remote node (federation)
187+
Sync {
188+
#[command(subcommand)]
189+
cmd: StorageSyncCommand,
190+
},
191+
}
192+
193+
#[derive(Subcommand)]
194+
pub enum ProfileCommand {
195+
/// Show the current local profile
196+
Show,
197+
198+
/// Set profile display name and metadata
199+
Set {
200+
/// Display name
201+
#[arg(long)]
202+
name: String,
203+
204+
/// Short description of this node
205+
#[arg(long)]
206+
description: Option<String>,
207+
208+
/// Public URL where this node is reachable
209+
#[arg(long)]
210+
public_url: Option<String>,
211+
},
212+
213+
/// Upload an avatar image (png / jpg / webp)
214+
Avatar {
215+
/// Path to the image file
216+
file: std::path::PathBuf,
217+
},
218+
}
219+
220+
#[derive(Subcommand)]
221+
pub enum StorageSyncCommand {
222+
/// Pull a bucket from a remote node
223+
Pull {
224+
/// S3 endpoint of the remote node (e.g. http://peer.example:9000)
225+
#[arg(long)]
226+
remote_url: String,
227+
228+
/// Bucket name (profiles, backups, media, packages, shared)
229+
#[arg(long)]
230+
bucket: String,
231+
232+
/// S3 access key for the remote node
233+
#[arg(long)]
234+
access_key: String,
235+
236+
/// S3 secret key for the remote node
237+
#[arg(long)]
238+
secret_key: String,
239+
},
240+
241+
/// Push a local bucket to a remote node
242+
Push {
243+
/// S3 endpoint of the remote node
244+
#[arg(long)]
245+
remote_url: String,
246+
247+
/// Bucket name
248+
#[arg(long)]
249+
bucket: String,
250+
251+
/// S3 access key for the remote node
252+
#[arg(long)]
253+
access_key: String,
254+
255+
/// S3 secret key for the remote node
256+
#[arg(long)]
257+
secret_key: String,
258+
},
259+
260+
/// Fetch a remote node's public profile
261+
FetchProfile {
262+
/// S3 endpoint of the remote node
263+
#[arg(long)]
264+
remote_url: String,
265+
266+
/// Node ID to fetch
267+
#[arg(long)]
268+
node_id: String,
269+
270+
/// S3 access key (leave empty for public read)
271+
#[arg(long, default_value = "")]
272+
access_key: String,
273+
274+
/// S3 secret key (leave empty for public read)
275+
#[arg(long, default_value = "")]
276+
secret_key: String,
277+
},
164278
}
165279

166280
#[derive(Subcommand)]
@@ -397,5 +511,11 @@ pub async fn run() -> Result<()> {
397511
Command::Deps { service } => {
398512
commands::deps::run(&root, cli.project.as_deref(), &service).await
399513
},
514+
Command::Storage { cmd } => match cmd {
515+
StorageCommand::Status => commands::storage::status(&root).await,
516+
StorageCommand::Init => commands::storage::init(&root).await,
517+
StorageCommand::Profile { cmd } => commands::storage::profile(&root, cmd).await,
518+
StorageCommand::Sync { cmd } => commands::storage::sync(&root, cmd).await,
519+
},
400520
}
401521
}

cli/crates/fsn-node-cli/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod restart;
1212
pub mod serve;
1313
pub mod server_setup;
1414
pub mod status;
15+
pub mod storage;
1516
pub mod store;
1617
pub mod sync;
1718
pub mod tui;

cli/crates/fsn-node-cli/src/commands/serve.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
// `fsn serve` — embedded HTTP server exposing the store knowledge API.
1+
// `fsn serve` — embedded HTTP server + S3 storage server.
22
//
3-
// Routes (all under /api/store/know/):
3+
// HTTP routes (all under /api/store/know/):
44
// GET /api/store/know/catalog → full catalog as JSON
55
// GET /api/store/know/search?q=... → filtered catalog
66
// GET /api/store/know/package/:id → single package details
77
// GET /api/store/know/installed → installed packages from DB
88
// GET /api/store/know/i18n → available language packs
99
//
10-
// The Desktop (fsd) connects to this API to render the Store UI.
10+
// S3 server (default port 9000):
11+
// Standard AWS S3 API, backed by the local filesystem.
12+
// Buckets: profiles (public), backups, media, packages, shared.
13+
//
14+
// The Desktop (fsd) connects to the HTTP API to render the Store UI.
15+
// Remote nodes connect to the S3 port for federation.
1116

1217
use std::path::Path;
1318

@@ -21,6 +26,7 @@ use axum::{
2126
};
2227
use fsn_db::InstalledPackageRepo;
2328
use fsn_node_core::store::StoreEntry;
29+
use fsn_s3::{S3Server, StorageConfig};
2430
use fsn_store::StoreClient;
2531
use serde::{Deserialize, Serialize};
2632
use tower_http::cors::{Any, CorsLayer};
@@ -53,9 +59,23 @@ struct InstalledRow {
5359

5460
// ── run ───────────────────────────────────────────────────────────────────────
5561

56-
pub async fn run(_root: &Path, _project: Option<&Path>, bind: &str, port: u16) -> Result<()> {
62+
pub async fn run(root: &Path, _project: Option<&Path>, bind: &str, port: u16) -> Result<()> {
5763
let addr = format!("{bind}:{port}");
5864

65+
// ── S3 server ─────────────────────────────────────────────────────────────
66+
let s3_config = StorageConfig {
67+
enabled: true,
68+
port: 9000,
69+
bind: "127.0.0.1".to_owned(),
70+
data_root: root.join("storage"),
71+
access_key: "fsn_local".to_owned(),
72+
secret_key: "changeme_secret_key".to_owned(),
73+
sync: None,
74+
};
75+
let s3 = S3Server::new(s3_config);
76+
let _s3_handle = s3.start().await?;
77+
78+
// ── HTTP store API ────────────────────────────────────────────────────────
5979
let cors = CorsLayer::new()
6080
.allow_origin(Any)
6181
.allow_methods(Any)
@@ -72,7 +92,8 @@ pub async fn run(_root: &Path, _project: Option<&Path>, bind: &str, port: u16) -
7292

7393
let listener = tokio::net::TcpListener::bind(&addr).await?;
7494
info!("fsn store API listening on http://{addr}");
75-
println!("Store API running at http://{addr}/api/store/know/");
95+
println!("Store API : http://{addr}/api/store/know/");
96+
println!("S3 API : http://127.0.0.1:9000");
7697
println!("Press Ctrl+C to stop.");
7798

7899
axum::serve(listener, app).await?;

0 commit comments

Comments
 (0)