diff options
Diffstat (limited to 'gitolfs3-server/src/api.rs')
-rw-r--r-- | gitolfs3-server/src/api.rs | 220 |
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 @@ | |||
1 | use std::collections::HashMap; | 1 | use std::collections::HashMap; |
2 | 2 | ||
3 | use axum::{ | 3 | use 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 | }; |
10 | use chrono::{DateTime, Utc}; | 9 | use chrono::{DateTime, Utc}; |
11 | use gitolfs3_common::{Oid, Operation}; | 10 | use gitolfs3_common::{Oid, Operation}; |
12 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; | 11 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; |
13 | |||
14 | pub const REPO_NOT_FOUND: GitLfsErrorResponse = | ||
15 | make_error_resp(StatusCode::NOT_FOUND, "Repository not found"); | ||
16 | |||
17 | #[derive(Clone)] | ||
18 | pub struct RepositoryName(pub String); | ||
19 | |||
20 | pub struct RepositoryNameRejection; | ||
21 | |||
22 | impl 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] | ||
29 | impl<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 ---------------------- |
42 | pub 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)] | 15 | pub type GitLfsErrorResponse<'a> = (http::StatusCode, GitLfsJson<GitLfsErrorData<'a>>); |
50 | pub enum HashAlgo { | ||
51 | #[serde(rename = "sha256")] | ||
52 | Sha256, | ||
53 | #[serde(other)] | ||
54 | Unknown, | ||
55 | } | ||
56 | 16 | ||
57 | impl Default for HashAlgo { | 17 | #[derive(Debug, Serialize)] |
58 | fn default() -> Self { | 18 | pub struct GitLfsErrorData<'a> { |
59 | Self::Sha256 | 19 | pub message: &'a str, |
60 | } | ||
61 | } | ||
62 | |||
63 | #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] | ||
64 | pub struct BatchRequestObject { | ||
65 | pub oid: Oid, | ||
66 | pub size: i64, | ||
67 | } | ||
68 | |||
69 | #[derive(Debug, Serialize, Deserialize, Clone)] | ||
70 | struct BatchRef { | ||
71 | name: String, | ||
72 | } | 20 | } |
73 | 21 | ||
74 | fn default_transfers() -> Vec<TransferAdapter> { | 22 | pub 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)] | 26 | pub const REPO_NOT_FOUND: GitLfsErrorResponse = |
79 | pub 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)] |
89 | pub struct GitLfsJson<T>(pub Json<T>); | 30 | pub 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 | ||
127 | fn has_git_lfs_json_content_type(req: &Request) -> bool { | 68 | fn 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] | ||
138 | impl<T, S> FromRequest<S> for GitLfsJson<T> | 78 | impl<T, S> FromRequest<S> for GitLfsJson<T> |
139 | where | 79 | where |
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)] |
169 | pub struct GitLfsErrorData<'a> { | 109 | pub struct RepositoryName(pub String); |
170 | pub message: &'a str, | 110 | |
111 | pub struct RepositoryNameRejection; | ||
112 | |||
113 | impl 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 | ||
173 | pub type GitLfsErrorResponse<'a> = (StatusCode, GitLfsJson<GitLfsErrorData<'a>>); | 123 | impl<S: Send + Sync> FromRequestParts<S> for RepositoryName { |
124 | type Rejection = RepositoryNameRejection; | ||
174 | 125 | ||
175 | pub 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 ----------------------- |
180 | pub struct BatchResponseObjectAction { | 139 | |
181 | pub href: String, | 140 | #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] |
182 | #[serde(skip_serializing_if = "HashMap::is_empty")] | 141 | pub 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)] |
188 | pub struct BatchResponseObjectActions { | 151 | pub 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)] |
198 | pub struct BatchResponseObjectError { | 157 | pub enum TransferAdapter { |
199 | pub code: u16, | 158 | #[serde(rename = "basic")] |
200 | pub message: String, | 159 | Basic, |
160 | #[serde(other)] | ||
161 | Unknown, | ||
162 | } | ||
163 | |||
164 | fn default_transfers() -> Vec<TransferAdapter> { | ||
165 | vec![TransferAdapter::Basic] | ||
166 | } | ||
167 | |||
168 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] | ||
169 | pub enum HashAlgo { | ||
170 | #[serde(rename = "sha256")] | ||
171 | Sha256, | ||
172 | #[serde(other)] | ||
173 | Unknown, | ||
174 | } | ||
175 | |||
176 | impl Default for HashAlgo { | ||
177 | fn default() -> Self { | ||
178 | Self::Sha256 | ||
179 | } | ||
180 | } | ||
181 | |||
182 | #[derive(Debug, Serialize, Deserialize, Clone)] | ||
183 | struct BatchRef { | ||
184 | name: String, | ||
185 | } | ||
186 | |||
187 | #[derive(Debug, Serialize, Clone)] | ||
188 | pub 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)] | ||
206 | pub struct BatchResponseObjectError { | ||
207 | pub code: u16, | ||
208 | pub message: String, | ||
209 | } | ||
210 | |||
214 | impl BatchResponseObject { | 211 | impl 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)] |
234 | pub struct BatchResponse { | 231 | pub 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)] | ||
239 | pub 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] |