diff options
author | Rutger Broekhoff | 2024-07-12 00:29:57 +0200 |
---|---|---|
committer | Rutger Broekhoff | 2024-07-12 00:29:57 +0200 |
commit | bc709f0f23be345a1e2ccd06acd36bd5dac40bde (patch) | |
tree | 4ffe66b1ac246e0a9eab4a2649a7db5bb3a1ff0a /gitolfs3-server/src/handler.rs | |
parent | 3e67a3486eed22522f4352503ef7067ca81a8050 (diff) | |
download | gitolfs3-main.tar.gz gitolfs3-main.zip |
Diffstat (limited to 'gitolfs3-server/src/handler.rs')
-rw-r--r-- | gitolfs3-server/src/handler.rs | 388 |
1 files changed, 194 insertions, 194 deletions
diff --git a/gitolfs3-server/src/handler.rs b/gitolfs3-server/src/handler.rs index 6516291..b9f9bcf 100644 --- a/gitolfs3-server/src/handler.rs +++ b/gitolfs3-server/src/handler.rs | |||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; | |||
3 | use aws_sdk_s3::{error::SdkError, operation::head_object::HeadObjectOutput}; | 3 | use aws_sdk_s3::{error::SdkError, operation::head_object::HeadObjectOutput}; |
4 | use axum::{ | 4 | use axum::{ |
5 | extract::{Path, State}, | 5 | extract::{Path, State}, |
6 | http::{header, HeaderMap, StatusCode}, | 6 | http, |
7 | response::{IntoResponse, Response}, | 7 | response::{IntoResponse, Response}, |
8 | Json, | 8 | Json, |
9 | }; | 9 | }; |
@@ -33,102 +33,6 @@ pub struct AppState { | |||
33 | pub dl_limiter: Arc<Mutex<DownloadLimiter>>, | 33 | pub dl_limiter: Arc<Mutex<DownloadLimiter>>, |
34 | } | 34 | } |
35 | 35 | ||
36 | fn validate_checksum(oid: Oid, obj: &HeadObjectOutput) -> bool { | ||
37 | if let Some(checksum) = obj.checksum_sha256() { | ||
38 | if let Ok(checksum) = BASE64_STANDARD.decode(checksum) { | ||
39 | if let Ok(checksum32b) = TryInto::<[u8; 32]>::try_into(checksum) { | ||
40 | return Oid::from(checksum32b) == oid; | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | true | ||
45 | } | ||
46 | |||
47 | fn validate_size(expected: i64, obj: &HeadObjectOutput) -> bool { | ||
48 | if let Some(length) = obj.content_length() { | ||
49 | return length == expected; | ||
50 | } | ||
51 | true | ||
52 | } | ||
53 | |||
54 | async fn handle_upload_object( | ||
55 | state: &AppState, | ||
56 | repo: &str, | ||
57 | obj: &BatchRequestObject, | ||
58 | ) -> Option<BatchResponseObject> { | ||
59 | let (oid0, oid1) = (HexByte(obj.oid[0]), HexByte(obj.oid[1])); | ||
60 | let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, obj.oid); | ||
61 | |||
62 | match state | ||
63 | .s3_client | ||
64 | .head_object() | ||
65 | .bucket(&state.s3_bucket) | ||
66 | .key(full_path.clone()) | ||
67 | .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) | ||
68 | .send() | ||
69 | .await | ||
70 | { | ||
71 | Ok(result) => { | ||
72 | if validate_size(obj.size, &result) && validate_checksum(obj.oid, &result) { | ||
73 | return None; | ||
74 | } | ||
75 | } | ||
76 | Err(SdkError::ServiceError(e)) if e.err().is_not_found() => {} | ||
77 | Err(e) => { | ||
78 | println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); | ||
79 | return Some(BatchResponseObject::error( | ||
80 | obj, | ||
81 | StatusCode::INTERNAL_SERVER_ERROR, | ||
82 | "Failed to query object information".to_string(), | ||
83 | )); | ||
84 | } | ||
85 | }; | ||
86 | |||
87 | let expires_in = std::time::Duration::from_secs(5 * 60); | ||
88 | let expires_at = Utc::now() + expires_in; | ||
89 | |||
90 | let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { | ||
91 | return Some(BatchResponseObject::error( | ||
92 | obj, | ||
93 | StatusCode::INTERNAL_SERVER_ERROR, | ||
94 | "Failed to generate upload URL".to_string(), | ||
95 | )); | ||
96 | }; | ||
97 | let Ok(presigned) = state | ||
98 | .s3_client | ||
99 | .put_object() | ||
100 | .bucket(&state.s3_bucket) | ||
101 | .key(full_path) | ||
102 | .checksum_sha256(obj.oid.to_string()) | ||
103 | .content_length(obj.size) | ||
104 | .presigned(config) | ||
105 | .await | ||
106 | else { | ||
107 | return Some(BatchResponseObject::error( | ||
108 | obj, | ||
109 | StatusCode::INTERNAL_SERVER_ERROR, | ||
110 | "Failed to generate upload URL".to_string(), | ||
111 | )); | ||
112 | }; | ||
113 | Some(BatchResponseObject { | ||
114 | oid: obj.oid, | ||
115 | size: obj.size, | ||
116 | authenticated: Some(true), | ||
117 | actions: BatchResponseObjectActions { | ||
118 | upload: Some(BatchResponseObjectAction { | ||
119 | header: presigned | ||
120 | .headers() | ||
121 | .map(|(k, v)| (k.to_owned(), v.to_owned())) | ||
122 | .collect(), | ||
123 | expires_at, | ||
124 | href: presigned.uri().to_string(), | ||
125 | }), | ||
126 | ..Default::default() | ||
127 | }, | ||
128 | error: None, | ||
129 | }) | ||
130 | } | ||
131 | |||
132 | async fn handle_download_object( | 36 | async fn handle_download_object( |
133 | state: &AppState, | 37 | state: &AppState, |
134 | repo: &str, | 38 | repo: &str, |
@@ -152,24 +56,24 @@ async fn handle_download_object( | |||
152 | println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); | 56 | println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); |
153 | return BatchResponseObject::error( | 57 | return BatchResponseObject::error( |
154 | obj, | 58 | obj, |
155 | StatusCode::INTERNAL_SERVER_ERROR, | 59 | http::StatusCode::INTERNAL_SERVER_ERROR, |
156 | "Failed to query object information".to_string(), | 60 | "Failed to query object information".to_string(), |
157 | ); | 61 | ); |
158 | } | 62 | } |
159 | }; | 63 | }; |
160 | 64 | ||
161 | // Scaleway actually doesn't provide SHA256 suport, but maybe in the future :) | 65 | // Scaleway actually doesn't provide SHA256 support, but maybe in the future :) |
162 | if !validate_checksum(obj.oid, &result) { | 66 | if !s3_validate_checksum(obj.oid, &result) { |
163 | return BatchResponseObject::error( | 67 | return BatchResponseObject::error( |
164 | obj, | 68 | obj, |
165 | StatusCode::UNPROCESSABLE_ENTITY, | 69 | http::StatusCode::UNPROCESSABLE_ENTITY, |
166 | "Object corrupted".to_string(), | 70 | "Object corrupted".to_string(), |
167 | ); | 71 | ); |
168 | } | 72 | } |
169 | if !validate_size(obj.size, &result) { | 73 | if !s3_validate_size(obj.size, &result) { |
170 | return BatchResponseObject::error( | 74 | return BatchResponseObject::error( |
171 | obj, | 75 | obj, |
172 | StatusCode::UNPROCESSABLE_ENTITY, | 76 | http::StatusCode::UNPROCESSABLE_ENTITY, |
173 | "Incorrect size specified (or object corrupted)".to_string(), | 77 | "Incorrect size specified (or object corrupted)".to_string(), |
174 | ); | 78 | ); |
175 | } | 79 | } |
@@ -181,7 +85,7 @@ async fn handle_download_object( | |||
181 | let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { | 85 | let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { |
182 | return BatchResponseObject::error( | 86 | return BatchResponseObject::error( |
183 | obj, | 87 | obj, |
184 | StatusCode::INTERNAL_SERVER_ERROR, | 88 | http::StatusCode::INTERNAL_SERVER_ERROR, |
185 | "Failed to generate upload URL".to_string(), | 89 | "Failed to generate upload URL".to_string(), |
186 | ); | 90 | ); |
187 | }; | 91 | }; |
@@ -195,7 +99,7 @@ async fn handle_download_object( | |||
195 | else { | 99 | else { |
196 | return BatchResponseObject::error( | 100 | return BatchResponseObject::error( |
197 | obj, | 101 | obj, |
198 | StatusCode::INTERNAL_SERVER_ERROR, | 102 | http::StatusCode::INTERNAL_SERVER_ERROR, |
199 | "Failed to generate upload URL".to_string(), | 103 | "Failed to generate upload URL".to_string(), |
200 | ); | 104 | ); |
201 | }; | 105 | }; |
@@ -231,7 +135,7 @@ async fn handle_download_object( | |||
231 | Ok(false) => { | 135 | Ok(false) => { |
232 | return BatchResponseObject::error( | 136 | return BatchResponseObject::error( |
233 | obj, | 137 | obj, |
234 | StatusCode::SERVICE_UNAVAILABLE, | 138 | http::StatusCode::SERVICE_UNAVAILABLE, |
235 | "Public LFS downloads temporarily unavailable".to_string(), | 139 | "Public LFS downloads temporarily unavailable".to_string(), |
236 | ); | 140 | ); |
237 | } | 141 | } |
@@ -239,7 +143,7 @@ async fn handle_download_object( | |||
239 | println!("Failed to request {content_length} bytes from download limiter: {e}"); | 143 | println!("Failed to request {content_length} bytes from download limiter: {e}"); |
240 | return BatchResponseObject::error( | 144 | return BatchResponseObject::error( |
241 | obj, | 145 | obj, |
242 | StatusCode::INTERNAL_SERVER_ERROR, | 146 | http::StatusCode::INTERNAL_SERVER_ERROR, |
243 | "Internal server error".to_string(), | 147 | "Internal server error".to_string(), |
244 | ); | 148 | ); |
245 | } | 149 | } |
@@ -257,7 +161,7 @@ async fn handle_download_object( | |||
257 | ) else { | 161 | ) else { |
258 | return BatchResponseObject::error( | 162 | return BatchResponseObject::error( |
259 | obj, | 163 | obj, |
260 | StatusCode::INTERNAL_SERVER_ERROR, | 164 | http::StatusCode::INTERNAL_SERVER_ERROR, |
261 | "Internal server error".to_string(), | 165 | "Internal server error".to_string(), |
262 | ); | 166 | ); |
263 | }; | 167 | }; |
@@ -292,83 +196,6 @@ async fn handle_download_object( | |||
292 | } | 196 | } |
293 | } | 197 | } |
294 | 198 | ||
295 | fn repo_exists(name: &str) -> bool { | ||
296 | let Ok(metadata) = std::fs::metadata(name) else { | ||
297 | return false; | ||
298 | }; | ||
299 | metadata.is_dir() | ||
300 | } | ||
301 | |||
302 | fn is_repo_public(name: &str) -> Option<bool> { | ||
303 | if !repo_exists(name) { | ||
304 | return None; | ||
305 | } | ||
306 | match std::fs::metadata(format!("{name}/git-daemon-export-ok")) { | ||
307 | Ok(metadata) if metadata.is_file() => Some(true), | ||
308 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(false), | ||
309 | _ => None, | ||
310 | } | ||
311 | } | ||
312 | |||
313 | pub async fn batch( | ||
314 | State(state): State<Arc<AppState>>, | ||
315 | headers: HeaderMap, | ||
316 | RepositoryName(repo): RepositoryName, | ||
317 | GitLfsJson(Json(payload)): GitLfsJson<BatchRequest>, | ||
318 | ) -> Response { | ||
319 | let Some(public) = is_repo_public(&repo) else { | ||
320 | return REPO_NOT_FOUND.into_response(); | ||
321 | }; | ||
322 | let Trusted(trusted) = match authorize_batch( | ||
323 | &state.authz_conf, | ||
324 | &repo, | ||
325 | public, | ||
326 | payload.operation, | ||
327 | &headers, | ||
328 | ) { | ||
329 | Ok(authn) => authn, | ||
330 | Err(e) => return e.into_response(), | ||
331 | }; | ||
332 | |||
333 | if !headers | ||
334 | .get_all("Accept") | ||
335 | .iter() | ||
336 | .filter_map(|v| v.to_str().ok()) | ||
337 | .any(is_git_lfs_json_mimetype) | ||
338 | { | ||
339 | let message = format!("Expected `{LFS_MIME}` in list of acceptable response media types"); | ||
340 | return make_error_resp(StatusCode::NOT_ACCEPTABLE, &message).into_response(); | ||
341 | } | ||
342 | |||
343 | if payload.hash_algo != HashAlgo::Sha256 { | ||
344 | let message = "Unsupported hashing algorithm specified"; | ||
345 | return make_error_resp(StatusCode::CONFLICT, message).into_response(); | ||
346 | } | ||
347 | if !payload.transfers.is_empty() && !payload.transfers.contains(&TransferAdapter::Basic) { | ||
348 | let message = "Unsupported transfer adapter specified (supported: basic)"; | ||
349 | return make_error_resp(StatusCode::CONFLICT, message).into_response(); | ||
350 | } | ||
351 | |||
352 | let mut resp = BatchResponse { | ||
353 | transfer: TransferAdapter::Basic, | ||
354 | objects: vec![], | ||
355 | hash_algo: HashAlgo::Sha256, | ||
356 | }; | ||
357 | for obj in payload.objects { | ||
358 | match payload.operation { | ||
359 | Operation::Download => resp | ||
360 | .objects | ||
361 | .push(handle_download_object(&state, &repo, &obj, trusted).await), | ||
362 | Operation::Upload => { | ||
363 | if let Some(obj_resp) = handle_upload_object(&state, &repo, &obj).await { | ||
364 | resp.objects.push(obj_resp); | ||
365 | } | ||
366 | } | ||
367 | }; | ||
368 | } | ||
369 | GitLfsJson(Json(resp)).into_response() | ||
370 | } | ||
371 | |||
372 | #[derive(Deserialize, Copy, Clone)] | 199 | #[derive(Deserialize, Copy, Clone)] |
373 | #[serde(remote = "Self")] | 200 | #[serde(remote = "Self")] |
374 | pub struct FileParams { | 201 | pub struct FileParams { |
@@ -382,11 +209,11 @@ impl<'de> Deserialize<'de> for FileParams { | |||
382 | where | 209 | where |
383 | D: serde::Deserializer<'de>, | 210 | D: serde::Deserializer<'de>, |
384 | { | 211 | { |
385 | let unchecked @ FileParams { | 212 | let unchecked @ Self { |
386 | oid0: HexByte(oid0), | 213 | oid0: HexByte(oid0), |
387 | oid1: HexByte(oid1), | 214 | oid1: HexByte(oid1), |
388 | oid, | 215 | oid, |
389 | } = FileParams::deserialize(deserializer)?; | 216 | } = Self::deserialize(deserializer)?; |
390 | if oid0 != oid.as_bytes()[0] { | 217 | if oid0 != oid.as_bytes()[0] { |
391 | return Err(de::Error::custom( | 218 | return Err(de::Error::custom( |
392 | "first OID path part does not match first byte of full OID", | 219 | "first OID path part does not match first byte of full OID", |
@@ -401,9 +228,9 @@ impl<'de> Deserialize<'de> for FileParams { | |||
401 | } | 228 | } |
402 | } | 229 | } |
403 | 230 | ||
404 | pub async fn obj_download( | 231 | pub async fn handle_obj_download( |
405 | State(state): State<Arc<AppState>>, | 232 | State(state): State<Arc<AppState>>, |
406 | headers: HeaderMap, | 233 | headers: http::HeaderMap, |
407 | RepositoryName(repo): RepositoryName, | 234 | RepositoryName(repo): RepositoryName, |
408 | Path(FileParams { oid0, oid1, oid }): Path<FileParams>, | 235 | Path(FileParams { oid0, oid1, oid }): Path<FileParams>, |
409 | ) -> Response { | 236 | ) -> Response { |
@@ -425,26 +252,26 @@ pub async fn obj_download( | |||
425 | Err(e) => { | 252 | Err(e) => { |
426 | println!("Failed to GetObject (repo {repo}, OID {oid}): {e}"); | 253 | println!("Failed to GetObject (repo {repo}, OID {oid}): {e}"); |
427 | return ( | 254 | return ( |
428 | StatusCode::INTERNAL_SERVER_ERROR, | 255 | http::StatusCode::INTERNAL_SERVER_ERROR, |
429 | "Failed to query object information", | 256 | "Failed to query object information", |
430 | ) | 257 | ) |
431 | .into_response(); | 258 | .into_response(); |
432 | } | 259 | } |
433 | }; | 260 | }; |
434 | 261 | ||
435 | let mut headers = header::HeaderMap::new(); | 262 | let mut headers = http::header::HeaderMap::new(); |
436 | if let Some(content_type) = result.content_type { | 263 | if let Some(content_type) = result.content_type { |
437 | let Ok(header_value) = content_type.try_into() else { | 264 | let Ok(header_value) = content_type.try_into() else { |
438 | return ( | 265 | return ( |
439 | StatusCode::INTERNAL_SERVER_ERROR, | 266 | http::StatusCode::INTERNAL_SERVER_ERROR, |
440 | "Object has invalid content type", | 267 | "Object has invalid content type", |
441 | ) | 268 | ) |
442 | .into_response(); | 269 | .into_response(); |
443 | }; | 270 | }; |
444 | headers.insert(header::CONTENT_TYPE, header_value); | 271 | headers.insert(http::header::CONTENT_TYPE, header_value); |
445 | } | 272 | } |
446 | if let Some(content_length) = result.content_length { | 273 | if let Some(content_length) = result.content_length { |
447 | headers.insert(header::CONTENT_LENGTH, content_length.into()); | 274 | headers.insert(http::header::CONTENT_LENGTH, content_length.into()); |
448 | } | 275 | } |
449 | 276 | ||
450 | let async_read = result.body.into_async_read(); | 277 | let async_read = result.body.into_async_read(); |
@@ -453,3 +280,176 @@ pub async fn obj_download( | |||
453 | 280 | ||
454 | (headers, body).into_response() | 281 | (headers, body).into_response() |
455 | } | 282 | } |
283 | |||
284 | async fn handle_upload_object( | ||
285 | state: &AppState, | ||
286 | repo: &str, | ||
287 | obj: &BatchRequestObject, | ||
288 | ) -> Option<BatchResponseObject> { | ||
289 | let (oid0, oid1) = (HexByte(obj.oid[0]), HexByte(obj.oid[1])); | ||
290 | let full_path = format!("{repo}/lfs/objects/{}/{}/{}", oid0, oid1, obj.oid); | ||
291 | |||
292 | match state | ||
293 | .s3_client | ||
294 | .head_object() | ||
295 | .bucket(&state.s3_bucket) | ||
296 | .key(full_path.clone()) | ||
297 | .checksum_mode(aws_sdk_s3::types::ChecksumMode::Enabled) | ||
298 | .send() | ||
299 | .await | ||
300 | { | ||
301 | Ok(result) => { | ||
302 | if s3_validate_size(obj.size, &result) && s3_validate_checksum(obj.oid, &result) { | ||
303 | return None; | ||
304 | } | ||
305 | } | ||
306 | Err(SdkError::ServiceError(e)) if e.err().is_not_found() => {} | ||
307 | Err(e) => { | ||
308 | println!("Failed to HeadObject (repo {repo}, OID {}): {e}", obj.oid); | ||
309 | return Some(BatchResponseObject::error( | ||
310 | obj, | ||
311 | http::StatusCode::INTERNAL_SERVER_ERROR, | ||
312 | "Failed to query object information".to_string(), | ||
313 | )); | ||
314 | } | ||
315 | }; | ||
316 | |||
317 | let expires_in = std::time::Duration::from_secs(5 * 60); | ||
318 | let expires_at = Utc::now() + expires_in; | ||
319 | |||
320 | let Ok(config) = aws_sdk_s3::presigning::PresigningConfig::expires_in(expires_in) else { | ||
321 | return Some(BatchResponseObject::error( | ||
322 | obj, | ||
323 | http::StatusCode::INTERNAL_SERVER_ERROR, | ||
324 | "Failed to generate upload URL".to_string(), | ||
325 | )); | ||
326 | }; | ||
327 | let Ok(presigned) = state | ||
328 | .s3_client | ||
329 | .put_object() | ||
330 | .bucket(&state.s3_bucket) | ||
331 | .key(full_path) | ||
332 | .checksum_sha256(obj.oid.to_string()) | ||
333 | .content_length(obj.size) | ||
334 | .presigned(config) | ||
335 | .await | ||
336 | else { | ||
337 | return Some(BatchResponseObject::error( | ||
338 | obj, | ||
339 | http::StatusCode::INTERNAL_SERVER_ERROR, | ||
340 | "Failed to generate upload URL".to_string(), | ||
341 | )); | ||
342 | }; | ||
343 | Some(BatchResponseObject { | ||
344 | oid: obj.oid, | ||
345 | size: obj.size, | ||
346 | authenticated: Some(true), | ||
347 | actions: BatchResponseObjectActions { | ||
348 | upload: Some(BatchResponseObjectAction { | ||
349 | header: presigned | ||
350 | .headers() | ||
351 | .map(|(k, v)| (k.to_owned(), v.to_owned())) | ||
352 | .collect(), | ||
353 | expires_at, | ||
354 | href: presigned.uri().to_string(), | ||
355 | }), | ||
356 | ..Default::default() | ||
357 | }, | ||
358 | error: None, | ||
359 | }) | ||
360 | } | ||
361 | |||
362 | pub async fn handle_batch( | ||
363 | State(state): State<Arc<AppState>>, | ||
364 | headers: http::HeaderMap, | ||
365 | RepositoryName(repo): RepositoryName, | ||
366 | GitLfsJson(Json(payload)): GitLfsJson<BatchRequest>, | ||
367 | ) -> Response { | ||
368 | let Some(public) = is_repo_public(&repo) else { | ||
369 | return REPO_NOT_FOUND.into_response(); | ||
370 | }; | ||
371 | let Trusted(trusted) = match authorize_batch( | ||
372 | &state.authz_conf, | ||
373 | &repo, | ||
374 | public, | ||
375 | payload.operation, | ||
376 | &headers, | ||
377 | ) { | ||
378 | Ok(authn) => authn, | ||
379 | Err(e) => return e.into_response(), | ||
380 | }; | ||
381 | |||
382 | if !headers | ||
383 | .get_all("Accept") | ||
384 | .iter() | ||
385 | .filter_map(|v| v.to_str().ok()) | ||
386 | .any(is_git_lfs_json_mimetype) | ||
387 | { | ||
388 | let message = format!("Expected `{LFS_MIME}` in list of acceptable response media types"); | ||
389 | return make_error_resp(http::StatusCode::NOT_ACCEPTABLE, &message).into_response(); | ||
390 | } | ||
391 | |||
392 | if payload.hash_algo != HashAlgo::Sha256 { | ||
393 | let message = "Unsupported hashing algorithm specified"; | ||
394 | return make_error_resp(http::StatusCode::CONFLICT, message).into_response(); | ||
395 | } | ||
396 | if !payload.transfers.is_empty() && !payload.transfers.contains(&TransferAdapter::Basic) { | ||
397 | let message = "Unsupported transfer adapter specified (supported: basic)"; | ||
398 | return make_error_resp(http::StatusCode::CONFLICT, message).into_response(); | ||
399 | } | ||
400 | |||
401 | let mut resp = BatchResponse { | ||
402 | transfer: TransferAdapter::Basic, | ||
403 | objects: vec![], | ||
404 | hash_algo: HashAlgo::Sha256, | ||
405 | }; | ||
406 | for obj in payload.objects { | ||
407 | match payload.operation { | ||
408 | Operation::Download => resp | ||
409 | .objects | ||
410 | .push(handle_download_object(&state, &repo, &obj, trusted).await), | ||
411 | Operation::Upload => { | ||
412 | if let Some(obj_resp) = handle_upload_object(&state, &repo, &obj).await { | ||
413 | resp.objects.push(obj_resp); | ||
414 | } | ||
415 | } | ||
416 | }; | ||
417 | } | ||
418 | GitLfsJson(Json(resp)).into_response() | ||
419 | } | ||
420 | |||
421 | fn s3_validate_checksum(oid: Oid, obj: &HeadObjectOutput) -> bool { | ||
422 | if let Some(checksum) = obj.checksum_sha256() { | ||
423 | if let Ok(checksum) = BASE64_STANDARD.decode(checksum) { | ||
424 | if let Ok(checksum32b) = TryInto::<[u8; 32]>::try_into(checksum) { | ||
425 | return Oid::from(checksum32b) == oid; | ||
426 | } | ||
427 | } | ||
428 | } | ||
429 | true | ||
430 | } | ||
431 | |||
432 | fn s3_validate_size(expected: i64, obj: &HeadObjectOutput) -> bool { | ||
433 | if let Some(length) = obj.content_length() { | ||
434 | return length == expected; | ||
435 | } | ||
436 | true | ||
437 | } | ||
438 | |||
439 | fn repo_exists(name: &str) -> bool { | ||
440 | let Ok(metadata) = std::fs::metadata(name) else { | ||
441 | return false; | ||
442 | }; | ||
443 | metadata.is_dir() | ||
444 | } | ||
445 | |||
446 | fn is_repo_public(name: &str) -> Option<bool> { | ||
447 | if !repo_exists(name) { | ||
448 | return None; | ||
449 | } | ||
450 | match std::fs::metadata(format!("{name}/git-daemon-export-ok")) { | ||
451 | Ok(metadata) if metadata.is_file() => Some(true), | ||
452 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(false), | ||
453 | _ => None, | ||
454 | } | ||
455 | } | ||