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.rs213
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;
3use axum::{ 3use 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};
11use gitolfs3_common::{Oid, Operation}; 11use gitolfs3_common::{Oid, Operation};
12use serde::{de::DeserializeOwned, Deserialize, Serialize}; 12use serde::{de::DeserializeOwned, Deserialize, Serialize};
13 13
14pub const REPO_NOT_FOUND: GitLfsErrorResponse = 14// ----------------------- Generic facilities ----------------------
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 15
41#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] 16pub type GitLfsErrorResponse<'a> = (http::StatusCode, GitLfsJson<GitLfsErrorData<'a>>);
42pub 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)]
50pub enum HashAlgo { 19pub struct GitLfsErrorData<'a> {
51 #[serde(rename = "sha256")] 20 pub message: &'a str,
52 Sha256,
53 #[serde(other)]
54 Unknown,
55}
56
57impl Default for HashAlgo {
58 fn default() -> Self {
59 Self::Sha256
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} 21}
73 22
74fn default_transfers() -> Vec<TransferAdapter> { 23pub 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)] 27pub const REPO_NOT_FOUND: GitLfsErrorResponse =
79pub 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)]
89pub struct GitLfsJson<T>(pub Json<T>); 31pub 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
127fn has_git_lfs_json_content_type(req: &Request) -> bool { 69fn 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)]
169pub struct GitLfsErrorData<'a> { 111pub struct RepositoryName(pub String);
170 pub message: &'a str, 112
113pub struct RepositoryNameRejection;
114
115impl 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
173pub type GitLfsErrorResponse<'a> = (StatusCode, GitLfsJson<GitLfsErrorData<'a>>); 125#[async_trait]
126impl<S: Send + Sync> FromRequestParts<S> for RepositoryName {
127 type Rejection = RepositoryNameRejection;
174 128
175pub 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 -----------------------
180pub struct BatchResponseObjectAction { 142
181 pub href: String, 143#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
182 #[serde(skip_serializing_if = "HashMap::is_empty")] 144pub 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)]
188pub struct BatchResponseObjectActions { 154pub 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)]
198pub struct BatchResponseObjectError { 160pub enum TransferAdapter {
199 pub code: u16, 161 #[serde(rename = "basic")]
200 pub message: String, 162 Basic,
163 #[serde(other)]
164 Unknown,
165}
166
167fn default_transfers() -> Vec<TransferAdapter> {
168 vec![TransferAdapter::Basic]
169}
170
171#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
172pub enum HashAlgo {
173 #[serde(rename = "sha256")]
174 Sha256,
175 #[serde(other)]
176 Unknown,
177}
178
179impl Default for HashAlgo {
180 fn default() -> Self {
181 Self::Sha256
182 }
183}
184
185#[derive(Debug, Serialize, Deserialize, Clone)]
186struct BatchRef {
187 name: String,
188}
189
190#[derive(Debug, Serialize, Clone)]
191pub 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)]
209pub struct BatchResponseObjectError {
210 pub code: u16,
211 pub message: String,
212}
213
214impl BatchResponseObject { 214impl 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)]
234pub struct BatchResponse { 234pub 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)]
242pub 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]