aboutsummaryrefslogtreecommitdiffstats
path: root/gitolfs3-server/src/api.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gitolfs3-server/src/api.rs')
-rw-r--r--gitolfs3-server/src/api.rs220
1 files changed, 114 insertions, 106 deletions
diff --git a/gitolfs3-server/src/api.rs b/gitolfs3-server/src/api.rs
index dba7ada..e1a2983 100644
--- a/gitolfs3-server/src/api.rs
+++ b/gitolfs3-server/src/api.rs
@@ -1,89 +1,30 @@
1use std::collections::HashMap; 1use std::collections::HashMap;
2 2
3use axum::{ 3use axum::{
4 async_trait,
5 extract::{rejection, FromRequest, FromRequestParts, Request},
6 http::{header, request::Parts, HeaderValue, StatusCode},
7 response::{IntoResponse, Response},
8 Extension, Json, 4 Extension, Json,
5 extract::{FromRequest, FromRequestParts, Request, rejection},
6 http,
7 response::{IntoResponse, Response},
9}; 8};
10use chrono::{DateTime, Utc}; 9use chrono::{DateTime, Utc};
11use gitolfs3_common::{Oid, Operation}; 10use gitolfs3_common::{Oid, Operation};
12use serde::{de::DeserializeOwned, Deserialize, Serialize}; 11use serde::{Deserialize, Serialize, de::DeserializeOwned};
13
14pub const REPO_NOT_FOUND: GitLfsErrorResponse =
15 make_error_resp(StatusCode::NOT_FOUND, "Repository not found");
16
17#[derive(Clone)]
18pub struct RepositoryName(pub String);
19
20pub struct RepositoryNameRejection;
21
22impl IntoResponse for RepositoryNameRejection {
23 fn into_response(self) -> Response {
24 (StatusCode::INTERNAL_SERVER_ERROR, "Missing repository name").into_response()
25 }
26}
27
28#[async_trait]
29impl<S: Send + Sync> FromRequestParts<S> for RepositoryName {
30 type Rejection = RepositoryNameRejection;
31
32 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
33 let Ok(Extension(repo_name)) = Extension::<Self>::from_request_parts(parts, state).await
34 else {
35 return Err(RepositoryNameRejection);
36 };
37 Ok(repo_name)
38 }
39}
40 12
41#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] 13// ----------------------- Generic facilities ----------------------
42pub enum TransferAdapter {
43 #[serde(rename = "basic")]
44 Basic,
45 #[serde(other)]
46 Unknown,
47}
48 14
49#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] 15pub type GitLfsErrorResponse<'a> = (http::StatusCode, GitLfsJson<GitLfsErrorData<'a>>);
50pub enum HashAlgo {
51 #[serde(rename = "sha256")]
52 Sha256,
53 #[serde(other)]
54 Unknown,
55}
56 16
57impl Default for HashAlgo { 17#[derive(Debug, Serialize)]
58 fn default() -> Self { 18pub struct GitLfsErrorData<'a> {
59 Self::Sha256 19 pub message: &'a str,
60 }
61}
62
63#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
64pub struct BatchRequestObject {
65 pub oid: Oid,
66 pub size: i64,
67}
68
69#[derive(Debug, Serialize, Deserialize, Clone)]
70struct BatchRef {
71 name: String,
72} 20}
73 21
74fn default_transfers() -> Vec<TransferAdapter> { 22pub const fn make_error_resp(code: http::StatusCode, message: &str) -> GitLfsErrorResponse {
75 vec![TransferAdapter::Basic] 23 (code, GitLfsJson(Json(GitLfsErrorData { message })))
76} 24}
77 25
78#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] 26pub const REPO_NOT_FOUND: GitLfsErrorResponse =
79pub struct BatchRequest { 27 make_error_resp(http::StatusCode::NOT_FOUND, "Repository not found");
80 pub operation: Operation,
81 #[serde(default = "default_transfers")]
82 pub transfers: Vec<TransferAdapter>,
83 pub objects: Vec<BatchRequestObject>,
84 #[serde(default)]
85 pub hash_algo: HashAlgo,
86}
87 28
88#[derive(Debug, Clone)] 29#[derive(Debug, Clone)]
89pub struct GitLfsJson<T>(pub Json<T>); 30pub struct GitLfsJson<T>(pub Json<T>);
@@ -100,7 +41,7 @@ impl IntoResponse for GitLfsJsonRejection {
100 match self { 41 match self {
101 Self::Json(rej) => rej.into_response(), 42 Self::Json(rej) => rej.into_response(),
102 Self::MissingGitLfsJsonContentType => make_error_resp( 43 Self::MissingGitLfsJsonContentType => make_error_resp(
103 StatusCode::UNSUPPORTED_MEDIA_TYPE, 44 http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
104 &format!("Expected request with `Content-Type: {LFS_MIME}`"), 45 &format!("Expected request with `Content-Type: {LFS_MIME}`"),
105 ) 46 )
106 .into_response(), 47 .into_response(),
@@ -125,7 +66,7 @@ pub fn is_git_lfs_json_mimetype(mimetype: &str) -> bool {
125} 66}
126 67
127fn has_git_lfs_json_content_type(req: &Request) -> bool { 68fn has_git_lfs_json_content_type(req: &Request) -> bool {
128 let Some(content_type) = req.headers().get(header::CONTENT_TYPE) else { 69 let Some(content_type) = req.headers().get(http::header::CONTENT_TYPE) else {
129 return false; 70 return false;
130 }; 71 };
131 let Ok(content_type) = content_type.to_str() else { 72 let Ok(content_type) = content_type.to_str() else {
@@ -134,7 +75,6 @@ fn has_git_lfs_json_content_type(req: &Request) -> bool {
134 is_git_lfs_json_mimetype(content_type) 75 is_git_lfs_json_mimetype(content_type)
135} 76}
136 77
137#[async_trait]
138impl<T, S> FromRequest<S> for GitLfsJson<T> 78impl<T, S> FromRequest<S> for GitLfsJson<T>
139where 79where
140 T: DeserializeOwned, 80 T: DeserializeOwned,
@@ -158,46 +98,97 @@ impl<T: Serialize> IntoResponse for GitLfsJson<T> {
158 let GitLfsJson(json) = self; 98 let GitLfsJson(json) = self;
159 let mut resp = json.into_response(); 99 let mut resp = json.into_response();
160 resp.headers_mut().insert( 100 resp.headers_mut().insert(
161 header::CONTENT_TYPE, 101 http::header::CONTENT_TYPE,
162 HeaderValue::from_static("application/vnd.git-lfs+json; charset=utf-8"), 102 http::HeaderValue::from_static("application/vnd.git-lfs+json; charset=utf-8"),
163 ); 103 );
164 resp 104 resp
165 } 105 }
166} 106}
167 107
168#[derive(Debug, Serialize)] 108#[derive(Clone)]
169pub struct GitLfsErrorData<'a> { 109pub struct RepositoryName(pub String);
170 pub message: &'a str, 110
111pub struct RepositoryNameRejection;
112
113impl IntoResponse for RepositoryNameRejection {
114 fn into_response(self) -> Response {
115 (
116 http::StatusCode::INTERNAL_SERVER_ERROR,
117 "Missing repository name",
118 )
119 .into_response()
120 }
171} 121}
172 122
173pub type GitLfsErrorResponse<'a> = (StatusCode, GitLfsJson<GitLfsErrorData<'a>>); 123impl<S: Send + Sync> FromRequestParts<S> for RepositoryName {
124 type Rejection = RepositoryNameRejection;
174 125
175pub const fn make_error_resp(code: StatusCode, message: &str) -> GitLfsErrorResponse { 126 async fn from_request_parts(
176 (code, GitLfsJson(Json(GitLfsErrorData { message }))) 127 parts: &mut http::request::Parts,
128 state: &S,
129 ) -> Result<Self, Self::Rejection> {
130 let Ok(Extension(repo_name)) = Extension::<Self>::from_request_parts(parts, state).await
131 else {
132 return Err(RepositoryNameRejection);
133 };
134 Ok(repo_name)
135 }
177} 136}
178 137
179#[derive(Debug, Serialize, Clone)] 138// ----------------------- Git LFS Batch API -----------------------
180pub struct BatchResponseObjectAction { 139
181 pub href: String, 140#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
182 #[serde(skip_serializing_if = "HashMap::is_empty")] 141pub struct BatchRequest {
183 pub header: HashMap<String, String>, 142 pub operation: Operation,
184 pub expires_at: DateTime<Utc>, 143 #[serde(default = "default_transfers")]
144 pub transfers: Vec<TransferAdapter>,
145 pub objects: Vec<BatchRequestObject>,
146 #[serde(default)]
147 pub hash_algo: HashAlgo,
185} 148}
186 149
187#[derive(Default, Debug, Serialize, Clone)] 150#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
188pub struct BatchResponseObjectActions { 151pub struct BatchRequestObject {
189 #[serde(skip_serializing_if = "Option::is_none")] 152 pub oid: Oid,
190 pub upload: Option<BatchResponseObjectAction>, 153 pub size: i64,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub download: Option<BatchResponseObjectAction>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub verify: Option<BatchResponseObjectAction>,
195} 154}
196 155
197#[derive(Debug, Clone, Serialize)] 156#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
198pub struct BatchResponseObjectError { 157pub enum TransferAdapter {
199 pub code: u16, 158 #[serde(rename = "basic")]
200 pub message: String, 159 Basic,
160 #[serde(other)]
161 Unknown,
162}
163
164fn default_transfers() -> Vec<TransferAdapter> {
165 vec![TransferAdapter::Basic]
166}
167
168#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
169pub enum HashAlgo {
170 #[serde(rename = "sha256")]
171 Sha256,
172 #[serde(other)]
173 Unknown,
174}
175
176impl Default for HashAlgo {
177 fn default() -> Self {
178 Self::Sha256
179 }
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone)]
183struct BatchRef {
184 name: String,
185}
186
187#[derive(Debug, Serialize, Clone)]
188pub struct BatchResponse {
189 pub transfer: TransferAdapter,
190 pub objects: Vec<BatchResponseObject>,
191 pub hash_algo: HashAlgo,
201} 192}
202 193
203#[derive(Debug, Serialize, Clone)] 194#[derive(Debug, Serialize, Clone)]
@@ -211,10 +202,16 @@ pub struct BatchResponseObject {
211 pub error: Option<BatchResponseObjectError>, 202 pub error: Option<BatchResponseObjectError>,
212} 203}
213 204
205#[derive(Debug, Clone, Serialize)]
206pub struct BatchResponseObjectError {
207 pub code: u16,
208 pub message: String,
209}
210
214impl BatchResponseObject { 211impl BatchResponseObject {
215 pub fn error( 212 pub fn error(
216 obj: &BatchRequestObject, 213 obj: &BatchRequestObject,
217 code: StatusCode, 214 code: http::StatusCode,
218 message: String, 215 message: String,
219 ) -> BatchResponseObject { 216 ) -> BatchResponseObject {
220 BatchResponseObject { 217 BatchResponseObject {
@@ -231,10 +228,21 @@ impl BatchResponseObject {
231} 228}
232 229
233#[derive(Debug, Serialize, Clone)] 230#[derive(Debug, Serialize, Clone)]
234pub struct BatchResponse { 231pub struct BatchResponseObjectAction {
235 pub transfer: TransferAdapter, 232 pub href: String,
236 pub objects: Vec<BatchResponseObject>, 233 #[serde(skip_serializing_if = "HashMap::is_empty")]
237 pub hash_algo: HashAlgo, 234 pub header: HashMap<String, String>,
235 pub expires_at: DateTime<Utc>,
236}
237
238#[derive(Default, Debug, Serialize, Clone)]
239pub struct BatchResponseObjectActions {
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub upload: Option<BatchResponseObjectAction>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub download: Option<BatchResponseObjectAction>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub verify: Option<BatchResponseObjectAction>,
238} 246}
239 247
240#[test] 248#[test]