1use chrono::{DateTime, Utc};
4use core::fmt;
5use futures::{
6 Stream, StreamExt as _, TryStreamExt as _,
7 future::ready,
8 stream::{self, BoxStream, FuturesOrdered},
9};
10use itertools::Itertools as _;
11use serde::{Deserialize, Serialize};
12use std::{
13 cmp::{max, min},
14 collections::HashSet,
15 ffi::OsString,
16 fmt::Display,
17 iter,
18 num::{NonZeroU16, NonZeroU64},
19 ops::Range,
20 pin::Pin,
21 sync::{Arc, Mutex, OnceLock},
22};
23use tokio::io::AsyncBufRead;
24use tokio_util::io::StreamReader;
25use tracing::{instrument, warn};
26
27use async_trait::async_trait;
28use bytes::Bytes;
29use thiserror::Error;
30
31use crate::ICError;
32use crate::sealed;
33
34#[derive(Debug, Error)]
36pub enum StorageErrorKind {
37 #[error("object not found")]
38 ObjectNotFound,
39 #[error("bad object store prefix {0:?}")]
40 BadPrefix(OsString),
41 #[error("object store error {0}")]
42 ObjectStore(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
43 #[error("I/O error: {0}")]
44 IOError(#[from] std::io::Error),
45 #[error("storage configuration error: {0}")]
46 R2ConfigurationError(String),
47 #[error("error parsing URL: {url:?}")]
48 CannotParseUrl {
49 #[source]
50 cause: url::ParseError,
51 url: String,
52 },
53 #[error("Redirect Storage error: {0}")]
54 BadRedirect(String),
55 #[error("storage error: {0}")]
56 Other(String),
57}
58pub type StorageError = ICError<StorageErrorKind>;
59
60pub type StorageResult<A> = Result<A, StorageError>;
61
62pub fn obj_store_error_res<T>(
63 err: impl std::error::Error + Send + Sync + 'static,
64) -> StorageResult<T> {
65 Err(obj_store_error(err))
66}
67
68pub fn obj_store_error(
69 err: impl std::error::Error + Send + Sync + 'static,
70) -> StorageError {
71 StorageError::capture(StorageErrorKind::ObjectStore(Box::new(err)))
72}
73
74pub fn obj_not_found_res<T>() -> StorageResult<T> {
75 Err(StorageError::capture(StorageErrorKind::ObjectNotFound))
76}
77
78pub fn other_error(s: impl Into<String>) -> StorageError {
79 StorageError::capture(StorageErrorKind::Other(s.into()))
80}
81
82#[derive(Debug)]
83pub struct ListInfo<A> {
84 pub id: A,
85 pub created_at: DateTime<Utc>,
86 pub size_bytes: u64,
87}
88
89pub use icechunk_types::ETag;
90#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default)]
91pub struct Generation(pub String);
92
93#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
94pub struct VersionInfo {
95 pub etag: Option<ETag>,
96 pub generation: Option<Generation>,
97}
98
99impl VersionInfo {
100 pub fn for_creation() -> Self {
101 Self { etag: None, generation: None }
102 }
103
104 pub fn from_etag_only(etag: String) -> Self {
105 Self { etag: Some(ETag(etag)), generation: None }
106 }
107
108 pub fn is_create(&self) -> bool {
109 self.etag.is_none() && self.generation.is_none()
110 }
111
112 pub fn etag(&self) -> Option<&String> {
113 self.etag.as_ref().map(|e| &e.0)
114 }
115
116 pub fn generation(&self) -> Option<&String> {
117 self.generation.as_ref().map(|e| &e.0)
118 }
119}
120
121impl Display for VersionInfo {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 match (&self.etag, &self.generation) {
124 (Some(etag), Some(generation)) => {
125 write!(f, "etag={}, generation={}", etag.0, generation.0)
126 }
127 (Some(etag), None) => write!(f, "etag={}", etag.0),
128 (None, Some(generation)) => write!(f, "generation={}", generation.0),
129 (None, None) => write!(f, "new"),
130 }
131 }
132}
133
134#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Default)]
135pub struct RetriesSettings {
136 pub max_tries: Option<NonZeroU16>,
137 pub initial_backoff_ms: Option<u32>,
138 pub max_backoff_ms: Option<u32>,
139}
140
141impl RetriesSettings {
142 pub fn max_tries(&self) -> NonZeroU16 {
143 self.max_tries.unwrap_or_else(|| NonZeroU16::new(10).unwrap_or(NonZeroU16::MIN))
144 }
145
146 pub fn initial_backoff_ms(&self) -> u32 {
147 self.initial_backoff_ms.unwrap_or(100)
148 }
149
150 pub fn max_backoff_ms(&self) -> u32 {
151 self.max_backoff_ms.unwrap_or(3 * 60 * 1000)
152 }
153
154 pub fn merge(&self, other: Self) -> Self {
155 Self {
156 max_tries: other.max_tries.or(self.max_tries),
157 initial_backoff_ms: other.initial_backoff_ms.or(self.initial_backoff_ms),
158 max_backoff_ms: other.max_backoff_ms.or(self.max_backoff_ms),
159 }
160 }
161}
162
163#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Default)]
164pub struct TimeoutSettings {
165 pub connect_timeout_ms: Option<u32>,
166 pub read_timeout_ms: Option<u32>,
167 pub operation_timeout_ms: Option<u32>,
168 pub operation_attempt_timeout_ms: Option<u32>,
169}
170
171impl TimeoutSettings {
172 pub fn merge(&self, other: Self) -> Self {
173 Self {
174 connect_timeout_ms: other.connect_timeout_ms.or(self.connect_timeout_ms),
175 read_timeout_ms: other.read_timeout_ms.or(self.read_timeout_ms),
176 operation_timeout_ms: other
177 .operation_timeout_ms
178 .or(self.operation_timeout_ms),
179 operation_attempt_timeout_ms: other
180 .operation_attempt_timeout_ms
181 .or(self.operation_attempt_timeout_ms),
182 }
183 }
184}
185
186#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Default)]
187pub struct ConcurrencySettings {
188 pub max_concurrent_requests_for_object: Option<NonZeroU16>,
189 pub ideal_concurrent_request_size: Option<NonZeroU64>,
190}
191
192impl ConcurrencySettings {
193 pub fn max_concurrent_requests_for_object(&self) -> NonZeroU16 {
199 self.max_concurrent_requests_for_object
200 .unwrap_or_else(|| NonZeroU16::new(18).unwrap_or(NonZeroU16::MIN))
201 }
202 pub fn ideal_concurrent_request_size(&self) -> NonZeroU64 {
203 self.ideal_concurrent_request_size.unwrap_or_else(|| {
204 NonZeroU64::new(12 * 1024 * 1024).unwrap_or(NonZeroU64::MIN)
205 })
206 }
207
208 pub fn merge(&self, other: Self) -> Self {
209 Self {
210 max_concurrent_requests_for_object: other
211 .max_concurrent_requests_for_object
212 .or(self.max_concurrent_requests_for_object),
213 ideal_concurrent_request_size: other
214 .ideal_concurrent_request_size
215 .or(self.ideal_concurrent_request_size),
216 }
217 }
218}
219
220#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default)]
222pub struct Settings {
223 #[serde(default)]
224 pub concurrency: Option<ConcurrencySettings>,
225
226 #[serde(default)]
227 pub retries: Option<RetriesSettings>,
228
229 #[serde(default)]
230 pub timeouts: Option<TimeoutSettings>,
231
232 #[serde(default)]
233 pub unsafe_use_conditional_update: Option<bool>,
234
235 #[serde(default)]
236 pub unsafe_use_conditional_create: Option<bool>,
237
238 #[serde(default)]
247 pub unsafe_use_metadata: Option<bool>,
248
249 #[serde(default)]
250 pub storage_class: Option<String>,
251
252 #[serde(default)]
253 pub metadata_storage_class: Option<String>,
254
255 #[serde(default)]
256 pub chunks_storage_class: Option<String>,
257
258 #[serde(default)]
259 pub minimum_size_for_multipart_upload: Option<u64>,
260}
261
262static DEFAULT_CONCURRENCY: OnceLock<ConcurrencySettings> = OnceLock::new();
263static DEFAULT_RETRIES: OnceLock<RetriesSettings> = OnceLock::new();
264
265impl Settings {
266 pub fn concurrency(&self) -> &ConcurrencySettings {
267 self.concurrency
268 .as_ref()
269 .unwrap_or_else(|| DEFAULT_CONCURRENCY.get_or_init(Default::default))
270 }
271
272 pub fn retries(&self) -> &RetriesSettings {
273 self.retries
274 .as_ref()
275 .unwrap_or_else(|| DEFAULT_RETRIES.get_or_init(Default::default))
276 }
277
278 pub fn timeouts(&self) -> Option<&TimeoutSettings> {
279 self.timeouts.as_ref()
280 }
281
282 pub fn unsafe_use_conditional_create(&self) -> bool {
283 self.unsafe_use_conditional_create.unwrap_or(true)
284 }
285
286 pub fn unsafe_use_conditional_update(&self) -> bool {
287 self.unsafe_use_conditional_update.unwrap_or(true)
288 }
289
290 pub fn unsafe_use_metadata(&self) -> bool {
291 self.unsafe_use_metadata.unwrap_or(true)
292 }
293
294 pub fn metadata_storage_class(&self) -> Option<&String> {
295 self.metadata_storage_class.as_ref().or(self.storage_class.as_ref())
296 }
297
298 pub fn storage_class(&self) -> Option<&String> {
299 self.storage_class.as_ref()
300 }
301
302 pub fn chunks_storage_class(&self) -> Option<&String> {
303 self.chunks_storage_class.as_ref().or(self.storage_class.as_ref())
304 }
305
306 pub fn minimum_size_for_multipart_upload(&self) -> u64 {
307 self.minimum_size_for_multipart_upload.unwrap_or(100 * 1024 * 1024)
309 }
310
311 pub fn merge(&self, other: Self) -> Self {
312 Self {
313 concurrency: match (&self.concurrency, other.concurrency) {
314 (None, None) => None,
315 (None, Some(c)) => Some(c),
316 (Some(c), None) => Some(*c),
317 (Some(mine), Some(theirs)) => Some(mine.merge(theirs)),
318 },
319 retries: match (&self.retries, other.retries) {
320 (None, None) => None,
321 (None, Some(c)) => Some(c),
322 (Some(c), None) => Some(*c),
323 (Some(mine), Some(theirs)) => Some(mine.merge(theirs)),
324 },
325 timeouts: match (&self.timeouts, other.timeouts) {
326 (None, None) => None,
327 (None, Some(c)) => Some(c),
328 (Some(c), None) => Some(*c),
329 (Some(mine), Some(theirs)) => Some(mine.merge(theirs)),
330 },
331 unsafe_use_conditional_create: match (
332 &self.unsafe_use_conditional_create,
333 other.unsafe_use_conditional_create,
334 ) {
335 (None, None) => None,
336 (None, Some(c)) => Some(c),
337 (Some(c), None) => Some(*c),
338 (Some(_), Some(theirs)) => Some(theirs),
339 },
340 unsafe_use_conditional_update: match (
341 &self.unsafe_use_conditional_update,
342 other.unsafe_use_conditional_update,
343 ) {
344 (None, None) => None,
345 (None, Some(c)) => Some(c),
346 (Some(c), None) => Some(*c),
347 (Some(_), Some(theirs)) => Some(theirs),
348 },
349 unsafe_use_metadata: match (
350 &self.unsafe_use_metadata,
351 other.unsafe_use_metadata,
352 ) {
353 (None, None) => None,
354 (None, Some(c)) => Some(c),
355 (Some(c), None) => Some(*c),
356 (Some(_), Some(theirs)) => Some(theirs),
357 },
358 storage_class: match (&self.storage_class, other.storage_class) {
359 (None, None) => None,
360 (None, Some(c)) => Some(c),
361 (Some(c), None) => Some(c.clone()),
362 (Some(_), Some(theirs)) => Some(theirs),
363 },
364 metadata_storage_class: match (
365 &self.metadata_storage_class,
366 other.metadata_storage_class,
367 ) {
368 (None, None) => None,
369 (None, Some(c)) => Some(c),
370 (Some(c), None) => Some(c.clone()),
371 (Some(_), Some(theirs)) => Some(theirs),
372 },
373 chunks_storage_class: match (
374 &self.chunks_storage_class,
375 other.chunks_storage_class,
376 ) {
377 (None, None) => None,
378 (None, Some(c)) => Some(c),
379 (Some(c), None) => Some(c.clone()),
380 (Some(_), Some(theirs)) => Some(theirs),
381 },
382 minimum_size_for_multipart_upload: match (
383 &self.minimum_size_for_multipart_upload,
384 other.minimum_size_for_multipart_upload,
385 ) {
386 (None, None) => None,
387 (None, Some(c)) => Some(c),
388 (Some(c), None) => Some(*c),
389 (Some(_), Some(theirs)) => Some(theirs),
390 },
391 }
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub enum VersionedUpdateResult {
397 Updated { new_version: VersionInfo },
398 NotOnLatestVersion,
399}
400
401impl VersionedUpdateResult {
402 pub fn must_write(self) -> StorageResult<VersionInfo> {
403 match self {
404 VersionedUpdateResult::Updated { new_version } => Ok(new_version),
405 VersionedUpdateResult::NotOnLatestVersion => {
406 Err(StorageError::capture(StorageErrorKind::ObjectNotFound))
407 }
408 }
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Default)]
413pub struct DeleteObjectsResult {
414 pub deleted_objects: u64,
415 pub deleted_bytes: u64,
416}
417
418impl DeleteObjectsResult {
419 pub fn merge(&mut self, other: &Self) {
420 self.deleted_objects += other.deleted_objects;
421 self.deleted_bytes += other.deleted_bytes;
422 }
423}
424
425pub enum GetModifiedResult {
426 Modified { data: Pin<Box<dyn AsyncBufRead + Send>>, new_version: VersionInfo },
427 OnLatestVersion,
428}
429
430impl fmt::Debug for GetModifiedResult {
431 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432 match self {
433 Self::Modified { new_version, .. } => {
434 f.debug_struct("Modified").field("new_version", new_version).finish()
435 }
436 Self::OnLatestVersion => write!(f, "OnLatestVersion"),
437 }
438 }
439}
440
441impl Display for GetModifiedResult {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 match self {
444 Self::Modified { new_version, .. } => {
445 write!(f, "Modified(new_version={new_version})")
446 }
447 Self::OnLatestVersion => write!(f, "OnLatestVersion"),
448 }
449 }
450}
451
452#[derive(Debug, Clone)]
457pub struct StorageInfo {
458 pub backend_type: &'static str,
460 pub fields: Vec<(&'static str, String)>,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq)]
466pub enum RepositoryCreation {
467 Allowed,
469 RefusedEmptyPrefix,
473}
474
475pub fn filter_ids_by_first_char<'a>(
477 listing: BoxStream<'a, StorageResult<ListInfo<String>>>,
478 first_chars: &HashSet<char>,
479) -> BoxStream<'a, StorageResult<ListInfo<String>>> {
480 let first_chars = first_chars.clone();
481 listing
482 .try_filter(move |info| {
483 ready(info.id.chars().next().is_some_and(|c| first_chars.contains(&c)))
484 })
485 .boxed()
486}
487
488#[async_trait]
490#[typetag::serde(tag = "type")]
491pub trait Storage: fmt::Debug + Display + sealed::Sealed + Sync + Send {
492 fn storage_info(&self) -> StorageInfo;
494
495 async fn default_settings(&self) -> StorageResult<Settings> {
496 Ok(Default::default())
497 }
498
499 async fn can_write(&self) -> StorageResult<bool>;
500
501 async fn can_create_repository(&self) -> StorageResult<RepositoryCreation> {
503 Ok(RepositoryCreation::Allowed)
504 }
505
506 async fn create_location_if_needed(&self) -> StorageResult<()> {
514 Ok(())
515 }
516
517 async fn get_object(
518 &self,
519 settings: &Settings,
520 path: &str,
521 range: Option<&Range<u64>>,
522 ) -> StorageResult<(Pin<Box<dyn AsyncBufRead + Send>>, VersionInfo)> {
523 if let Some(range) = range {
524 self.get_object_concurrently(settings, path, range).await
525 } else {
526 self.get_object_range_read(settings, path, range).await
527 }
528 }
529
530 async fn get_object_range_read(
531 &self,
532 settings: &Settings,
533 path: &str,
534 range: Option<&Range<u64>>,
535 ) -> StorageResult<(Pin<Box<dyn AsyncBufRead + Send>>, VersionInfo)> {
536 let (stream, version) = self.get_object_range(settings, path, range).await?;
537 let reader = StreamReader::new(stream.map_err(std::io::Error::other));
538 Ok((Box::pin(reader), version))
539 }
540
541 async fn get_object_range(
542 &self,
543 settings: &Settings,
544 path: &str,
545 range: Option<&Range<u64>>,
546 ) -> StorageResult<(
547 Pin<Box<dyn Stream<Item = Result<Bytes, StorageError>> + Send>>,
548 VersionInfo,
549 )>;
550
551 async fn put_object(
552 &self,
553 settings: &Settings,
554 path: &str,
555 bytes: Bytes,
556 content_type: Option<&str>,
557 metadata: Vec<(String, String)>,
558 previous_version: Option<&VersionInfo>,
559 ) -> StorageResult<VersionedUpdateResult>;
560
561 async fn copy_object(
562 &self,
563 settings: &Settings,
564 from: &str,
565 to: &str,
566 content_type: Option<&str>,
567 version: &VersionInfo,
568 ) -> StorageResult<VersionedUpdateResult>;
569
570 async fn list_objects<'a>(
575 &'a self,
576 settings: &Settings,
577 prefix: &str,
578 ) -> StorageResult<BoxStream<'a, StorageResult<ListInfo<String>>>>;
579
580 async fn list_objects_with_id_first_chars<'a>(
583 &'a self,
584 settings: &Settings,
585 prefix: &str,
586 first_chars: &HashSet<char>,
587 ) -> StorageResult<BoxStream<'a, StorageResult<ListInfo<String>>>> {
588 Ok(filter_ids_by_first_char(
589 self.list_objects(settings, prefix).await?,
590 first_chars,
591 ))
592 }
593
594 async fn delete_batch(
595 &self,
596 settings: &Settings,
597 prefix: &str,
598 batch: Vec<(String, u64)>,
599 ) -> StorageResult<DeleteObjectsResult>;
600
601 async fn get_object_last_modified(
602 &self,
603 path: &str,
604 settings: &Settings,
605 ) -> StorageResult<DateTime<Utc>>;
606
607 async fn get_object_conditional(
608 &self,
609 settings: &Settings,
610 path: &str,
611 previous_version: Option<&VersionInfo>,
612 ) -> StorageResult<GetModifiedResult>;
613
614 #[instrument(skip(self, settings, ids))]
617 async fn delete_objects(
618 &self,
619 settings: &Settings,
620 prefix: &str,
621 ids: BoxStream<'_, (String, u64)>,
622 ) -> StorageResult<DeleteObjectsResult> {
623 let res = Arc::new(Mutex::new(DeleteObjectsResult::default()));
624 ids.chunks(1_000)
625 .for_each_concurrent(10, |batch| {
627 let res = Arc::clone(&res);
628 async move {
629 let new_deletes = self
630 .delete_batch(settings, prefix, batch)
631 .await
632 .unwrap_or_else(|_| {
633 warn!("ignoring error in Storage::delete_batch");
635 Default::default()
636 });
637 #[expect(clippy::expect_used)]
638 res.lock().expect("Bug in delete objects").merge(&new_deletes);
639 }
640 })
641 .await;
642 #[expect(clippy::expect_used)]
643 let res = res.lock().expect("Bug in delete objects");
644 Ok(res.clone())
645 }
646
647 async fn root_is_clean(&self, settings: &Settings) -> StorageResult<bool> {
648 match self.list_objects(settings, "").await {
649 Ok(mut stream) => match stream.next().await {
650 None => Ok(true),
651 Some(Ok(_)) => Ok(false),
652 Some(Err(err)) => Err(err),
653 },
654 Err(StorageError { kind: StorageErrorKind::ObjectNotFound, .. }) => Ok(true),
655 Err(err) => Err(err),
656 }
657 }
658
659 async fn get_object_concurrently_multiple(
660 &self,
661 settings: &Settings,
662 key: &str,
663 parts: Vec<Range<u64>>,
664 ) -> StorageResult<(Pin<Box<dyn AsyncBufRead + Send>>, VersionInfo)> {
665 let settings2 = settings.clone();
666 let key2 = key.to_string();
667 let results = parts
668 .into_iter()
669 .map(move |range| {
670 let key = key2.clone();
671 let settings = settings2.clone();
672 async move {
673 let (stream, version) = self
674 .get_object_range(&settings, key.as_ref(), Some(&range))
675 .await?;
676 let all_bytes: Vec<_> = stream.try_collect().await?;
677 Ok::<_, StorageError>((all_bytes, version))
678 }
679 })
680 .collect::<FuturesOrdered<_>>();
681
682 let results = results.peekable();
683 tokio::pin!(results);
684 let version = match results.as_mut().peek().await {
685 Some(Ok((_, version))) => version.clone(),
686 _ => VersionInfo::for_creation(),
687 };
688 let all_bytes = results
689 .map_ok(|(all_bytes, _)| stream::iter(all_bytes).map(Ok::<_, StorageError>))
690 .try_flatten()
691 .try_collect::<Vec<_>>()
692 .await?;
693
694 let res = StreamReader::new(stream::iter(all_bytes).map(Ok::<_, std::io::Error>));
695 Ok((Box::pin(res), version))
696 }
697
698 async fn get_object_concurrently(
699 &self,
700 settings: &Settings,
701 key: &str,
702 range: &Range<u64>,
703 ) -> StorageResult<(Pin<Box<dyn AsyncBufRead + Send>>, VersionInfo)> {
704 let parts = split_in_multiple_requests(
705 range,
706 settings.concurrency().ideal_concurrent_request_size().get(),
707 settings.concurrency().max_concurrent_requests_for_object().get(),
708 )
709 .collect::<Vec<_>>();
710
711 let res: (Pin<Box<dyn AsyncBufRead + Send>>, VersionInfo) = match parts.len() {
712 0 => (Box::pin(tokio::io::empty()), VersionInfo::for_creation()),
713 1 => self.get_object_range_read(settings, key, Some(range)).await?,
714 _ => self.get_object_concurrently_multiple(settings, key, parts).await?,
715 };
716 Ok(res)
717 }
718}
719
720pub fn split_in_multiple_requests(
731 range: &Range<u64>,
732 ideal_req_size: u64,
733 max_requests: u16,
734) -> impl Iterator<Item = Range<u64>> + use<> {
735 let size = max(0, range.end - range.start);
736 let num_parts = size.div_ceil(ideal_req_size);
738 let num_parts = max(1, min(num_parts, max_requests as u64));
740
741 let big_parts = size % num_parts;
744 let small_parts_size = size / num_parts;
745 let big_parts_size = small_parts_size + 1;
746
747 iter::successors(Some((1, range.start..range.start)), move |(index, prev_range)| {
748 let size = if *index <= big_parts { big_parts_size } else { small_parts_size };
749 Some((index + 1, prev_range.end..prev_range.end + size))
750 })
751 .dropping(1)
752 .take(num_parts as usize)
753 .map(|(_, range)| range)
754}
755
756pub fn split_in_multiple_equal_requests(
765 range: &Range<u64>,
766 ideal_req_size: u64,
767 max_requests: u16,
768) -> impl Iterator<Item = Range<u64>> + use<> {
769 let size = max(0, range.end - range.start);
770 let num_parts = size.div_ceil(ideal_req_size);
772 let num_parts = max(1, min(num_parts, max_requests as u64));
774
775 let big_parts = num_parts - 1;
776 let big_parts_size = size / max(1, big_parts);
777 let small_part_size = size - big_parts_size * big_parts;
778
779 iter::successors(Some((1, range.start..range.start)), move |(index, prev_range)| {
780 let size = if *index <= big_parts { big_parts_size } else { small_part_size };
781 Some((index + 1, prev_range.end..prev_range.end + size))
782 })
783 .dropping(1)
784 .take(num_parts as usize)
785 .map(|(_, range)| range)
786}
787
788pub fn strip_quotes(s: &str) -> &str {
789 s.strip_prefix('"').and_then(|s| s.strip_suffix('"')).unwrap_or(s)
790}