Skip to main content

lfsx_server/
model.rs

1use serde::{Deserialize, Serialize};
2
3use crate::locks::Lock;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum Operation {
8    Upload,
9    Download,
10}
11
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct ObjectId {
14    pub oid: String,
15    pub size: u64,
16}
17
18#[derive(Debug, Deserialize)]
19pub struct BatchRequest {
20    pub operation: Operation,
21    #[serde(default)]
22    pub transfers: Vec<String>,
23    pub objects: Vec<ObjectId>,
24    #[serde(rename = "ref")]
25    pub reference: Option<Reference>,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct RetainRequest {
30    pub oids: Vec<String>,
31    #[serde(default)]
32    pub dry_run: bool,
33}
34
35#[derive(Debug, Deserialize)]
36pub struct CompressRequest {
37    #[serde(default)]
38    pub dry_run: bool,
39}
40
41#[derive(Debug, Deserialize)]
42pub struct DedupeRequest {
43    #[serde(default)]
44    pub dry_run: bool,
45}
46
47#[derive(Debug, Serialize)]
48pub struct BatchResponse {
49    pub transfer: &'static str,
50    pub objects: Vec<ObjectSpec>,
51}
52
53#[derive(Debug, Serialize)]
54pub struct ObjectSpec {
55    #[serde(flatten)]
56    pub id: ObjectId,
57    // Set only when the href carries its own credentials, which is true of a
58    // pre-signed bucket URL and of nothing else this server hands out. Claiming
59    // it for a URL pointing back here makes the client transfer with no
60    // credentials at all, and loop on the 401 that follows.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub authenticated: Option<bool>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub actions: Option<Actions>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub error: Option<ObjectError>,
67}
68
69#[derive(Debug, Default, Serialize)]
70pub struct Actions {
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub upload: Option<Action>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub download: Option<Action>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub verify: Option<Action>,
77}
78
79#[derive(Debug, Serialize)]
80pub struct Action {
81    pub href: String,
82    // Headers the client must send with the request. Not advice: for a
83    // pre-signed upload the digest is bound into the signature, so an altered or
84    // missing header is a refused request rather than a weaker one.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub header: Option<std::collections::BTreeMap<String, String>>,
87    pub expires_in: u32,
88}
89
90#[derive(Debug, Serialize)]
91pub struct ObjectError {
92    pub code: u16,
93    pub message: String,
94}
95
96impl ObjectSpec {
97    pub fn missing(id: ObjectId) -> Self {
98        Self {
99            id,
100            authenticated: None,
101            actions: None,
102            error: Some(ObjectError {
103                code: 404,
104                message: "object not found".into(),
105            }),
106        }
107    }
108
109    pub fn too_large(id: ObjectId, limit: u64) -> Self {
110        Self {
111            error: Some(ObjectError {
112                code: 413,
113                message: format!(
114                    "object is {} bytes, over the {limit} byte limit this server accepts",
115                    id.size
116                ),
117            }),
118            id,
119            authenticated: None,
120            actions: None,
121        }
122    }
123
124    pub fn over_quota(id: ObjectId, used: u64, limit: u64) -> Self {
125        Self {
126            id,
127            authenticated: None,
128            actions: None,
129            error: Some(ObjectError {
130                code: 507,
131                message: format!(
132                    "this repository holds {used} bytes of its {limit} byte budget — collect what \
133                     it no longer references, or ask for more room"
134                ),
135            }),
136        }
137    }
138}
139
140#[derive(Debug, Deserialize)]
141pub struct CreateLockRequest {
142    pub path: String,
143}
144
145#[derive(Debug, Deserialize)]
146pub struct ListLocksQuery {
147    pub path: Option<String>,
148    pub id: Option<String>,
149    pub cursor: Option<String>,
150    pub limit: Option<usize>,
151    // Accepted and not acted on: see `refs` in the README. Rejecting it would
152    // break a client that sends what the specification tells it to send.
153    #[serde(rename = "refspec")]
154    pub refspec: Option<String>,
155}
156
157#[derive(Debug, Default, Deserialize)]
158pub struct VerifyLocksRequest {
159    pub cursor: Option<String>,
160    pub limit: Option<usize>,
161    #[serde(rename = "ref")]
162    pub reference: Option<Reference>,
163}
164
165#[derive(Debug, Deserialize)]
166pub struct Reference {
167    pub name: String,
168}
169
170#[derive(Debug, Deserialize)]
171pub struct UnlockRequest {
172    #[serde(default)]
173    pub force: bool,
174}
175
176#[derive(Debug, Serialize)]
177pub struct LockResponse {
178    pub lock: Lock,
179}
180
181#[derive(Debug, Serialize)]
182pub struct ListLocksResponse {
183    pub locks: Vec<Lock>,
184    #[serde(skip_serializing_if = "String::is_empty")]
185    pub next_cursor: String,
186}
187
188#[derive(Debug, Serialize)]
189pub struct VerifyLocksResponse {
190    pub ours: Vec<Lock>,
191    pub theirs: Vec<Lock>,
192    #[serde(skip_serializing_if = "String::is_empty")]
193    pub next_cursor: String,
194}
195
196#[derive(Debug, Serialize)]
197pub struct StatsResponse {
198    pub objects: u64,
199    pub bytes: u64,
200    pub locks: usize,
201}