1use crate::{CloudProvider, ObjectPath, StorageConfig, StorageError, StorageFailure, StoredObject};
2use coven_crypto::SecretBytes;
3use coven_crypto::SecretText;
4use coven_foundation::id_source::DeviceId;
5use serde::{Deserialize, Serialize};
6use std::ops::Range;
7
8pub(crate) const CLOUDKIT_PART_SIZE: usize = 8 * 1024 * 1024;
9pub(crate) const DROPBOX_PART_SIZE: usize = 8 * 1024 * 1024;
10pub(crate) const ONEDRIVE_PART_SIZE: usize = 24 * 320 * 1024;
11pub(crate) const S3_MAX_PARTS: usize = 10_000;
13pub(crate) const S3_REQUEST_LIMIT: u64 = 5 * 1024 * 1024 * 1024;
16const S3_PART_UNIT: u64 = 8 * 1024 * 1024;
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub enum UploadTarget {
22 Create(ObjectPath),
24 Replace(StoredObject),
26}
27impl UploadTarget {
28 pub fn path(&self) -> &ObjectPath {
30 match self {
31 Self::Create(path) => path,
32 Self::Replace(previous) => &previous.path,
33 }
34 }
35 pub(crate) fn check(&self) -> Result<(), StorageError> {
36 if let Self::Replace(previous) = self {
37 if !previous.path.is_replaceable() {
38 return Err(StorageFailure::InvalidPath.into());
39 }
40 previous.check(&previous.path)?;
41 }
42 Ok(())
43 }
44 pub(crate) fn error(&self, error: StorageError) -> StorageError {
45 match self {
46 Self::Create(_) => error,
47 Self::Replace(_) => error.replacement(),
48 }
49 }
50}
51
52#[derive(Clone, Serialize, Deserialize)]
76#[serde(try_from = "RecordedUploadSession")]
77pub struct UploadSession {
78 pub(crate) device: DeviceId,
81 pub(crate) location: StorageConfig,
82 pub(crate) target: UploadTarget,
83 pub(crate) total: u64,
84 pub(crate) confirmed: u64,
85 pub(crate) part_size: usize,
86 pub(crate) state: SessionState,
87}
88
89#[derive(Deserialize)]
92#[serde(deny_unknown_fields)]
93struct RecordedUploadSession {
94 device: DeviceId,
95 location: StorageConfig,
96 target: UploadTarget,
97 total: u64,
98 confirmed: u64,
99 part_size: usize,
100 state: SessionState,
101}
102
103impl TryFrom<RecordedUploadSession> for UploadSession {
104 type Error = StorageError;
105 fn try_from(recorded: RecordedUploadSession) -> Result<Self, Self::Error> {
106 let session = Self {
107 device: recorded.device,
108 location: recorded.location,
109 target: recorded.target,
110 total: recorded.total,
111 confirmed: recorded.confirmed,
112 part_size: recorded.part_size,
113 state: recorded.state,
114 };
115 session.validate()?;
116 Ok(session)
117 }
118}
119
120#[derive(Clone, Serialize, Deserialize)]
121#[serde(deny_unknown_fields)]
122pub(crate) enum SessionState {
123 S3 {
124 id: SecretText,
125 token: SecretText,
126 parts: Vec<S3Part>,
127 },
128 S3Restart {
129 id: SecretText,
130 token: SecretText,
131 },
132 S3Planned {
136 token: SecretText,
137 started: PlanStart,
138 },
139 Dropbox {
140 id: SecretText,
141 },
142 OneDrive {
143 temporary: uuid::Uuid,
144 upload: OneDriveUpload,
145 },
146 OneDriveReplacement {
147 url: SecretText,
148 },
149 CloudKit {
150 id: SecretText,
151 },
152 VerifyPublished,
153 Complete,
154 #[cfg(any(test, feature = "test-utils"))]
157 MemoryPlanned {
158 started: PlanStart,
159 },
160 #[cfg(any(test, feature = "test-utils"))]
161 Memory {
162 id: u64,
163 complete: bool,
164 },
165 #[cfg(any(test, feature = "test-utils"))]
169 MemoryParts {
170 id: u64,
171 parts: Vec<u64>,
172 complete: bool,
173 },
174}
175
176#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(from = "bool", into = "bool")]
181pub(crate) enum PlanStart {
182 Unstarted,
185 Starting,
188 MayHaveStarted,
191}
192
193impl From<bool> for PlanStart {
194 fn from(marked: bool) -> Self {
195 if marked {
196 Self::MayHaveStarted
197 } else {
198 Self::Unstarted
199 }
200 }
201}
202
203impl From<PlanStart> for bool {
204 fn from(start: PlanStart) -> Self {
205 start != PlanStart::Unstarted
206 }
207}
208
209#[derive(Clone, Default)]
215pub(crate) struct StartSent(std::sync::Arc<std::sync::atomic::AtomicBool>);
216
217impl StartSent {
218 pub(crate) fn going(&self) {
220 self.0.store(true, std::sync::atomic::Ordering::SeqCst);
221 }
222 fn went(&self) -> bool {
223 self.0.load(std::sync::atomic::Ordering::SeqCst)
224 }
225}
226
227#[derive(Clone, Serialize, Deserialize)]
228#[serde(deny_unknown_fields)]
229pub(crate) struct S3Part {
230 pub(crate) number: i32,
231 pub(crate) size: u64,
232 pub(crate) etag: String,
233}
234
235#[derive(Clone, Serialize, Deserialize)]
238#[serde(deny_unknown_fields)]
239pub(crate) enum OneDriveUpload {
240 Planned {
242 #[serde(skip)]
244 unstarted: bool,
245 },
246 Receiving {
247 url: SecretText,
248 },
249 Uploaded {
250 item: String,
251 },
252 Copying {
253 monitor: SecretText,
254 },
255 Refused,
257 VerifyPublished,
258 Published,
259}
260
261impl S3Part {
262 pub(crate) fn fits(&self, total: u64, part_size: usize) -> bool {
266 u64::try_from(self.number)
267 .ok()
268 .and_then(|number| number.checked_sub(1))
269 .and_then(|index| part_span(total, part_size, index))
270 .is_some_and(|span| span.end - span.start == self.size)
271 && !self.etag.is_empty()
272 }
273}
274
275pub(crate) fn part_span(total: u64, part_size: usize, index: u64) -> Option<Range<u64>> {
278 let start = index.checked_mul(part_size as u64)?;
279 (start < total).then(|| start..start.saturating_add(part_size as u64).min(total))
280}
281
282pub struct ConfirmedPart(pub(crate) Confirmation);
285
286impl std::fmt::Debug for ConfirmedPart {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 let index = match &self.0 {
289 Confirmation::S3(part) => i64::from(part.number) - 1,
290 #[cfg(any(test, feature = "test-utils"))]
291 Confirmation::Memory { index } => *index as i64,
292 };
293 f.debug_struct("ConfirmedPart").field("index", &index).finish()
294 }
295}
296
297pub(crate) enum Confirmation {
298 S3(S3Part),
299 #[cfg(any(test, feature = "test-utils"))]
301 Memory {
302 index: u64,
303 },
304}
305
306impl UploadSession {
307 pub(crate) fn mark_start(&mut self) -> bool {
311 let started = match &mut self.state {
312 SessionState::S3Planned { started, .. } => started,
313 #[cfg(any(test, feature = "test-utils"))]
314 SessionState::MemoryPlanned { started } => started,
315 _ => return false,
316 };
317 if *started != PlanStart::Unstarted {
318 return false;
319 }
320 *started = PlanStart::Starting;
321 true
322 }
323 pub(crate) fn start_failed(&mut self, start: &StartSent) {
329 let started = match &mut self.state {
330 SessionState::S3Planned { started, .. } => started,
331 #[cfg(any(test, feature = "test-utils"))]
332 SessionState::MemoryPlanned { started } => started,
333 _ => return,
334 };
335 if !start.went() {
336 *started = PlanStart::Unstarted;
337 }
338 }
339 pub fn needs_initiation(&self) -> bool {
342 matches!(self.state, SessionState::OneDrive { upload: OneDriveUpload::Planned { .. }, .. })
343 }
344 pub fn device(&self) -> DeviceId {
346 self.device
347 }
348 pub fn path(&self) -> &ObjectPath {
350 self.target.path()
351 }
352 pub fn target(&self) -> &UploadTarget {
354 &self.target
355 }
356 pub fn confirmed_bytes(&self) -> u64 {
358 self.confirmed
359 }
360 pub fn total_bytes(&self) -> u64 {
362 self.total
363 }
364 pub fn part_size(&self) -> usize {
367 self.part_size
368 }
369 pub fn is_complete(&self) -> bool {
371 match self.state {
372 SessionState::Complete => true,
373 #[cfg(any(test, feature = "test-utils"))]
374 SessionState::Memory { complete, .. } | SessionState::MemoryParts { complete, .. } => {
375 complete
376 }
377 _ => false,
378 }
379 }
380 pub fn parts_independent(&self) -> bool {
385 match self.state {
386 SessionState::S3 { .. } => true,
387 #[cfg(any(test, feature = "test-utils"))]
388 SessionState::MemoryParts { complete, .. } => !complete,
389 _ => false,
390 }
391 }
392 pub fn parts_to_send(&self) -> Vec<Range<u64>> {
397 if self.is_complete() || self.confirmed >= self.total {
398 return Vec::new();
399 }
400 if let Some(confirmed) = self.confirmed_parts() {
401 return (0..self.total.div_ceil(self.part_size as u64))
402 .filter(|index| confirmed.binary_search(index).is_err())
403 .filter_map(|index| part_span(self.total, self.part_size, index))
404 .collect();
405 }
406 let next = (self.confirmed / self.part_size as u64 + 1) * self.part_size as u64;
407 std::iter::once(self.confirmed..next.min(self.total)).collect()
408 }
409 fn confirmed_parts(&self) -> Option<Vec<u64>> {
412 match &self.state {
413 SessionState::S3 { parts, .. } => Some(
414 parts
415 .iter()
416 .filter_map(|part| u64::try_from(part.number).ok()?.checked_sub(1))
417 .collect(),
418 ),
419 #[cfg(any(test, feature = "test-utils"))]
420 SessionState::MemoryParts { parts, complete: false, .. } => Some(parts.clone()),
421 _ => None,
422 }
423 }
424 pub fn confirm(&mut self, part: ConfirmedPart) -> Result<(), StorageError> {
428 let (total, part_size) = (self.total, self.part_size);
429 let size = match (&mut self.state, part.0) {
430 (SessionState::S3 { parts, .. }, Confirmation::S3(part)) => {
431 if !part.fits(total, part_size) {
432 return Err(StorageFailure::Protocol
433 .with_source("S3 confirmed a part the session doesn't have"));
434 }
435 let at = parts
436 .binary_search_by_key(&part.number, |held| held.number)
437 .err()
438 .ok_or(StorageFailure::InvalidPart)?;
439 let size = part.size;
440 parts.insert(at, part);
441 size
442 }
443 #[cfg(any(test, feature = "test-utils"))]
444 (
445 SessionState::MemoryParts { parts, complete: false, .. },
446 Confirmation::Memory { index },
447 ) => {
448 let span = part_span(total, part_size, index).ok_or(StorageFailure::Protocol)?;
449 let at = parts.binary_search(&index).err().ok_or(StorageFailure::InvalidPart)?;
450 parts.insert(at, index);
451 span.end - span.start
452 }
453 _ => return Err(StorageFailure::SessionMismatch.into()),
454 };
455 self.confirmed += size;
456 Ok(())
457 }
458 pub fn encode(&self) -> Result<SecretBytes, StorageError> {
460 self.validate()?;
461 crate::secret_json::encode(self)
462 }
463 pub fn decode(bytes: &[u8]) -> Result<Self, StorageError> {
468 let value: RecordedUploadSession = crate::secret_json::decode(bytes)?;
469 value.try_into()
470 }
471 fn validate(&self) -> Result<(), StorageError> {
472 self.location.validate()?;
473 self.target.check()?;
474 if self.part_size == 0
475 || self.total == 0
476 || self.confirmed > self.total
477 || (self.is_complete() && self.confirmed != self.total)
478 {
479 return Err(StorageFailure::InvalidPart.into());
480 }
481 let provider = self.location.provider();
482 match &self.state {
483 SessionState::S3 { id, token, parts } if provider == CloudProvider::S3 => {
484 nonempty(id)?;
485 nonempty(token)?;
486 if parts.len() > S3_MAX_PARTS {
487 return Err(StorageFailure::InvalidPart.into());
488 }
489 let mut confirmed = 0u64;
492 let mut previous = 0;
493 for part in parts {
494 if part.number <= previous || !part.fits(self.total, self.part_size) {
495 return Err(StorageFailure::InvalidPart.into());
496 }
497 previous = part.number;
498 confirmed += part.size;
499 }
500 if confirmed != self.confirmed {
501 return Err(StorageFailure::InvalidPart.into());
502 }
503 }
504 SessionState::S3Restart { id, token } if provider == CloudProvider::S3 => {
505 nonempty(id)?;
506 nonempty(token)?;
507 if self.confirmed != 0 {
508 return Err(StorageFailure::InvalidPart.into());
509 }
510 }
511 SessionState::S3Planned { token, .. } if provider == CloudProvider::S3 => {
512 nonempty(token)?;
513 if self.confirmed != 0 {
514 return Err(StorageFailure::InvalidPart.into());
515 }
516 }
517 SessionState::Dropbox { id } if provider == CloudProvider::Dropbox => nonempty(id)?,
518 SessionState::OneDrive { upload, .. }
519 if provider == CloudProvider::OneDrive
520 && matches!(self.target, UploadTarget::Create(_)) =>
521 {
522 match upload {
523 OneDriveUpload::Planned { .. } => {
524 if self.confirmed != 0 {
525 return Err(StorageFailure::InvalidPart.into());
526 }
527 }
528 OneDriveUpload::Receiving { url } => {
529 transfer_url(url)?;
530 if self.confirmed == self.total {
531 return Err(StorageFailure::InvalidPart.into());
532 }
533 }
534 OneDriveUpload::Uploaded { item } => {
535 if item.is_empty() || self.confirmed != self.total {
536 return Err(StorageFailure::InvalidPart.into());
537 }
538 }
539 OneDriveUpload::Copying { monitor } => {
540 transfer_url(monitor)?;
541 if self.confirmed != self.total {
542 return Err(StorageFailure::InvalidPart.into());
543 }
544 }
545 OneDriveUpload::Published | OneDriveUpload::Refused
546 if self.confirmed != self.total =>
547 {
548 return Err(StorageFailure::InvalidPart.into());
549 }
550 OneDriveUpload::VerifyPublished if self.confirmed == self.total => {
551 return Err(StorageFailure::InvalidPart.into());
552 }
553 OneDriveUpload::VerifyPublished
554 | OneDriveUpload::Published
555 | OneDriveUpload::Refused => {}
556 }
557 }
558 SessionState::OneDriveReplacement { url }
559 if provider == CloudProvider::OneDrive
560 && matches!(self.target, UploadTarget::Replace(_)) =>
561 {
562 transfer_url(url)?;
563 if self.confirmed == self.total {
564 return Err(StorageFailure::InvalidPart.into());
565 }
566 }
567 SessionState::CloudKit { id } if provider == CloudProvider::CloudKit => {
568 check_cloudkit_id(id.as_str())?
569 }
570 SessionState::VerifyPublished
571 if provider == CloudProvider::Dropbox && !self.path().is_replaceable() =>
572 {
573 if self.confirmed == self.total {
574 return Err(StorageFailure::InvalidPart.into());
575 }
576 }
577 SessionState::Complete => {}
578 #[cfg(any(test, feature = "test-utils"))]
579 SessionState::MemoryPlanned { .. } => {
580 return if self.confirmed == 0 {
581 Ok(())
582 } else {
583 Err(StorageFailure::InvalidPart.into())
584 };
585 }
586 #[cfg(any(test, feature = "test-utils"))]
587 SessionState::Memory { id, .. } => {
588 return if *id != 0 { Ok(()) } else { Err(StorageFailure::InvalidPart.into()) };
589 }
590 #[cfg(any(test, feature = "test-utils"))]
591 SessionState::MemoryParts { id, parts, complete } => {
592 let mut confirmed = 0u64;
593 for (at, index) in parts.iter().enumerate() {
594 let span = part_span(self.total, self.part_size, *index)
595 .ok_or(StorageFailure::InvalidPart)?;
596 if at > 0 && parts[at - 1] >= *index {
597 return Err(StorageFailure::InvalidPart.into());
598 }
599 confirmed += span.end - span.start;
600 }
601 let held = if *complete { self.total } else { confirmed };
602 return if *id != 0 && held == self.confirmed {
603 Ok(())
604 } else {
605 Err(StorageFailure::InvalidPart.into())
606 };
607 }
608 _ => return Err(StorageFailure::SessionMismatch.into()),
609 }
610 let valid_size = match provider {
611 CloudProvider::S3 => self.part_size == s3_part_size(self.total)?,
612 CloudProvider::Dropbox => self.part_size == DROPBOX_PART_SIZE,
613 CloudProvider::OneDrive => self.part_size == ONEDRIVE_PART_SIZE,
614 CloudProvider::CloudKit => self.part_size == CLOUDKIT_PART_SIZE,
615 };
616 if !valid_size {
617 return Err(StorageFailure::InvalidPart.into());
618 }
619 Ok(())
620 }
621 pub(crate) fn check(&self, location: &StorageConfig) -> Result<(), StorageError> {
622 self.validate()?;
623 if !self.location.same_location(location) {
624 return Err(StorageFailure::SessionMismatch.into());
625 }
626 Ok(())
627 }
628 pub(crate) fn part_end(&self, offset: u64, len: usize) -> Result<u64, StorageError> {
633 let end = offset.checked_add(len as u64).ok_or(StorageFailure::InvalidPart)?;
634 if self.is_complete() || len == 0 {
635 return Err(StorageFailure::InvalidPart.into());
636 }
637 if let Some(confirmed) = self.confirmed_parts() {
638 let index = offset / self.part_size as u64;
639 let whole = offset.is_multiple_of(self.part_size as u64)
640 && part_span(self.total, self.part_size, index).is_some_and(|span| span.end == end);
641 if !whole || confirmed.binary_search(&index).is_ok() {
642 return Err(StorageFailure::InvalidPart.into());
643 }
644 return Ok(end);
645 }
646 if offset != self.confirmed
647 || len > self.part_size
648 || end > self.total
649 || (end != self.total && end % self.part_size as u64 != 0)
650 {
651 return Err(StorageFailure::InvalidPart.into());
652 }
653 Ok(end)
654 }
655 pub(crate) fn next_part(&self, len: usize) -> Result<Range<u64>, StorageError> {
658 let offset = self
659 .parts_to_send()
660 .first()
661 .map(|part| part.start)
662 .ok_or(StorageFailure::InvalidPart)?;
663 Ok(offset..self.part_end(offset, len)?)
664 }
665}
666
667pub(crate) fn s3_part_size(total: u64) -> Result<usize, StorageError> {
668 let parts = S3_MAX_PARTS as u64;
669 if total == 0 || total > parts * S3_REQUEST_LIMIT {
670 return Err(StorageFailure::InvalidPart.into());
671 }
672 usize::try_from(total.div_ceil(parts).div_ceil(S3_PART_UNIT) * S3_PART_UNIT)
673 .map_err(|error| StorageFailure::InvalidPart.with_source(error))
674}
675pub(crate) fn check_cloudkit_id(value: &str) -> Result<(), StorageError> {
676 coven_foundation::id_source::canonical_uuid(value)
677 .map_err(|error| StorageFailure::SessionMismatch.with_source(error))?;
678 Ok(())
679}
680fn nonempty(id: &SecretText) -> Result<(), StorageError> {
681 if id.as_str().is_empty() {
682 return Err(StorageFailure::SessionMismatch.into());
683 }
684 Ok(())
685}
686fn transfer_url(value: &SecretText) -> Result<(), StorageError> {
687 let url = url::Url::parse(value.as_str())
688 .map_err(|error| StorageFailure::SessionMismatch.with_source(error))?;
689 if !crate::web_url::is_web_url(&url) || url.fragment().is_some() {
690 return Err(StorageFailure::SessionMismatch.into());
691 }
692 Ok(())
693}
694
695#[cfg(test)]
696#[path = "session_tests.rs"]
697mod tests;