1use std::collections::HashMap;
19use std::ops::Range;
20use std::sync::{Arc, OnceLock};
21
22use bytes::Bytes;
23use futures::{Stream, StreamExt, stream};
24
25use super::storage::{
26 LocalFsStorageFactory, MemoryStorageFactory, Storage, StorageConfig, StorageFactory,
27};
28use crate::Result;
29
30#[derive(Clone, Debug)]
64pub struct FileIO {
65 config: StorageConfig,
67 factory: Arc<dyn StorageFactory>,
69 storage: Arc<OnceLock<Arc<dyn Storage>>>,
71 prefixed: Arc<Vec<PrefixedStorage>>,
74}
75
76#[derive(Debug)]
78struct PrefixedStorage {
79 prefix: String,
80 config: StorageConfig,
81 storage: OnceLock<Arc<dyn Storage>>,
82}
83
84impl FileIO {
85 pub fn new_with_memory() -> Self {
89 Self {
90 config: StorageConfig::new(),
91 factory: Arc::new(MemoryStorageFactory),
92 storage: Arc::new(OnceLock::new()),
93 prefixed: Arc::new(Vec::new()),
94 }
95 }
96
97 pub fn new_with_fs() -> Self {
101 Self {
102 config: StorageConfig::new(),
103 factory: Arc::new(LocalFsStorageFactory),
104 storage: Arc::new(OnceLock::new()),
105 prefixed: Arc::new(Vec::new()),
106 }
107 }
108
109 pub fn config(&self) -> &StorageConfig {
111 &self.config
112 }
113
114 fn get_storage(&self, path: &str) -> Result<Arc<dyn Storage>> {
117 for ps in self.prefixed.iter() {
121 if path.starts_with(&ps.prefix) {
122 return Self::get_or_build(&ps.storage, &self.factory, &ps.config);
123 }
124 }
125 Self::get_or_build(&self.storage, &self.factory, &self.config)
126 }
127
128 fn get_or_build(
130 cell: &OnceLock<Arc<dyn Storage>>,
131 factory: &Arc<dyn StorageFactory>,
132 config: &StorageConfig,
133 ) -> Result<Arc<dyn Storage>> {
134 if let Some(storage) = cell.get() {
135 return Ok(storage.clone());
136 }
137 let storage = factory.build(config)?;
138 let _ = cell.set(storage);
140 Ok(cell.get().unwrap().clone())
141 }
142
143 pub async fn delete(&self, path: impl AsRef<str>) -> Result<()> {
149 self.get_storage(path.as_ref())?.delete(path.as_ref()).await
150 }
151
152 pub async fn delete_prefix(&self, path: impl AsRef<str>) -> Result<()> {
164 self.get_storage(path.as_ref())?
165 .delete_prefix(path.as_ref())
166 .await
167 }
168
169 pub async fn delete_stream(
175 &self,
176 paths: impl Stream<Item = String> + Send + 'static,
177 ) -> Result<()> {
178 if self.prefixed.is_empty() {
180 return self.get_storage("")?.delete_stream(paths.boxed()).await;
181 }
182
183 const DELETE_BATCH_SIZE: usize = 1000;
186 let mut groups: HashMap<String, Vec<String>> = HashMap::new();
187 let mut paths = paths.boxed();
188 while let Some(path) = paths.next().await {
189 let key = self
190 .prefixed
191 .iter()
192 .find(|ps| path.starts_with(&ps.prefix))
193 .map(|ps| ps.prefix.clone())
194 .unwrap_or_default();
195 let buf = groups.entry(key).or_default();
196 buf.push(path);
197 if buf.len() >= DELETE_BATCH_SIZE {
198 let full = std::mem::take(buf);
199 self.get_storage(&full[0])?
200 .delete_stream(stream::iter(full).boxed())
201 .await?;
202 }
203 }
204
205 for batch in groups.into_values() {
207 if batch.is_empty() {
208 continue;
209 }
210 self.get_storage(&batch[0])?
211 .delete_stream(stream::iter(batch).boxed())
212 .await?;
213 }
214 Ok(())
215 }
216
217 pub async fn exists(&self, path: impl AsRef<str>) -> Result<bool> {
223 self.get_storage(path.as_ref())?.exists(path.as_ref()).await
224 }
225
226 pub fn new_input(&self, path: impl AsRef<str>) -> Result<InputFile> {
232 self.get_storage(path.as_ref())?.new_input(path.as_ref())
233 }
234
235 pub fn new_output(&self, path: impl AsRef<str>) -> Result<OutputFile> {
241 self.get_storage(path.as_ref())?.new_output(path.as_ref())
242 }
243}
244
245#[derive(Clone, Debug)]
250pub struct FileIOBuilder {
251 factory: Arc<dyn StorageFactory>,
253 config: StorageConfig,
255 prefixed: Vec<(String, StorageConfig)>,
257}
258
259impl FileIOBuilder {
260 pub fn new(factory: Arc<dyn StorageFactory>) -> Self {
262 Self {
263 factory,
264 config: StorageConfig::new(),
265 prefixed: Vec::new(),
266 }
267 }
268
269 pub fn with_prop(mut self, key: impl ToString, value: impl ToString) -> Self {
271 self.config = self.config.with_prop(key.to_string(), value.to_string());
272 self
273 }
274
275 pub fn with_props(
277 mut self,
278 args: impl IntoIterator<Item = (impl ToString, impl ToString)>,
279 ) -> Self {
280 self.config = self
281 .config
282 .with_props(args.into_iter().map(|e| (e.0.to_string(), e.1.to_string())));
283 self
284 }
285
286 pub fn with_prefixed_props(
289 mut self,
290 prefix: impl Into<String>,
291 props: impl IntoIterator<Item = (impl ToString, impl ToString)>,
292 ) -> Self {
293 let config = StorageConfig::from_props(
294 props
295 .into_iter()
296 .map(|(k, v)| (k.to_string(), v.to_string()))
297 .collect(),
298 );
299 self.prefixed.push((prefix.into(), config));
300 self
301 }
302
303 pub fn config(&self) -> &StorageConfig {
305 &self.config
306 }
307
308 pub fn build(self) -> FileIO {
310 let mut prefixed: Vec<PrefixedStorage> = self
311 .prefixed
312 .into_iter()
313 .map(|(prefix, config)| PrefixedStorage {
314 prefix,
315 config,
316 storage: OnceLock::new(),
317 })
318 .collect();
319 prefixed.sort_by_key(|item| std::cmp::Reverse(item.prefix.len()));
321 FileIO {
322 config: self.config,
323 factory: self.factory,
324 storage: Arc::new(OnceLock::new()),
325 prefixed: Arc::new(prefixed),
326 }
327 }
328}
329
330pub struct FileMetadata {
334 pub size: u64,
336}
337
338#[async_trait::async_trait]
344pub trait FileRead: Send + Sync + Unpin + 'static {
345 async fn read(&self, range: Range<u64>) -> crate::Result<Bytes>;
349}
350
351#[async_trait::async_trait]
352impl<T: AsRef<dyn FileRead> + Send + Sync + Unpin + 'static> FileRead for T {
353 async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
354 self.as_ref().read(range).await
355 }
356}
357
358#[derive(Debug)]
360pub struct InputFile {
361 storage: Arc<dyn Storage>,
362 path: String,
364}
365
366impl InputFile {
367 pub fn new(storage: Arc<dyn Storage>, path: String) -> Self {
369 Self { storage, path }
370 }
371
372 pub fn location(&self) -> &str {
374 &self.path
375 }
376
377 pub async fn exists(&self) -> crate::Result<bool> {
379 self.storage.exists(&self.path).await
380 }
381
382 pub async fn metadata(&self) -> crate::Result<FileMetadata> {
384 self.storage.metadata(&self.path).await
385 }
386
387 pub async fn read(&self) -> crate::Result<Bytes> {
391 self.storage.read(&self.path).await
392 }
393
394 pub async fn reader(&self) -> crate::Result<Box<dyn FileRead>> {
398 self.storage.reader(&self.path).await
399 }
400}
401
402#[async_trait::async_trait]
409pub trait FileWrite: Send + Unpin + 'static {
410 async fn write(&mut self, bs: Bytes) -> crate::Result<()>;
414
415 async fn close(&mut self) -> crate::Result<()>;
419}
420
421#[derive(Debug)]
423pub struct OutputFile {
424 storage: Arc<dyn Storage>,
425 path: String,
427}
428
429impl OutputFile {
430 pub fn new(storage: Arc<dyn Storage>, path: String) -> Self {
432 Self { storage, path }
433 }
434
435 pub fn location(&self) -> &str {
437 &self.path
438 }
439
440 pub async fn exists(&self) -> Result<bool> {
442 self.storage.exists(&self.path).await
443 }
444
445 pub async fn delete(&self) -> Result<()> {
449 self.storage.delete(&self.path).await
450 }
451
452 pub fn to_input_file(self) -> InputFile {
454 InputFile {
455 storage: self.storage,
456 path: self.path,
457 }
458 }
459
460 pub async fn write(&self, bs: Bytes) -> crate::Result<()> {
467 self.storage.write(&self.path, bs).await
468 }
469
470 pub async fn writer(&self) -> crate::Result<Box<dyn FileWrite>> {
476 self.storage.writer(&self.path).await
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use std::fs::{File, create_dir_all};
483 use std::io::Write;
484 use std::path::Path;
485 use std::sync::Arc;
486
487 use bytes::Bytes;
488 use futures::AsyncReadExt;
489 use futures::io::AllowStdIo;
490 use tempfile::TempDir;
491
492 use super::{FileIO, FileIOBuilder};
493 use crate::io::{LocalFsStorageFactory, MemoryStorageFactory};
494
495 fn create_local_file_io() -> FileIO {
496 FileIO::new_with_fs()
497 }
498
499 fn write_to_file<P: AsRef<Path>>(s: &str, path: P) {
500 create_dir_all(path.as_ref().parent().unwrap()).unwrap();
501 let mut f = File::create(path).unwrap();
502 write!(f, "{s}").unwrap();
503 }
504
505 async fn read_from_file<P: AsRef<Path>>(path: P) -> String {
506 let mut f = AllowStdIo::new(File::open(path).unwrap());
507 let mut s = String::new();
508 f.read_to_string(&mut s).await.unwrap();
509 s
510 }
511
512 #[tokio::test]
513 async fn test_local_input_file() {
514 let tmp_dir = TempDir::new().unwrap();
515
516 let file_name = "a.txt";
517 let content = "Iceberg loves rust.";
518
519 let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name);
520 write_to_file(content, &full_path);
521
522 let file_io = create_local_file_io();
523 let input_file = file_io.new_input(&full_path).unwrap();
524
525 assert!(input_file.exists().await.unwrap());
526 assert_eq!(&full_path, input_file.location());
527 let read_content = read_from_file(full_path).await;
528
529 assert_eq!(content, &read_content);
530 }
531
532 #[tokio::test]
533 async fn test_delete_local_file() {
534 let tmp_dir = TempDir::new().unwrap();
535
536 let a_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), "a.txt");
537 let sub_dir_path = format!("{}/sub", tmp_dir.path().to_str().unwrap());
538 let b_path = format!("{}/{}", sub_dir_path, "b.txt");
539 let c_path = format!("{}/{}", sub_dir_path, "c.txt");
540 write_to_file("Iceberg loves rust.", &a_path);
541 write_to_file("Iceberg loves rust.", &b_path);
542 write_to_file("Iceberg loves rust.", &c_path);
543
544 let file_io = create_local_file_io();
545 assert!(file_io.exists(&a_path).await.unwrap());
546
547 file_io.delete_prefix(&a_path).await.unwrap();
549 assert!(file_io.exists(&a_path).await.unwrap());
550
551 file_io.delete_prefix("not_exists/").await.unwrap();
553
554 file_io.delete_prefix(&sub_dir_path).await.unwrap();
556 assert!(!file_io.exists(&b_path).await.unwrap());
557 assert!(!file_io.exists(&c_path).await.unwrap());
558 assert!(file_io.exists(&a_path).await.unwrap());
559
560 file_io.delete(&a_path).await.unwrap();
561 assert!(!file_io.exists(&a_path).await.unwrap());
562 }
563
564 #[tokio::test]
565 async fn test_delete_non_exist_file() {
566 let tmp_dir = TempDir::new().unwrap();
567
568 let file_name = "a.txt";
569 let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name);
570
571 let file_io = create_local_file_io();
572 assert!(!file_io.exists(&full_path).await.unwrap());
573 assert!(file_io.delete(&full_path).await.is_ok());
574 assert!(file_io.delete_prefix(&full_path).await.is_ok());
575 }
576
577 #[tokio::test]
578 async fn test_local_output_file() {
579 let tmp_dir = TempDir::new().unwrap();
580
581 let file_name = "a.txt";
582 let content = "Iceberg loves rust.";
583
584 let full_path = format!("{}/{}", tmp_dir.path().to_str().unwrap(), file_name);
585
586 let file_io = create_local_file_io();
587 let output_file = file_io.new_output(&full_path).unwrap();
588
589 assert!(!output_file.exists().await.unwrap());
590 {
591 output_file.write(content.into()).await.unwrap();
592 }
593
594 assert_eq!(&full_path, output_file.location());
595
596 let read_content = read_from_file(full_path).await;
597
598 assert_eq!(content, &read_content);
599 }
600
601 #[tokio::test]
602 async fn test_memory_io() {
603 let io = FileIO::new_with_memory();
604
605 let path = format!("{}/1.txt", TempDir::new().unwrap().path().to_str().unwrap());
606
607 let output_file = io.new_output(&path).unwrap();
608 output_file.write("test".into()).await.unwrap();
609
610 assert!(io.exists(&path.clone()).await.unwrap());
611 let input_file = io.new_input(&path).unwrap();
612 let content = input_file.read().await.unwrap();
613 assert_eq!(content, Bytes::from("test"));
614
615 io.delete(&path).await.unwrap();
616 assert!(!io.exists(&path).await.unwrap());
617 }
618
619 #[tokio::test]
620 async fn test_file_io_builder_with_props() {
621 let factory = Arc::new(MemoryStorageFactory);
622 let file_io = FileIOBuilder::new(factory)
623 .with_prop("key1", "value1")
624 .with_prop("key2", "value2")
625 .build();
626
627 assert_eq!(file_io.config().get("key1"), Some(&"value1".to_string()));
628 assert_eq!(file_io.config().get("key2"), Some(&"value2".to_string()));
629 }
630
631 #[tokio::test]
632 async fn test_file_io_builder_with_multiple_props() {
633 let factory = Arc::new(LocalFsStorageFactory);
634 let props = vec![("key1", "value1"), ("key2", "value2")];
635 let file_io = FileIOBuilder::new(factory).with_props(props).build();
636
637 assert_eq!(file_io.config().get("key1"), Some(&"value1".to_string()));
638 assert_eq!(file_io.config().get("key2"), Some(&"value2".to_string()));
639 }
640
641 #[tokio::test]
642 async fn test_prefixed_props_sorted_by_descending_prefix_length() {
643 let factory = Arc::new(MemoryStorageFactory);
644 let file_io = FileIOBuilder::new(factory)
645 .with_prefixed_props("memory://a/", [("k", "short")])
646 .with_prefixed_props("memory://a/longer/", [("k", "long")])
647 .build();
648
649 let prefixes: Vec<&str> = file_io.prefixed.iter().map(|p| p.prefix.as_str()).collect();
651 assert_eq!(prefixes, vec!["memory://a/longer/", "memory://a/"]);
652 }
653
654 #[tokio::test]
655 async fn test_prefixed_config_carries_credential_values() {
656 let factory = Arc::new(MemoryStorageFactory);
658 let file_io = FileIOBuilder::new(factory)
659 .with_prop("s3.region", "us-east-1")
660 .with_prefixed_props("s3://bucket/table", [
661 ("s3.region", "us-east-1"),
662 ("s3.access-key-id", "vended-key"),
663 ("s3.secret-access-key", "vended-secret"),
664 ])
665 .build();
666
667 assert_eq!(
669 file_io.config().get("s3.region"),
670 Some(&"us-east-1".to_string())
671 );
672 assert_eq!(file_io.config().get("s3.access-key-id"), None);
673
674 let prefixed = &file_io.prefixed[0].config;
676 assert_eq!(prefixed.get("s3.region"), Some(&"us-east-1".to_string()));
677 assert_eq!(
678 prefixed.get("s3.access-key-id"),
679 Some(&"vended-key".to_string())
680 );
681 assert_eq!(
682 prefixed.get("s3.secret-access-key"),
683 Some(&"vended-secret".to_string())
684 );
685 }
686
687 #[tokio::test]
688 async fn test_get_storage_routes_by_prefix() {
689 let factory = Arc::new(MemoryStorageFactory);
690 let file_io = FileIOBuilder::new(factory)
691 .with_prop("scope", "default")
692 .with_prefixed_props("memory://creds/", [("scope", "prefixed")])
693 .build();
694
695 let default_a = file_io.get_storage("memory://other/x").unwrap();
696 let default_b = file_io.get_storage("memory://other/y").unwrap();
697 let prefixed_a = file_io.get_storage("memory://creds/x").unwrap();
698 let prefixed_b = file_io.get_storage("memory://creds/y").unwrap();
699
700 assert!(Arc::ptr_eq(&default_a, &default_b));
702 assert!(Arc::ptr_eq(&prefixed_a, &prefixed_b));
703 assert!(!Arc::ptr_eq(&default_a, &prefixed_a));
705 }
706
707 #[tokio::test]
708 async fn test_delete_stream_routes_by_prefix() {
709 let factory = Arc::new(MemoryStorageFactory);
710 let file_io = FileIOBuilder::new(factory)
711 .with_prefixed_props("memory:/creds/", [("k", "v")])
712 .build();
713
714 let default_path = "memory:/other/a.txt";
716 let prefixed_path = "memory:/creds/b.txt";
717 for path in [default_path, prefixed_path] {
718 file_io
719 .new_output(path)
720 .unwrap()
721 .write("x".into())
722 .await
723 .unwrap();
724 assert!(file_io.exists(path).await.unwrap());
725 }
726
727 file_io
729 .delete_stream(futures::stream::iter(vec![
730 default_path.to_string(),
731 prefixed_path.to_string(),
732 ]))
733 .await
734 .unwrap();
735
736 assert!(!file_io.exists(default_path).await.unwrap());
737 assert!(!file_io.exists(prefixed_path).await.unwrap());
738 }
739
740 #[tokio::test]
741 async fn test_delete_stream_flushes_across_batches() {
742 let factory = Arc::new(MemoryStorageFactory);
744 let file_io = FileIOBuilder::new(factory)
745 .with_prefixed_props("memory:/creds/", [("k", "v")])
746 .build();
747
748 let n = 1050;
749 let mut paths = Vec::with_capacity(n);
750 for i in 0..n {
751 let p = format!("memory:/creds/f{i}.txt");
752 file_io
753 .new_output(&p)
754 .unwrap()
755 .write("x".into())
756 .await
757 .unwrap();
758 paths.push(p);
759 }
760
761 file_io
762 .delete_stream(futures::stream::iter(paths.clone()))
763 .await
764 .unwrap();
765
766 for p in &paths {
767 assert!(!file_io.exists(p).await.unwrap());
768 }
769 }
770}