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/// What a listing entry actually is, decided by the server from the file's
136/// leading bytes (magic numbers via `infer`, plus a text/binary heuristic) —
137/// not from its name. Drives the icon and the preview the client offers.
138///
139/// Deliberately coarse: this answers "which viewer opens this", not "what
140/// exact format is it". Syntax highlighting still keys off the extension,
141/// because `.h` is C or C++ and no amount of sniffing decides that.
142#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
143#[serde(rename_all = "lowercase")]
144pub enum FileKind {
145 Dir,
146 Image,
147 Video,
148 Audio,
149 Pdf,
150 Archive,
151 /// Anything that decodes as text: source code, markup, config, plain text.
152 Text,
153 /// Recognized-but-not-viewable, or undecodable bytes.
154 Binary,
155}
156
157#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
158pub struct Entry {
159 pub name: String,
160 pub is_dir: bool,
161 pub size: u64,
162 /// RFC 3339 UTC modification time.
163 pub mtime: String,
164 /// Content-sniffed kind (see [`FileKind`]).
165 pub kind: FileKind,
166}
167
168#[derive(Serialize, Deserialize)]
169pub struct FilesResp {
170 pub entries: Vec<Entry>,
171}
172
173#[derive(Serialize, Deserialize, Clone)]
174pub struct UserInfo {
175 pub id: i64,
176 pub name: String,
177 pub is_admin: bool,
178}
179
180#[derive(Serialize, Deserialize, Clone)]
181pub struct RootInfo {
182 pub id: i64,
183 pub name: String,
184 pub path: String,
185 pub mode: String,
186}
187
188/// GET `{AUTH_ME}`.
189#[derive(Serialize, Deserialize, Clone)]
190pub struct Me {
191 /// True while no users exist yet (first-boot setup).
192 pub first_boot: bool,
193 /// None on first boot.
194 pub user: Option<UserInfo>,
195 pub roots: Vec<RootInfo>,
196 pub allow_writable_shares: bool,
197}
198
199/// GET/POST `{SHARES}`, GET `{SHARE}/{token}`.
200#[derive(Serialize, Deserialize, Clone)]
201pub struct ShareInfo {
202 pub id: i64,
203 pub token: String,
204 /// Display name (file/folder name, or the root's name for ".").
205 pub name: String,
206 pub is_file: bool,
207 pub writable: bool,
208 /// Path relative to the server root.
209 pub target: String,
210 /// RFC 3339 UTC creation time.
211 pub created_at: String,
212 /// RFC 3339 UTC expiry; None = never.
213 pub expires_at: Option<String>,
214 /// Synthetic root id to use in file API calls.
215 pub root_id: i64,
216}
217
218/// GET/POST `{ADMIN_USERS}`, PUT `{ADMIN_USERS}/{id}`.
219#[derive(Serialize, Deserialize, Clone)]
220pub struct AdminUser {
221 pub id: i64,
222 pub name: String,
223 pub is_admin: bool,
224 pub active: bool,
225 pub roots: Vec<RootInfo>,
226}
227
228/// Acknowledges a successful mutation: `{"ok": true}`.
229#[derive(Serialize, Deserialize)]
230pub struct OkResp {
231 pub ok: bool,
232}
233
234/// Upload success: `{"ok": true, "uploaded": n}`.
235#[derive(Serialize, Deserialize)]
236pub struct UploadResp {
237 pub ok: bool,
238 pub uploaded: usize,
239}
240
241/// PUT `?action=content` (editor save): the file's new mtime (unix seconds).
242#[derive(Serialize, Deserialize)]
243pub struct SaveResp {
244 pub ok: bool,
245 pub mtime: i64,
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn mutation_round_trip_skips_absent_fields() {
254 let m = Mutation {
255 op: OP_MOVE.to_string(),
256 new_name: None,
257 dst_root_id: Some(3),
258 dst: Some("docs".into()),
259 overwrite: true,
260 };
261 let s = serde_json::to_string(&m).unwrap();
262 assert!(!s.contains("new_name"));
263 let back: Mutation = serde_json::from_str(&s).unwrap();
264 assert_eq!(back.dst_root_id, Some(3));
265 assert_eq!(back.op, OP_MOVE);
266 }
267
268 #[test]
269 fn mutation_defaults_missing_fields() {
270 let m: Mutation = serde_json::from_str(r#"{"op":"rename","new_name":"a.txt"}"#).unwrap();
271 assert!(!m.overwrite);
272 assert_eq!(m.dst, None);
273 }
274
275 #[test]
276 fn root_defaults_mode_to_rw() {
277 let r: Root = serde_json::from_str(r#"{"path":"docs"}"#).unwrap();
278 assert_eq!(r.mode, "rw");
279 }
280
281 #[test]
282 fn me_round_trip() {
283 let me = Me {
284 first_boot: false,
285 user: Some(UserInfo {
286 id: 1,
287 name: "admin".into(),
288 is_admin: true,
289 }),
290 roots: vec![RootInfo {
291 id: 1,
292 name: "root".into(),
293 path: ".".into(),
294 mode: "rw".into(),
295 }],
296 allow_writable_shares: false,
297 };
298 let s = serde_json::to_string(&me).unwrap();
299 let back: Me = serde_json::from_str(&s).unwrap();
300 assert_eq!(back.roots.len(), 1);
301 }
302}
303