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