git_lfs_spec/spec/
batch.rs

1use chrono::{DateTime, FixedOffset};
2use serde_derive::{Deserialize, Serialize};
3use std::collections::HashMap;
4use url::Url;
5
6use crate::spec::Object;
7
8/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests
9#[derive(PartialEq, Eq, Debug, Deserialize)]
10pub struct BatchRequest {
11    pub operation: Operation,
12    #[serde(default = "Transfer::default_vec")]
13    pub transfer: Vec<Transfer>,
14    #[serde(rename = "ref")]
15    pub ref_property: Option<Ref>,
16    pub objects: Vec<Object>,
17}
18
19/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#successful-responses
20#[derive(PartialEq, Eq, Debug, Serialize)]
21pub struct BatchResponse {
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub transfer: Option<Transfer>,
24    pub objects: Vec<ObjectResponse>,
25}
26
27/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests
28#[derive(PartialEq, Eq, Debug, Deserialize, Serialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Operation {
31    Download,
32    Upload,
33}
34
35/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md#basic-transfer-api
36#[derive(PartialEq, Eq, Debug, Deserialize, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum Transfer {
39    Basic,
40    Custom,
41}
42
43impl Transfer {
44    fn default_vec() -> Vec<Self> {
45        vec![Transfer::Basic]
46    }
47}
48
49impl Default for Transfer {
50    fn default() -> Self {
51        Transfer::Basic
52    }
53}
54
55/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#ref-property
56#[derive(PartialEq, Eq, Debug, Deserialize)]
57pub struct Ref {
58    pub name: String,
59}
60
61/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#successful-responses
62#[derive(PartialEq, Eq, Debug, Serialize)]
63#[serde(untagged)]
64pub enum ObjectResponse {
65    Success {
66        #[serde(flatten)]
67        object: Object,
68        #[serde(skip_serializing_if = "Option::is_none")]
69        authenticated: Option<bool>,
70        actions: Actions,
71    },
72    Error {
73        #[serde(flatten)]
74        object: Object,
75        error: ObjectError,
76    },
77}
78
79impl ObjectResponse {
80    pub fn success(object: Object, actions: Actions) -> Self {
81        ObjectResponse::Success {
82            object,
83            authenticated: None,
84            actions,
85        }
86    }
87
88    pub fn error(object: Object, error: ObjectError) -> Self {
89        ObjectResponse::Error { object, error }
90    }
91}
92
93/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#successful-responses
94#[derive(PartialEq, Eq, Debug, Serialize)]
95pub struct ObjectSuccess {
96    #[serde(skip_serializing_if = "Option::is_none")]
97    authenticated: Option<bool>,
98    actions: Actions,
99}
100
101/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#response-errors
102#[derive(PartialEq, Eq, Debug, Serialize)]
103pub struct ObjectError {
104    code: u16,
105    message: &'static str,
106}
107
108impl ObjectError {
109    pub fn DoesNotExist() -> Self {
110        Self {
111            code: 404u16,
112            message: "Object does not exist",
113        }
114    }
115
116    pub fn RemovedByOwner() -> Self {
117        Self {
118            code: 410u16,
119            message: "Object removed by owner",
120        }
121    }
122    pub fn ValidationError() -> Self {
123        Self {
124            code: 422u16,
125            message: "Validation error",
126        }
127    }
128}
129
130/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md#basic-transfer-api
131#[derive(PartialEq, Eq, Debug, Serialize)]
132#[serde(untagged)]
133pub enum Actions {
134    Download { download: Action },
135    None,
136    Upload { upload: Action },
137    UploadAndVerify { upload: Action, verify: Action },
138}
139
140/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md#basic-transfer-api
141#[derive(PartialEq, Eq, Debug, Serialize)]
142pub struct Action {
143    #[serde(with = "url_serde")]
144    href: Url,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    header: Option<HashMap<String, String>>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    expires_in: Option<i32>,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    expires_at: Option<DateTime<FixedOffset>>,
151}
152
153impl Action {
154    pub fn new(href: Url) -> Self {
155        Self {
156            href,
157            header: None,
158            expires_in: None,
159            expires_at: None,
160        }
161    }
162}
163
164/// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#response-errors
165#[derive(PartialEq, Eq, Debug, Serialize)]
166pub struct LfsErrorResponse {
167    message: &'static str,
168    #[serde(with = "url_serde")]
169    documentation_url: Option<Url>,
170    request_id: Option<String>,
171    #[serde(skip)]
172    status: u16,
173}
174
175impl LfsErrorResponse {
176    const ACCEPT_HEADER_INCORRECT: Self = Self {
177        message: "The Accept header needs to be `application/vnd.git-lfs+json`.",
178        documentation_url: None,
179        request_id: None,
180        status: 406u16,
181    };
182    const RATE_LIMIT_HIT: Self = Self {
183        message: "A rate limit has been hit with the server.",
184        documentation_url: None,
185        request_id: None,
186        status: 429u16,
187    };
188    const NOT_IMPLEMENTED: Self = Self {
189        message: "The server has not implemented the current method.",
190        documentation_url: None,
191        request_id: None,
192        status: 501u16,
193    };
194    const INSUFFICIENT_STORAGE: Self = Self {
195        message: "The server has insufficient storage capacity to complete the request.",
196        documentation_url: None,
197        request_id: None,
198        status: 507u16,
199    };
200
201    const BANDWIDTH_LIMIT_EXCEEDED: Self = Self {
202        message: "A bandwidth limit has been exceeded.",
203        documentation_url: None,
204        request_id: None,
205        status: 509u16,
206    };
207}
208
209#[cfg(test)]
210mod test {
211    use super::*;
212    use pretty_assertions::assert_eq;
213
214    #[test]
215    fn batch_response_serializes_correctly() {
216        assert_eq!(
217            include_str!("test/batch_response_success.json"),
218            serde_json::to_string_pretty(&BatchResponse {
219                transfer: Some(Transfer::Basic),
220                objects: vec![ObjectResponse::Success {
221                    object: Object {
222                        oid: "1111111".to_string(),
223                        size: 123,
224                    },
225                    authenticated: Some(true),
226                    actions: Actions::Download {
227                        download: Action {
228                            href: Url::parse("https://some-download.com").unwrap(),
229                            header: Some(
230                                [("Key", "value")]
231                                    .iter()
232                                    .map(|(k, v)| (k.to_string(), v.to_string()))
233                                    .collect()
234                            ),
235                            expires_in: None,
236                            expires_at: DateTime::parse_from_rfc3339("2016-11-10T15:29:07Z")
237                                .unwrap()
238                                .into()
239                        }
240                    }
241                }],
242            })
243            .unwrap(),
244        );
245
246        assert_eq!(
247            include_str!("test/batch_response_error.json"),
248            serde_json::to_string_pretty(&BatchResponse {
249                transfer: Some(Transfer::Basic),
250                objects: vec![ObjectResponse::Error {
251                    error: ObjectError::DoesNotExist(),
252                    object: Object {
253                        oid: "1111111".to_string(),
254                        size: 123,
255                    },
256                }],
257            })
258            .unwrap()
259        );
260    }
261
262    #[test]
263    fn lfs_error_serializes_correctly() {
264        assert_eq!(
265            include_str!("test/lfs_error.json"),
266            serde_json::to_string_pretty(&LfsErrorResponse {
267                message: "Not found",
268                documentation_url: Url::parse("https://lfs-server.com/docs/errors")
269                    .unwrap()
270                    .into(),
271                request_id: Some("123".to_string()),
272                status: 404u16
273            })
274            .unwrap(),
275        );
276    }
277}