Skip to content

Commit 751f4e4

Browse files
authored
Don't create local path when opening a repo (#2145)
Fix #2105 - `get_client` in `ObjectStorage` returns an `StorageResult`, so we can track errors during client instantiation. - `create_location_if_needed` in the `Storage` trait. This is mostly aimed at storages that are cheap/easy to create, like local FS. Add tests for both Rust and Python.
1 parent 9a96545 commit 751f4e4

4 files changed

Lines changed: 100 additions & 24 deletions

File tree

icechunk-arrow-object-store/src/lib.rs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ use serde::{Deserialize, Serialize};
4848
use std::{
4949
collections::HashMap,
5050
fmt::{self, Debug, Display},
51-
fs::create_dir_all,
5251
future::ready,
5352
num::{NonZeroU16, NonZeroU64},
5453
ops::Range,
@@ -264,15 +263,12 @@ impl ObjectStorage {
264263
/// client is not serializeable and must be initialized after deserialization. Under normal construction
265264
/// the original client is returned immediately.
266265
#[instrument(skip_all)]
267-
async fn get_client(&self, settings: &Settings) -> &Arc<dyn ObjectStore> {
266+
async fn get_client(
267+
&self,
268+
settings: &Settings,
269+
) -> StorageResult<&Arc<dyn ObjectStore>> {
268270
self.client
269-
.get_or_init(|| async {
270-
// TODO: handle error better?
271-
#[expect(clippy::expect_used)]
272-
self.backend
273-
.mk_object_store(settings)
274-
.expect("failed to create object store")
275-
})
271+
.get_or_try_init(|| async { self.backend.mk_object_store(settings) })
276272
.await
277273
}
278274

@@ -287,7 +283,7 @@ impl ObjectStorage {
287283
/// Intended for testing and debugging purposes only.
288284
pub async fn all_keys(&self) -> StorageResult<Vec<String>> {
289285
self.get_client(&self.backend.default_settings())
290-
.await
286+
.await?
291287
.list(None)
292288
.map_ok(|obj| obj.location.to_string())
293289
.try_collect()
@@ -362,6 +358,10 @@ impl Storage for ObjectStorage {
362358
Ok(self.backend.can_write())
363359
}
364360

361+
async fn create_location_if_needed(&self) -> StorageResult<()> {
362+
self.backend.create_location_if_needed()
363+
}
364+
365365
#[instrument(skip_all)]
366366
async fn default_settings(&self) -> StorageResult<Settings> {
367367
Ok(self.backend.default_settings())
@@ -394,7 +394,7 @@ impl Storage for ObjectStorage {
394394
let options = PutOptions { mode, attributes, ..PutOptions::default() };
395395
// FIXME: use multipart
396396
let res =
397-
self.get_client(settings).await.put_opts(&path, bytes.into(), options).await;
397+
self.get_client(settings).await?.put_opts(&path, bytes.into(), options).await;
398398
match res {
399399
Ok(res) => {
400400
let new_version = VersionInfo {
@@ -431,7 +431,7 @@ impl Storage for ObjectStorage {
431431
if_match: version.etag().map(|e| strip_quotes(e).into()),
432432
..Default::default()
433433
};
434-
let result = self.get_client(settings).await.get_opts(&from, opts).await;
434+
let result = self.get_client(settings).await?.get_opts(&from, opts).await;
435435
match result {
436436
Ok(result) => {
437437
let bytes = result
@@ -440,7 +440,7 @@ impl Storage for ObjectStorage {
440440
.map_err(|e| StorageErrorKind::ObjectStore(Box::new(e)))
441441
.capture()?;
442442
self.get_client(settings)
443-
.await
443+
.await?
444444
.put(&to, bytes.into())
445445
.await
446446
.map_err(|e| StorageErrorKind::ObjectStore(Box::new(e)))
@@ -454,7 +454,7 @@ impl Storage for ObjectStorage {
454454
Err(err) => Err(obj_store_error(err)),
455455
}
456456
} else {
457-
match self.get_client(settings).await.copy(&from, &to).await {
457+
match self.get_client(settings).await?.copy(&from, &to).await {
458458
Ok(_) => {
459459
Ok(VersionedUpdateResult::Updated { new_version: version.clone() })
460460
}
@@ -472,7 +472,7 @@ impl Storage for ObjectStorage {
472472
) -> StorageResult<BoxStream<'a, StorageResult<ListInfo<String>>>> {
473473
let prefix = ObjectPath::from(format!("{}/{}", self.backend.prefix(), prefix));
474474
let stream =
475-
self.get_client(settings).await.list(Some(&prefix)).map(move |object| {
475+
self.get_client(settings).await?.list(Some(&prefix)).map(move |object| {
476476
let prefix = prefix.clone();
477477
object
478478
.map_err(obj_store_error)
@@ -496,7 +496,7 @@ impl Storage for ObjectStorage {
496496
sizes.insert(path, size);
497497
}
498498
let results =
499-
self.get_client(settings).await.delete_stream(stream::iter(ids).boxed());
499+
self.get_client(settings).await?.delete_stream(stream::iter(ids).boxed());
500500
let res = results
501501
.fold(DeleteObjectsResult::default(), |mut res, delete_result| {
502502
if let Ok(deleted_path) = delete_result {
@@ -525,7 +525,7 @@ impl Storage for ObjectStorage {
525525
let path = self.prefixed_path(path);
526526
let res = self
527527
.get_client(settings)
528-
.await
528+
.await?
529529
.head(&path)
530530
.await
531531
.map_err(Box::new)
@@ -598,7 +598,7 @@ impl ObjectStorage {
598598
.and_then(|v| v.etag().map(|e| strip_quotes(e).into())),
599599
..Default::default()
600600
};
601-
let res = self.get_client(settings).await.get_opts(&full_key, opts).await;
601+
let res = self.get_client(settings).await?.get_opts(&full_key, opts).await;
602602

603603
match res {
604604
Ok(result) => {
@@ -640,6 +640,10 @@ pub trait ObjectStoreBackend: Debug + Display + Sync + Send {
640640
fn can_write(&self) -> bool {
641641
true
642642
}
643+
644+
fn create_location_if_needed(&self) -> Result<(), StorageError> {
645+
Ok(())
646+
}
643647
}
644648

645649
#[derive(Debug, Serialize, Deserialize)]
@@ -717,8 +721,13 @@ impl ObjectStoreBackend for LocalFileSystemObjectStoreBackend {
717721
&self,
718722
_settings: &Settings,
719723
) -> Result<Arc<dyn ObjectStore>, StorageError> {
720-
create_dir_all(&self.path).capture()?;
721-
let path = std::fs::canonicalize(&self.path).capture()?;
724+
let path = std::fs::canonicalize(&self.path).map_err(|err| {
725+
if err.kind() == std::io::ErrorKind::NotFound {
726+
StorageError::capture(StorageErrorKind::ObjectNotFound)
727+
} else {
728+
StorageError::capture(StorageErrorKind::IOError(err))
729+
}
730+
})?;
722731
let fs = LocalFileSystem::new_with_prefix(path).capture_box()?;
723732
Ok(Arc::new(fs))
724733
}
@@ -751,6 +760,11 @@ impl ObjectStoreBackend for LocalFileSystemObjectStoreBackend {
751760
..Default::default()
752761
}
753762
}
763+
764+
fn create_location_if_needed(&self) -> Result<(), StorageError> {
765+
std::fs::create_dir_all(&self.path).capture()?;
766+
Ok(())
767+
}
754768
}
755769

756770
#[cfg(feature = "http")]

icechunk-python/tests/test_session.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import tempfile
33
import time
44
from datetime import UTC, datetime
5+
from pathlib import Path
56
from typing import Any, cast
67

78
import pytest
@@ -504,6 +505,19 @@ async def test_repo_status_readonly_blocks_rearrange_session_async() -> None:
504505
await repo.garbage_collect_async(datetime.now(UTC))
505506

506507

508+
def test_open_doesnt_create_dir() -> None:
509+
with tempfile.TemporaryDirectory() as tmpdir:
510+
missing = Path(tmpdir) / "should_not_exist"
511+
present = Path(tmpdir) / "present"
512+
513+
with pytest.raises(IcechunkError, match=r"the repository doesn.t exist"):
514+
Repository.open(local_filesystem_storage(str(missing)))
515+
assert not missing.exists()
516+
517+
Repository.create(local_filesystem_storage(str(present)))
518+
assert present.exists()
519+
520+
507521
async def test_repo_status_readonly_blocks_writable_session_async() -> None:
508522
repo = await Repository.create_async(
509523
storage=in_memory_storage(),

icechunk-storage/src/storage.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,17 @@ pub trait Storage: fmt::Debug + Display + sealed::Sealed + Sync + Send {
464464

465465
async fn can_write(&self) -> StorageResult<bool>;
466466

467+
/// Ensure the storage location is ready to receive writes.
468+
///
469+
/// Called by [`Repository::create`] before any other I/O. The default
470+
/// implementation is a no-op; backends that need to materialize the
471+
/// location (e.g. the local filesystem creating the directory) override
472+
/// this. Read paths like `open` never call it, so a missing location
473+
/// stays missing.
474+
async fn create_location_if_needed(&self) -> StorageResult<()> {
475+
Ok(())
476+
}
477+
467478
async fn get_object(
468479
&self,
469480
settings: &Settings,
@@ -581,10 +592,14 @@ pub trait Storage: fmt::Debug + Display + sealed::Sealed + Sync + Send {
581592
}
582593

583594
async fn root_is_clean(&self, settings: &Settings) -> StorageResult<bool> {
584-
match self.list_objects(settings, "").await?.next().await {
585-
None => Ok(true),
586-
Some(Ok(_)) => Ok(false),
587-
Some(Err(err)) => Err(err),
595+
match self.list_objects(settings, "").await {
596+
Ok(mut stream) => match stream.next().await {
597+
None => Ok(true),
598+
Some(Ok(_)) => Ok(false),
599+
Some(Err(err)) => Err(err),
600+
},
601+
Err(StorageError { kind: StorageErrorKind::ObjectNotFound, .. }) => Ok(true),
602+
Err(err) => Err(err),
588603
}
589604
}
590605

icechunk/src/repository.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ impl Repository {
210210
) -> RepositoryResult<Self> {
211211
debug!("Creating Repository");
212212
raise_if_cant_write(storage.as_ref(), "Cannot create repository").await?;
213+
storage.create_location_if_needed().await.inject()?;
213214

214215
let has_overriden_config = match config {
215216
Some(ref config) => config != &RepositoryConfig::default(),
@@ -3908,6 +3909,38 @@ mod tests {
39083909
Ok(())
39093910
}
39103911

3912+
#[cfg(feature = "object-store-fs")]
3913+
#[tokio::test]
3914+
async fn open_doesnt_create_dir() -> Result<(), Box<dyn Error>> {
3915+
use crate::new_local_filesystem_storage;
3916+
3917+
let repo_dir = TempDir::new()?;
3918+
let missing = repo_dir.path().join("should_not_exist");
3919+
let present = repo_dir.path().join("present");
3920+
3921+
let storage: Arc<dyn Storage + Send + Sync> =
3922+
new_local_filesystem_storage(&missing)
3923+
.await
3924+
.expect("Creating local storage failed");
3925+
3926+
let open_err = Repository::open(None, Arc::clone(&storage), HashMap::new()).await;
3927+
assert!(matches!(
3928+
open_err,
3929+
Err(RepositoryError { kind: RepositoryErrorKind::RepositoryDoesntExist, .. })
3930+
));
3931+
assert!(!missing.exists());
3932+
3933+
let storage: Arc<dyn Storage + Send + Sync> =
3934+
new_local_filesystem_storage(&present)
3935+
.await
3936+
.expect("Creating local storage failed");
3937+
Repository::create(None, Arc::clone(&storage), HashMap::new(), None, true)
3938+
.await?;
3939+
assert!(present.exists());
3940+
3941+
Ok(())
3942+
}
3943+
39113944
#[tokio::test]
39123945
async fn set_metadata() -> Result<(), Box<dyn Error>> {
39133946
let storage: Arc<dyn Storage + Send + Sync> = new_in_memory_storage().await?;

0 commit comments

Comments
 (0)