1use crate::{
2 ByteProgress, ByteRange, CloudProvider, HeldTokens, OAuthTokens, StorageError, StorageFailure,
3 TokenCustody,
4};
5use coven_crypto::SecretBytes;
6use coven_foundation::clock::ClockRef;
7use reqwest::{Method, Response};
8use serde_json::Value;
9use std::fmt;
10use std::sync::Arc;
11
12pub struct OAuthSession {
16 client: super::http_client::HttpClient,
17 provider: CloudProvider,
18 tokens: HeldTokens,
19 clock: ClockRef,
20}
21impl OAuthSession {
22 pub(crate) fn new(
25 provider: CloudProvider,
26 tokens: OAuthTokens,
27 custody: Arc<dyn TokenCustody>,
28 clock: ClockRef,
29 client: super::http_client::HttpClient,
30 ) -> Result<Self, StorageError> {
31 if !provider.signs_in_with_oauth() {
32 return Err(
33 StorageFailure::InvalidConfiguration.with_source("provider does not use OAuth")
34 );
35 }
36 Ok(Self { client, provider, tokens: HeldTokens::new(tokens, custody), clock })
37 }
38 pub async fn account(&self) -> Result<String, StorageError> {
40 let api = super::oauth_api(self.provider).expect("checked OAuth provider");
41 self.account_at(api).await
42 }
43 pub(crate) async fn account_at(&self, api: &str) -> Result<String, StorageError> {
44 let (method, url, body) = match self.provider {
45 CloudProvider::Dropbox => (
46 Method::POST,
47 endpoint(api, &["users", "get_current_account"], &[])?,
48 Body::Json(Value::Null),
49 ),
50 CloudProvider::OneDrive => (
51 Method::GET,
52 endpoint(api, &["me"], &[("$select", "mail,userPrincipalName")])?,
53 Body::Empty,
54 ),
55 _ => unreachable!("checked OAuth provider"),
56 };
57 let value = json(self.provider, self.send(method, &url, &[], body, true).await?).await?;
58 let account = match self.provider {
59 CloudProvider::Dropbox => string(&value, "email")?,
60 CloudProvider::OneDrive => {
61 if value["mail"].is_null() {
62 string(&value, "userPrincipalName")?
63 } else {
64 string(&value, "mail")?
65 }
66 }
67 _ => unreachable!("checked OAuth provider"),
68 };
69 let account = coven_format::MemberAccess::normalized_account(account);
70 if account.is_empty() {
71 return Err(StorageFailure::Protocol.with_source("empty signed-in account"));
72 }
73 Ok(account)
74 }
75 pub async fn set_tokens(&self, tokens: OAuthTokens) {
77 self.tokens.install(tokens).await;
78 }
79 pub(crate) fn provider(&self) -> CloudProvider {
80 self.provider
81 }
82 pub(crate) async fn deadline(&self, duration: std::time::Duration) {
84 self.clock.deadline(duration).await;
85 }
86 pub(crate) async fn wait_out(&self, wait: std::time::Duration) {
88 self.clock.wait_out(wait).await;
89 }
90 pub(crate) async fn send(
91 &self,
92 method: Method,
93 url: &str,
94 headers: &[(&str, String)],
95 body: Body,
96 authenticated: bool,
97 ) -> Result<Response, StorageError> {
98 self.send_request(None, method, url, headers, body, authenticated).await
99 }
100 pub(crate) async fn publish(
101 &self,
102 publication: &crate::PublicationRequest,
103 method: Method,
104 url: &str,
105 headers: &[(&str, String)],
106 body: Body,
107 authenticated: bool,
108 ) -> Result<Response, StorageError> {
109 self.send_request(Some(publication), method, url, headers, body, authenticated).await
110 }
111 async fn send_request(
112 &self,
113 publication: Option<&crate::PublicationRequest>,
114 method: Method,
115 url: &str,
116 headers: &[(&str, String)],
117 body: Body,
118 authenticated: bool,
119 ) -> Result<Response, StorageError> {
120 let mut request = self.client.request(method, url);
121 for (name, value) in headers {
122 request = request.header(*name, value);
123 }
124 let (request, sent) = match body {
125 Body::Empty => (request, ByteProgress::unread()),
126 Body::Json(value) => (request.json(&value), ByteProgress::unread()),
127 Body::Bytes(bytes, sent) => (request.body(bytes), sent),
128 };
129 let request = request.build().map_err(|e| transport(self.provider, e))?;
130 if !authenticated {
131 return self.execute(publication, request, None, &sent).await;
132 }
133 let retry = request.try_clone();
136 let used = self.tokens.current().await;
137 let response = self.execute(publication, request, Some(&used), &sent).await?;
138 if response.status() != reqwest::StatusCode::UNAUTHORIZED {
139 return Ok(response);
140 }
141 let rejection = response_error(self.provider, response).await;
142 if rejection.failure() != StorageFailure::Authentication {
145 return Err(rejection);
146 }
147 let Some(retry) = retry else {
148 return Err(rejection);
149 };
150 let replacement = self.tokens.rejected(&used).await?;
151 self.execute(publication, retry, Some(&replacement), &sent).await
152 }
153 async fn execute(
157 &self,
158 publication: Option<&crate::PublicationRequest>,
159 mut request: reqwest::Request,
160 tokens: Option<&OAuthTokens>,
161 sent: &ByteProgress,
162 ) -> Result<Response, StorageError> {
163 if let Some(tokens) = tokens {
164 let mut value = reqwest::header::HeaderValue::try_from(format!(
165 "Bearer {}",
166 tokens.access_token.as_str()
167 ))
168 .map_err(|error| StorageFailure::InvalidConfiguration.with_source(error))?;
169 value.set_sensitive(true);
170 request.headers_mut().insert(reqwest::header::AUTHORIZATION, value);
171 }
172 if let Some(publication) = publication {
173 publication.check()?;
174 }
175 let mut response = self
176 .client
177 .execute(request, sent)
178 .await
179 .map_err(|error| error.into_storage(self.provider))?;
180 if let Some(tokens) = tokens {
181 response.extensions_mut().insert(tokens.access);
182 }
183 Ok(response)
184 }
185}
186pub(crate) enum Body {
187 Empty,
188 Json(Value),
189 Bytes(Vec<u8>, ByteProgress),
192}
193pub(crate) fn transport(provider: CloudProvider, error: reqwest::Error) -> StorageError {
198 let failure = if error.is_builder() {
199 StorageFailure::InvalidConfiguration
200 } else {
201 StorageFailure::Network
202 };
203 StorageError::Provider {
204 provider,
205 failure,
206 source: Box::new(super::error_text::WithCauses(super::error_text::origin_only(error))),
207 }
208}
209
210pub struct ProviderResponse {
212 status: u16,
213 headers: reqwest::header::HeaderMap,
214 body: SecretBytes,
215}
216impl ProviderResponse {
217 pub fn status(&self) -> u16 {
219 self.status
220 }
221 pub fn headers(&self) -> &reqwest::header::HeaderMap {
224 &self.headers
225 }
226 pub(crate) fn new(status: u16, headers: reqwest::header::HeaderMap, body: Vec<u8>) -> Self {
228 Self { status, headers, body: SecretBytes::new(body) }
229 }
230 #[cfg(any(test, feature = "test-utils"))]
234 pub(crate) fn injected(
235 provider: CloudProvider,
236 failure: StorageFailure,
237 status: u16,
238 retry_after: Option<u64>,
239 ) -> StorageError {
240 let mut headers = reqwest::header::HeaderMap::new();
241 if let Some(seconds) = retry_after {
242 headers.insert(reqwest::header::RETRY_AFTER, seconds.into());
243 }
244 StorageError::Provider {
245 provider,
246 failure,
247 source: Box::new(Self::new(status, headers, Vec::new())),
248 }
249 }
250 pub(crate) fn retry_after(&self, provider: CloudProvider) -> Option<crate::RetryAfter> {
254 use reqwest::header::{DATE, RETRY_AFTER};
255 if let Some(value) = self.headers.get(RETRY_AFTER) {
256 return Some(retry_after_header(
257 value.to_str().unwrap_or_default(),
258 self.headers.get(DATE).and_then(|date| date.to_str().ok()),
259 ));
260 }
261 if provider == CloudProvider::Dropbox {
262 let value = self.json()?;
263 return value["error"]["retry_after"]
264 .as_u64()
265 .map(|seconds| crate::RetryAfter::Wait(std::time::Duration::from_secs(seconds)));
266 }
267 None
268 }
269 pub(crate) fn into_error(self, provider: CloudProvider) -> StorageError {
270 StorageError::Provider {
271 provider,
272 failure: classify(provider, self.status, self.body()),
273 source: Box::new(self),
274 }
275 }
276 pub fn body(&self) -> &[u8] {
278 self.body.as_bytes()
279 }
280 pub(crate) fn json(&self) -> Option<Value> {
282 serde_json::from_slice(self.body()).ok()
283 }
284}
285
286pub(crate) fn provider_response(error: &StorageError) -> Option<&ProviderResponse> {
288 let StorageError::Provider { source, .. } = error else {
289 return None;
290 };
291 source.downcast_ref::<ProviderResponse>()
292}
293
294pub(crate) fn provider_json(error: &StorageError) -> Option<Value> {
296 provider_response(error)?.json()
297}
298
299pub(crate) fn graph_errors(error: &Value) -> impl Iterator<Item = &Value> {
302 std::iter::successors(Some(error).filter(|error| error.is_object()), |error| {
303 error
304 .get("innerError")
305 .or_else(|| error.get("innererror"))
306 .filter(|inner| inner.is_object())
307 })
308}
309pub(crate) fn http_date(value: &str) -> Option<aws_sdk_s3::primitives::DateTime> {
312 use aws_sdk_s3::primitives::{DateTime, DateTimeFormat};
313 DateTime::from_str(value.trim(), DateTimeFormat::HttpDate).ok()
314}
315pub(crate) fn retry_after_header(value: &str, date: Option<&str>) -> crate::RetryAfter {
317 use crate::RetryAfter;
318 use std::time::Duration;
319 if let Ok(seconds) = value.trim().parse::<u64>() {
320 return RetryAfter::Wait(Duration::from_secs(seconds));
321 }
322 let parse = |value: &str| http_date(value).map(|time| time.secs());
323 match (parse(value), date.and_then(parse)) {
324 (Some(retry), Some(basis)) => {
325 RetryAfter::Wait(Duration::from_secs(retry.saturating_sub(basis).max(0) as u64))
326 }
327 _ => RetryAfter::NoTimeBasis,
328 }
329}
330
331impl ProviderResponse {
332 fn codes(&self) -> Vec<String> {
339 let Some(value) = self.json() else {
340 return Vec::new();
341 };
342 let mut codes = Vec::new();
343 let mut push = |code: Option<String>| {
344 if let Some(code) = code.filter(|code| !codes.contains(code)) {
345 codes.push(code);
346 }
347 };
348 push(dropbox_tags(&value));
350 let error = &value["error"];
351 push(error.as_str().and_then(fixed_word));
352 push(dropbox_tags(error));
353 for inner in graph_errors(error) {
354 push(inner["code"].as_str().and_then(fixed_word));
355 }
356 push(error["status"].as_str().and_then(fixed_word));
357 for list in [&error["errors"], &error["details"]] {
358 for item in list.as_array().into_iter().flatten() {
359 push(item["reason"].as_str().and_then(fixed_word));
360 }
361 }
362 for code in value["error_codes"].as_array().into_iter().flatten() {
363 push(code.as_u64().map(|code| code.to_string()));
364 }
365 for item in value["errors"].as_array().into_iter().flatten() {
366 push(item["code"].as_u64().map(|code| code.to_string()));
367 }
368 codes
369 }
370}
371pub(super) fn fixed_word(word: &str) -> Option<String> {
374 let fixed = (1..=64).contains(&word.len())
375 && word.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-' | '/'));
376 fixed.then(|| word.to_owned())
377}
378fn dropbox_tags(value: &Value) -> Option<String> {
380 let mut tags = Vec::new();
381 let mut union = value;
382 while let Some(tag) = union.get(".tag").and_then(Value::as_str).and_then(fixed_word) {
383 union = &union[tag.as_str()];
384 tags.push(tag);
385 }
386 (!tags.is_empty()).then(|| tags.join("/"))
387}
388impl fmt::Debug for ProviderResponse {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.debug_struct("ProviderResponse")
391 .field("status", &self.status)
392 .field("codes", &self.codes())
393 .finish_non_exhaustive()
394 }
395}
396impl fmt::Display for ProviderResponse {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 write!(f, "provider returned HTTP {}", self.status)?;
401 let codes = self.codes();
402 if !codes.is_empty() {
403 write!(f, " ({})", codes.join(", "))?;
404 }
405 Ok(())
406 }
407}
408impl std::error::Error for ProviderResponse {}
409
410pub(crate) fn invalid_response(
411 provider: CloudProvider,
412 status: u16,
413 headers: reqwest::header::HeaderMap,
414 body: Vec<u8>,
415) -> StorageError {
416 StorageError::Provider {
417 provider,
418 failure: StorageFailure::Protocol,
419 source: Box::new(ProviderResponse::new(status, headers, body)),
420 }
421}
422
423pub(crate) async fn checked(
424 provider: CloudProvider,
425 response: Response,
426) -> Result<Response, StorageError> {
427 if response.status().is_success() {
428 return Ok(response);
429 }
430 Err(response_error(provider, response).await)
431}
432pub(crate) async fn response_error(provider: CloudProvider, response: Response) -> StorageError {
433 let status = response.status().as_u16();
434 let headers = response.headers().clone();
435 let body = match response.bytes().await {
436 Ok(body) => body.to_vec(),
437 Err(error) => return transport(provider, error),
438 };
439 ProviderResponse::new(status, headers, body).into_error(provider)
440}
441pub(crate) fn container_error(mut error: StorageError) -> StorageError {
442 match &mut error {
443 StorageError::Provider { failure, .. } if *failure == StorageFailure::NotFound => {
444 *failure = StorageFailure::ContainerNotFound;
445 }
446 _ => {}
447 }
448 error
449}
450pub(crate) fn classify(provider: CloudProvider, status: u16, body: &[u8]) -> StorageFailure {
451 let parsed = match serde_json::from_slice::<Value>(body) {
452 Ok(value) => Some(value),
453 Err(error) => {
454 tracing::debug!(%error, "provider error response is not JSON");
455 None
456 }
457 };
458 if let Some(failure) = parsed
459 .as_ref()
460 .and_then(|value| value["error"].as_str())
461 .and_then(super::oauth::classify_error)
462 {
463 return failure;
464 }
465 let native = parsed.as_ref().and_then(|value| match provider {
466 CloudProvider::Dropbox => dropbox_failure(value),
467 CloudProvider::OneDrive => onedrive_failure(value),
468 _ => None,
469 });
470 if let Some(failure) = native {
471 return failure;
472 }
473 match status {
474 401 => StorageFailure::Authentication,
475 403 => StorageFailure::PermissionDenied,
476 404 => StorageFailure::NotFound,
477 409 if provider == CloudProvider::Dropbox => StorageFailure::Refused,
478 409 | 412 => StorageFailure::AlreadyExists,
479 416 => StorageFailure::InvalidRange,
480 423 => StorageFailure::Conflict,
482 429 => StorageFailure::RateLimited,
483 507 => StorageFailure::QuotaExceeded,
484 408 | 500..=599 => StorageFailure::Network,
485 _ => StorageFailure::Refused,
486 }
487}
488fn onedrive_failure(value: &Value) -> Option<StorageFailure> {
489 Some(match value["error"]["code"].as_str()? {
490 "quotaLimitReached" => StorageFailure::QuotaExceeded,
491 "InvalidAuthenticationToken" => StorageFailure::Authentication,
492 "accessDenied" => StorageFailure::PermissionDenied,
493 "itemNotFound" => StorageFailure::NotFound,
494 "nameAlreadyExists" => StorageFailure::AlreadyExists,
495 "activityLimitReached" => StorageFailure::RateLimited,
496 "resourceLocked" => StorageFailure::Conflict,
497 _ => return None,
498 })
499}
500fn sharing_refusal(names: &[&str]) -> Option<coven_foundation::sharing::SharingRefusal> {
504 use coven_foundation::sharing::SharingRefusal as Refusal;
505 let named = |wanted: &[&str]| names.iter().any(|name| wanted.contains(name));
506 if named(&["invalid_email"]) {
507 Some(Refusal::InvalidAddress)
508 } else if named(&["email_unverified"]) {
509 Some(Refusal::EmailUnverified)
510 } else if named(&["cant_share_outside_team"]) {
511 Some(Refusal::TeamSharingOff)
512 } else if named(&["insufficient_plan"]) {
513 Some(Refusal::PlanWithoutSharing)
514 } else if named(&["too_many_members", "too_many_pending_invites"]) {
515 Some(Refusal::TooManyMembers)
516 } else {
517 None
518 }
519}
520fn dropbox_failure(value: &Value) -> Option<StorageFailure> {
521 fn tags<'a>(value: &'a Value, result: &mut Vec<&'a str>) {
522 if let Some(object) = value.as_object() {
523 if let Some(tag) = object.get(".tag").and_then(Value::as_str) {
524 result.push(tag);
525 }
526 for child in object.values().filter(|child| child.is_object()) {
527 tags(child, result);
528 }
529 }
530 }
531 let mut names = Vec::new();
532 if let Some(summary) = value["error_summary"].as_str() {
533 names.extend(summary.trim_end_matches('.').split('/'));
534 }
535 let transport_limit = names.starts_with(&["invalid_account_type", "feature"])
538 || (value["error"][".tag"].as_str() == Some("invalid_account_type")
539 && value["error"]["invalid_account_type"][".tag"].as_str() == Some("feature"));
540 tags(&value["error"], &mut names);
541 tags(&value["failed"], &mut names);
542 if names.iter().any(|name| {
544 matches!(*name, "too_many_requests" | "too_many_write_operations" | "rate_limit")
545 }) {
546 Some(StorageFailure::RateLimited)
547 } else if let Some(refusal) = sharing_refusal(&names) {
548 Some(StorageFailure::SharingRefused(refusal))
549 } else if names.contains(&"missing_scope") {
550 Some(StorageFailure::InvalidConfiguration)
552 } else if names
553 .iter()
554 .any(|name| matches!(*name, "expired_access_token" | "invalid_access_token"))
555 {
556 Some(StorageFailure::Authentication)
557 } else if names.iter().any(|name| {
558 matches!(
559 *name,
560 "no_permission"
561 | "no_write_permission"
562 | "access_denied"
563 | "not_a_member"
564 | "invalid_member"
565 | "user_suspended"
566 )
567 }) {
568 Some(StorageFailure::PermissionDenied)
569 } else if transport_limit
570 || names.iter().any(|name| {
571 matches!(
572 *name,
573 "insufficient_space"
574 | "team_folder_insufficient_space"
575 | "member_folder_insufficient_space"
576 | "upload_traffic_limit_reached"
577 )
578 })
579 {
580 Some(StorageFailure::QuotaExceeded)
581 } else if names.contains(&"conflict") {
582 Some(StorageFailure::AlreadyExists)
583 } else if names.contains(&"invalid_root")
584 || names.contains(&"invalid_namespace_id")
585 || names.contains(&"invalid_id")
586 {
587 Some(StorageFailure::ContainerNotFound)
588 } else if names.contains(&"not_found") || names.contains(&"not_folder") {
589 Some(StorageFailure::NotFound)
592 } else {
593 None
594 }
595}
596pub(crate) async fn json(
597 provider: CloudProvider,
598 response: Response,
599) -> Result<Value, StorageError> {
600 Ok(json_response(provider, response).await?.0)
601}
602#[expect(
603 clippy::map_err_ignore,
604 reason = "the response is kept as the cause; the parse error adds nothing to it"
605)]
606pub(crate) async fn json_response(
607 provider: CloudProvider,
608 response: Response,
609) -> Result<(Value, ProviderResponse), StorageError> {
610 let response = checked(provider, response).await?;
611 let status = response.status().as_u16();
612 let headers = response.headers().clone();
613 let body = response.bytes().await.map_err(|error| transport(provider, error))?;
614 let value = serde_json::from_slice(&body)
615 .map_err(|_| invalid_response(provider, status, headers.clone(), body.to_vec()))?;
616 Ok((value, ProviderResponse::new(status, headers, body.to_vec())))
617}
618pub(crate) fn string<'a>(value: &'a Value, field: &str) -> Result<&'a str, StorageError> {
619 value[field]
620 .as_str()
621 .filter(|s| !s.is_empty())
622 .ok_or(StorageFailure::Protocol.with_source("missing string field"))
623}
624pub(crate) fn page_cursor(value: &Value) -> Result<Option<&str>, StorageError> {
628 match optional_string(value, "cursor")? {
629 Some("") => Err(StorageFailure::Protocol.with_source("empty Dropbox page cursor")),
630 cursor => Ok(cursor),
631 }
632}
633pub(crate) fn next_link(value: &Value) -> Result<Option<&str>, StorageError> {
636 match value.get("@odata.nextLink") {
637 None => Ok(None),
638 Some(_) => string(value, "@odata.nextLink").map(Some),
639 }
640}
641pub(crate) fn optional_string<'a>(
643 value: &'a Value,
644 field: &str,
645) -> Result<Option<&'a str>, StorageError> {
646 match value.get(field) {
647 None | Some(Value::Null) => Ok(None),
648 Some(Value::String(value)) => Ok(Some(value)),
649 Some(_) => Err(StorageFailure::Protocol.with_source("non-string optional field")),
650 }
651}
652pub(crate) fn timestamp(
653 value: &Value,
654 field: &str,
655) -> Result<coven_format::value::StorageTime, StorageError> {
656 use aws_sdk_s3::primitives::{DateTime, DateTimeFormat};
657 let timestamp = DateTime::from_str(string(value, field)?, DateTimeFormat::DateTimeWithOffset)
658 .map_err(|error| StorageFailure::Protocol.with_source(error))?;
659 storage_time(timestamp)
660}
661#[expect(
664 clippy::map_err_ignore,
665 reason = "a time before the epoch is the protocol failure this names"
666)]
667pub(crate) fn storage_time(
668 time: aws_sdk_s3::primitives::DateTime,
669) -> Result<coven_format::value::StorageTime, StorageError> {
670 let seconds = u64::try_from(time.secs())
671 .map_err(|_| StorageFailure::Protocol.with_source("provider time precedes the epoch"))?;
672 coven_format::value::StorageTime::new(seconds, time.subsec_nanos())
673 .map_err(|error| StorageFailure::Protocol.with_source(error))
674}
675pub(crate) fn array<'a>(value: &'a Value, field: &str) -> Result<&'a Vec<Value>, StorageError> {
676 value[field].as_array().ok_or(StorageFailure::Protocol.with_source("missing array field"))
677}
678pub(crate) fn endpoint(
679 base: &str,
680 segments: &[&str],
681 query: &[(&str, &str)],
682) -> Result<String, StorageError> {
683 let mut url = url::Url::parse(base)
684 .map_err(|error| StorageFailure::InvalidConfiguration.with_source(error))?;
685 url.path_segments_mut()
686 .map_err(|()| {
687 StorageFailure::InvalidConfiguration.with_source("provider endpoint cannot hold paths")
688 })?
689 .pop_if_empty()
690 .extend(segments);
691 if !query.is_empty() {
692 url.query_pairs_mut().extend_pairs(query.iter().copied());
693 }
694 Ok(url.into())
695}
696pub(crate) fn same_origin(base: &str, target: &str) -> Result<(), StorageError> {
697 let base = url::Url::parse(base)
698 .map_err(|error| StorageFailure::InvalidConfiguration.with_source(error))?;
699 let target =
700 url::Url::parse(target).map_err(|error| StorageFailure::Protocol.with_source(error))?;
701 if target.origin() != base.origin()
702 || !target.username().is_empty()
703 || target.password().is_some()
704 {
705 return Err(StorageFailure::Protocol.with_source("provider URL crossed origins"));
706 }
707 Ok(())
708}
709pub(crate) fn validate_content_range(header: &str, range: ByteRange) -> Result<u64, StorageError> {
710 let (bounds, total) = header
711 .strip_prefix("bytes ")
712 .and_then(|h| h.split_once('/'))
713 .ok_or(StorageFailure::Protocol.with_source("invalid Content-Range"))?;
714 let total: u64 = total.parse().map_err(|error| StorageFailure::Protocol.with_source(error))?;
715 if range.start() < total
716 && range.end() > total
717 && bounds == format!("{}-{}", range.start(), total - 1)
718 {
719 return Err(StorageFailure::InvalidRange.into());
720 }
721 if bounds != format!("{}-{}", range.start(), range.end() - 1) || range.end() > total {
722 return Err(StorageFailure::Protocol.with_source("Content-Range disagrees with request"));
723 }
724 Ok(total)
725}
726
727pub(crate) async fn verify_published_part(
728 storage: &dyn crate::ProviderOps,
729 session: &mut crate::UploadSession,
730 bytes: &[u8],
731 end: u64,
732) -> Result<(), StorageError> {
733 compare_published_part(storage, session, bytes, end).await?;
734 session.confirmed = end;
735 if end == session.total {
736 session.state = crate::session::SessionState::Complete;
737 }
738 Ok(())
739}
740
741pub(crate) async fn compare_published_part(
742 storage: &dyn crate::ProviderOps,
743 session: &crate::UploadSession,
744 bytes: &[u8],
745 end: u64,
746) -> Result<(), StorageError> {
747 let range = ByteRange::new(session.confirmed, end)?;
748 let actual = storage.read_object(session.path(), Some(range)).await?;
749 actual.check(session.path(), Some(range))?;
750 if actual.metadata.size != session.total || actual.bytes != bytes {
751 return Err(StorageFailure::AlreadyExists.into());
752 }
753 Ok(())
754}
755
756#[cfg(test)]
757#[path = "http_tests.rs"]
758mod tests;