//! The HTTP wire contract of filebrowser-ng in one place. //! //! Both the server (axum) and the web frontend (wasm `fetch`) import these //! endpoint paths, query params and serde types, so the two sides cannot //! drift apart. Serde only — no axum, no wasm dependencies. use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- // Endpoint paths (single source of truth for the route table and the client) // --------------------------------------------------------------------------- pub const AUTH_LOGIN: &str = "/api/auth/login"; pub const AUTH_LOGOUT: &str = "/api/auth/logout"; pub const AUTH_ME: &str = "/api/auth/me"; pub const AUTH_SETUP: &str = "/api/auth/setup"; /// File operations: `{FILES}/{root_id}` and `{FILES}/{root_id}/{path...}`. pub const FILES: &str = "/api/files"; /// Share management (authenticated): `{SHARES}` and `{SHARES}/{id}`. pub const SHARES: &str = "/api/shares"; /// Public share resolve (no login): `{SHARE}/{token}`. pub const SHARE: &str = "/api/share"; /// Admin user management: `{ADMIN_USERS}` and `{ADMIN_USERS}/{id}`. pub const ADMIN_USERS: &str = "/api/admin/users"; pub const ADMIN_SETTINGS: &str = "/api/admin/settings"; // --------------------------------------------------------------------------- // Query params // --------------------------------------------------------------------------- /// `?action=...` on file URLs; without it the route lists the directory. pub const P_ACTION: &str = "action"; pub const ACTION_DOWNLOAD: &str = "download"; pub const ACTION_PREVIEW: &str = "preview"; pub const ACTION_CONTENT: &str = "content"; /// `?format=...` for folder downloads (values: see `server::archive::ArchiveFormat`). pub const P_FORMAT: &str = "format"; /// `?share=` — authenticate file calls with a public share token. pub const P_SHARE: &str = "share"; /// `?overwrite=true|1` on mutations and uploads. pub const P_OVERWRITE: &str = "overwrite"; // --------------------------------------------------------------------------- // Mutation ops (`Mutation::op`) // --------------------------------------------------------------------------- pub const OP_RENAME: &str = "rename"; pub const OP_MOVE: &str = "move"; pub const OP_COPY: &str = "copy"; // --------------------------------------------------------------------------- // Request bodies (client → server) // --------------------------------------------------------------------------- #[derive(Serialize, Deserialize)] pub struct Credentials { pub name: String, pub password: String, } /// Rename / move / copy (one body for all file mutations). #[derive(Serialize, Deserialize)] pub struct Mutation { /// One of [`OP_RENAME`], [`OP_MOVE`], [`OP_COPY`]. pub op: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub new_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dst_root_id: Option, /// Destination directory, relative to `dst_root_id`. #[serde(default, skip_serializing_if = "Option::is_none")] pub dst: Option, #[serde(default)] pub overwrite: bool, } /// A user folder: path relative to the server root + access mode. #[derive(Serialize, Deserialize)] pub struct Root { /// Path relative to the server root; "." means the whole root. pub path: String, /// "rw" or "ro". #[serde(default = "default_rw")] pub mode: String, } fn default_rw() -> String { "rw".to_string() } #[derive(Serialize, Deserialize)] pub struct CreateUser { pub name: String, pub password: String, #[serde(default)] pub is_admin: bool, #[serde(default)] pub roots: Vec, } #[derive(Serialize, Deserialize)] pub struct UpdateUser { #[serde(default, skip_serializing_if = "Option::is_none")] pub password: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub is_admin: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub active: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub roots: Option>, } /// Server settings (GET/PUT `{ADMIN_SETTINGS}`). #[derive(Serialize, Deserialize, Clone, Copy)] pub struct Settings { pub allow_writable_shares: bool, } #[derive(Serialize, Deserialize)] pub struct CreateShare { pub root_id: i64, /// Item path relative to the root ("" or "." for the root itself). pub path: String, #[serde(default)] pub writable: bool, /// Absolute expiry as RFC 3339; absent = never. #[serde(default, skip_serializing_if = "Option::is_none")] pub expires_at: Option, } // --------------------------------------------------------------------------- // Responses (server → client) // --------------------------------------------------------------------------- #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Entry { pub name: String, pub is_dir: bool, pub size: u64, /// RFC 3339 UTC modification time. pub mtime: String, } #[derive(Serialize, Deserialize)] pub struct FilesResp { pub entries: Vec, } #[derive(Serialize, Deserialize, Clone)] pub struct UserInfo { pub id: i64, pub name: String, pub is_admin: bool, } #[derive(Serialize, Deserialize, Clone)] pub struct RootInfo { pub id: i64, pub name: String, pub path: String, pub mode: String, } /// GET `{AUTH_ME}`. #[derive(Serialize, Deserialize, Clone)] pub struct Me { /// True while no users exist yet (first-boot setup). pub first_boot: bool, /// None on first boot. pub user: Option, pub roots: Vec, pub allow_writable_shares: bool, } /// GET/POST `{SHARES}`, GET `{SHARE}/{token}`. #[derive(Serialize, Deserialize, Clone)] pub struct ShareInfo { pub id: i64, pub token: String, /// Display name (file/folder name, or the root's name for "."). pub name: String, pub is_file: bool, pub writable: bool, /// Path relative to the server root. pub target: String, /// RFC 3339 UTC creation time. pub created_at: String, /// RFC 3339 UTC expiry; None = never. pub expires_at: Option, /// Synthetic root id to use in file API calls. pub root_id: i64, } /// GET/POST `{ADMIN_USERS}`, PUT `{ADMIN_USERS}/{id}`. #[derive(Serialize, Deserialize, Clone)] pub struct AdminUser { pub id: i64, pub name: String, pub is_admin: bool, pub active: bool, pub roots: Vec, } /// Acknowledges a successful mutation: `{"ok": true}`. #[derive(Serialize, Deserialize)] pub struct OkResp { pub ok: bool, } /// Upload success: `{"ok": true, "uploaded": n}`. #[derive(Serialize, Deserialize)] pub struct UploadResp { pub ok: bool, pub uploaded: usize, } /// PUT `?action=content` (editor save): the file's new mtime (unix seconds). #[derive(Serialize, Deserialize)] pub struct SaveResp { pub ok: bool, pub mtime: i64, } #[cfg(test)] mod tests { use super::*; #[test] fn mutation_round_trip_skips_absent_fields() { let m = Mutation { op: OP_MOVE.to_string(), new_name: None, dst_root_id: Some(3), dst: Some("docs".into()), overwrite: true, }; let s = serde_json::to_string(&m).unwrap(); assert!(!s.contains("new_name")); let back: Mutation = serde_json::from_str(&s).unwrap(); assert_eq!(back.dst_root_id, Some(3)); assert_eq!(back.op, OP_MOVE); } #[test] fn mutation_defaults_missing_fields() { let m: Mutation = serde_json::from_str(r#"{"op":"rename","new_name":"a.txt"}"#).unwrap(); assert!(!m.overwrite); assert_eq!(m.dst, None); } #[test] fn root_defaults_mode_to_rw() { let r: Root = serde_json::from_str(r#"{"path":"docs"}"#).unwrap(); assert_eq!(r.mode, "rw"); } #[test] fn me_round_trip() { let me = Me { first_boot: false, user: Some(UserInfo { id: 1, name: "admin".into(), is_admin: true, }), roots: vec![RootInfo { id: 1, name: "root".into(), path: ".".into(), mode: "rw".into(), }], allow_writable_shares: false, }; let s = serde_json::to_string(&me).unwrap(); let back: Me = serde_json::from_str(&s).unwrap(); assert_eq!(back.roots.len(), 1); } }