lib.rs
⎇
Raw
1//! The HTTP wire contract of filebrowser-ng in one place.
2//!
3//! Both the server (axum) and the web frontend (wasm `fetch`) import these
4//! endpoint paths, query params and serde types, so the two sides cannot
5//! drift apart. Serde only — no axum, no wasm dependencies.
6
7use serde::{Deserialize, Serialize};
8
9// ---------------------------------------------------------------------------
10// Endpoint paths (single source of truth for the route table and the client)
11// ---------------------------------------------------------------------------
12
13pub const AUTH_LOGIN: &str = "/api/auth/login";
14pub const AUTH_LOGOUT: &str = "/api/auth/logout";
15pub const AUTH_ME: &str = "/api/auth/me";
16pub const AUTH_SETUP: &str = "/api/auth/setup";
17/// File operations: `{FILES}/{root_id}` and `{FILES}/{root_id}/{path...}`.
18pub const FILES: &str = "/api/files";
19/// Share management (authenticated): `{SHARES}` and `{SHARES}/{id}`.
20pub const SHARES: &str = "/api/shares";
21/// Public share resolve (no login): `{SHARE}/{token}`.
22pub const SHARE: &str = "/api/share";
23/// Admin user management: `{ADMIN_USERS}` and `{ADMIN_USERS}/{id}`.
24pub const ADMIN_USERS: &str = "/api/admin/users";
25pub const ADMIN_SETTINGS: &str = "/api/admin/settings";
26
27// ---------------------------------------------------------------------------
28// Query params
29// ---------------------------------------------------------------------------
30
31/// `?action=...` on file URLs; without it the route lists the directory.
32pub const P_ACTION: &str = "action";
33pub const ACTION_DOWNLOAD: &str = "download";
34pub const ACTION_PREVIEW: &str = "preview";
35pub const ACTION_CONTENT: &str = "content";
36/// `?format=...` for folder downloads (values: see `server::archive::ArchiveFormat`).
37pub const P_FORMAT: &str = "format";
38/// `?share=<token>` — authenticate file calls with a public share token.
39pub const P_SHARE: &str = "share";
40/// `?overwrite=true|1` on mutations and uploads.
41pub const P_OVERWRITE: &str = "overwrite";
42
43// ---------------------------------------------------------------------------
44// Mutation ops (`Mutation::op`)
45// ---------------------------------------------------------------------------
46
47pub const OP_RENAME: &str = "rename";
48pub const OP_MOVE: &str = "move";
49pub const OP_COPY: &str = "copy";
50
51// ---------------------------------------------------------------------------
52// Request bodies (client → server)
53// ---------------------------------------------------------------------------
54
55#[derive(Serialize, Deserialize)]
56pub struct Credentials {
57 pub name: String,
58 pub password: String,
59}
60
61/// Rename / move / copy (one body for all file mutations).
62#[derive(Serialize, Deserialize)]
63pub struct Mutation {
64 /// One of [`OP_RENAME`], [`OP_MOVE`], [`OP_COPY`].
65 pub op: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub new_name: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub dst_root_id: Option<i64>,
70 /// Destination directory, relative to `dst_root_id`.
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub dst: Option<String>,
73 #[serde(default)]
74 pub overwrite: bool,
75}
76
77/// A user folder: path relative to the server root + access mode.
78#[derive(Serialize, Deserialize)]
79pub struct Root {
80 /// Path relative to the server root; "." means the whole root.
81 pub path: String,
82 /// "rw" or "ro".
83 #[serde(default = "default_rw")]
84 pub mode: String,
85}
86
87fn default_rw() -> String {
88 "rw".to_string()
89}
90
91#[derive(Serialize, Deserialize)]
92pub struct CreateUser {
93 pub name: String,
94 pub password: String,
95 #[serde(default)]
96 pub is_admin: bool,
97 #[serde(default)]
98 pub roots: Vec<Root>,
99}
100
101#[derive(Serialize, Deserialize)]
102pub struct UpdateUser {
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub password: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub is_admin: Option<bool>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub active: Option<bool>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub roots: Option<Vec<Root>>,
111}
112
113/// Server settings (GET/PUT `{ADMIN_SETTINGS}`).
114#[derive(Serialize, Deserialize, Clone, Copy)]
115pub struct Settings {
116 pub allow_writable_shares: bool,
117}
118
119#[derive(Serialize, Deserialize)]
120pub struct CreateShare {
121 pub root_id: i64,
122 /// Item path relative to the root ("" or "." for the root itself).
123 pub path: String,
124 #[serde(default)]
125 pub writable: bool,
126 /// Absolute expiry as RFC 3339; absent = never.
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub expires_at: Option<String>,
129}
130
131// ---------------------------------------------------------------------------
132// Responses (server → client)
133// ---------------------------------------------------------------------------
134
135#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
136pub struct Entry {
137 pub name: String,
138 pub is_dir: bool,
139 pub size: u64,
140 /// RFC 3339 UTC modification time.
141 pub mtime: String,
142}
143
144#[derive(Serialize, Deserialize)]
145pub struct FilesResp {
146 pub entries: Vec<Entry>,
147}
148
149#[derive(Serialize, Deserialize, Clone)]
150pub struct UserInfo {
151 pub id: i64,
152 pub name: String,
153 pub is_admin: bool,
154}
155
156#[derive(Serialize, Deserialize, Clone)]
157pub struct RootInfo {
158 pub id: i64,
159 pub name: String,
160 pub path: String,
161 pub mode: String,
162}
163
164/// GET `{AUTH_ME}`.
165#[derive(Serialize, Deserialize, Clone)]
166pub struct Me {
167 /// True while no users exist yet (first-boot setup).
168 pub first_boot: bool,
169 /// None on first boot.
170 pub user: Option<UserInfo>,
171 pub roots: Vec<RootInfo>,
172 pub allow_writable_shares: bool,
173}
174
175/// GET/POST `{SHARES}`, GET `{SHARE}/{token}`.
176#[derive(Serialize, Deserialize, Clone)]
177pub struct ShareInfo {
178 pub id: i64,
179 pub token: String,
180 /// Display name (file/folder name, or the root's name for ".").
181 pub name: String,
182 pub is_file: bool,
183 pub writable: bool,
184 /// Path relative to the server root.
185 pub target: String,
186 /// RFC 3339 UTC creation time.
187 pub created_at: String,
188 /// RFC 3339 UTC expiry; None = never.
189 pub expires_at: Option<String>,
190 /// Synthetic root id to use in file API calls.
191 pub root_id: i64,
192}
193
194/// GET/POST `{ADMIN_USERS}`, PUT `{ADMIN_USERS}/{id}`.
195#[derive(Serialize, Deserialize, Clone)]
196pub struct AdminUser {
197 pub id: i64,
198 pub name: String,
199 pub is_admin: bool,
200 pub active: bool,
201 pub roots: Vec<RootInfo>,
202}
203
204/// Acknowledges a successful mutation: `{"ok": true}`.
205#[derive(Serialize, Deserialize)]
206pub struct OkResp {
207 pub ok: bool,
208}
209
210/// Upload success: `{"ok": true, "uploaded": n}`.
211#[derive(Serialize, Deserialize)]
212pub struct UploadResp {
213 pub ok: bool,
214 pub uploaded: usize,
215}
216
217/// PUT `?action=content` (editor save): the file's new mtime (unix seconds).
218#[derive(Serialize, Deserialize)]
219pub struct SaveResp {
220 pub ok: bool,
221 pub mtime: i64,
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn mutation_round_trip_skips_absent_fields() {
230 let m = Mutation {
231 op: OP_MOVE.to_string(),
232 new_name: None,
233 dst_root_id: Some(3),
234 dst: Some("docs".into()),
235 overwrite: true,
236 };
237 let s = serde_json::to_string(&m).unwrap();
238 assert!(!s.contains("new_name"));
239 let back: Mutation = serde_json::from_str(&s).unwrap();
240 assert_eq!(back.dst_root_id, Some(3));
241 assert_eq!(back.op, OP_MOVE);
242 }
243
244 #[test]
245 fn mutation_defaults_missing_fields() {
246 let m: Mutation = serde_json::from_str(r#"{"op":"rename","new_name":"a.txt"}"#).unwrap();
247 assert!(!m.overwrite);
248 assert_eq!(m.dst, None);
249 }
250
251 #[test]
252 fn root_defaults_mode_to_rw() {
253 let r: Root = serde_json::from_str(r#"{"path":"docs"}"#).unwrap();
254 assert_eq!(r.mode, "rw");
255 }
256
257 #[test]
258 fn me_round_trip() {
259 let me = Me {
260 first_boot: false,
261 user: Some(UserInfo {
262 id: 1,
263 name: "admin".into(),
264 is_admin: true,
265 }),
266 roots: vec![RootInfo {
267 id: 1,
268 name: "root".into(),
269 path: ".".into(),
270 mode: "rw".into(),
271 }],
272 allow_writable_shares: false,
273 };
274 let s = serde_json::to_string(&me).unwrap();
275 let back: Me = serde_json::from_str(&s).unwrap();
276 assert_eq!(back.roots.len(), 1);
277 }
278}
279