Skip to main content

lance_table/io/commit/
external_manifest.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Trait for external manifest handler.
5//!
6//! This trait abstracts an external storage with put_if_not_exists semantics.
7
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use bytes::Bytes;
12use futures::StreamExt;
13use lance_core::utils::tracing::{
14    AUDIT_MODE_CREATE, AUDIT_MODE_DELETE, AUDIT_TYPE_MANIFEST, TRACE_FILE_AUDIT,
15};
16use lance_core::{Error, Result};
17use lance_io::object_store::ObjectStore;
18use log::warn;
19use object_store::ObjectMeta;
20use object_store::ObjectStoreExt;
21use object_store::{Error as ObjectStoreError, ObjectStore as OSObjectStore, path::Path};
22use tracing::info;
23
24use super::{
25    MANIFEST_EXTENSION, ManifestLocation, ManifestNamingScheme, current_manifest_path,
26    default_resolve_version, make_staging_manifest_path, write_version_hint,
27};
28use crate::format::{IndexMetadata, Manifest, Transaction};
29use crate::io::commit::{CommitError, CommitHandler};
30
31/// External manifest store
32///
33/// This trait abstracts an external storage for source of truth for manifests.
34/// The storage is expected to remember (uri, version) -> manifest_path
35/// and able to run transactions on the manifest_path.
36///
37/// This trait is called an **External** manifest store because the store is
38/// expected to work in tandem with the object store. We are only leveraging
39/// the external store for concurrent commit. Any manifest committed thru this
40/// trait should ultimately be materialized in the object store.
41/// For a visual explanation of the commit loop see
42/// <https://github.com/lance-format/lance/assets/12615154/b0822312-0826-432a-b554-3965f8d48d04>
43#[async_trait]
44pub trait ExternalManifestStore: std::fmt::Debug + Send + Sync {
45    /// Get the manifest path for a given base_uri and version
46    async fn get(&self, base_uri: &str, version: u64) -> Result<String>;
47
48    async fn get_manifest_location(
49        &self,
50        base_uri: &str,
51        version: u64,
52    ) -> Result<ManifestLocation> {
53        let path = self.get(base_uri, version).await?;
54        let path = Path::parse(&path).map_err(|e| Error::invalid_input(e.to_string()))?;
55        let naming_scheme = detect_naming_scheme_from_path(&path)?;
56        Ok(ManifestLocation {
57            version,
58            path,
59            size: None,
60            naming_scheme,
61            e_tag: None,
62        })
63    }
64
65    /// Get the latest version of a dataset at the base_uri, and the path to the manifest.
66    /// The path is provided as an optimization. The path is deterministic based on
67    /// the version and the store should not customize it.
68    async fn get_latest_version(&self, base_uri: &str) -> Result<Option<(u64, String)>>;
69
70    /// Get the latest manifest location for a given base_uri.
71    ///
72    /// By default, this calls get_latest_version.  Impls should
73    /// override this method if they store both the location and size
74    /// of the latest manifest.
75    async fn get_latest_manifest_location(
76        &self,
77        base_uri: &str,
78    ) -> Result<Option<ManifestLocation>> {
79        self.get_latest_version(base_uri).await.and_then(|res| {
80            res.map(|(version, uri)| {
81                let path = Path::parse(&uri).map_err(|e| Error::invalid_input(e.to_string()))?;
82                let naming_scheme = detect_naming_scheme_from_path(&path)?;
83                Ok(ManifestLocation {
84                    version,
85                    path,
86                    size: None,
87                    naming_scheme,
88                    e_tag: None,
89                })
90            })
91            .transpose()
92        })
93    }
94
95    /// Put the manifest to the external store.
96    ///
97    /// The staging manifest has been written to `staging_path` on the object store.
98    /// This method should atomically claim the version and return the final manifest location.
99    ///
100    /// The default implementation uses put_if_not_exists and put_if_exists to
101    /// implement a staging-based workflow. Implementations that can write directly
102    /// (e.g., namespace-backed stores) should override this method.
103    #[allow(clippy::too_many_arguments)]
104    async fn put(
105        &self,
106        base_path: &Path,
107        version: u64,
108        staging_path: &Path,
109        size: u64,
110        e_tag: Option<String>,
111        object_store: &dyn OSObjectStore,
112        naming_scheme: ManifestNamingScheme,
113    ) -> Result<ManifestLocation> {
114        // Default implementation: staging-based workflow
115
116        // Step 1: Record staging path atomically
117        self.put_if_not_exists(
118            base_path.as_ref(),
119            version,
120            staging_path.as_ref(),
121            size,
122            e_tag.clone(),
123        )
124        .await?;
125
126        // Step 2: Copy staging to final path
127        let final_path = naming_scheme.manifest_path(base_path, version);
128        let copied = match copy_size_aware(object_store, staging_path, &final_path, size).await {
129            Ok(_) => true,
130            Err(ObjectStoreError::NotFound { .. }) => false,
131            Err(e) => return Err(e.into()),
132        };
133        if copied {
134            info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_CREATE, r#type=AUDIT_TYPE_MANIFEST, path = final_path.as_ref());
135        }
136
137        // A copy creates a new object whose metadata may differ from the source.
138        // Read the destination metadata before publishing the final path.
139        let final_meta = object_store.head(&final_path).await?;
140        let final_size = final_meta.size;
141        let final_e_tag = final_meta.e_tag;
142
143        let location = ManifestLocation {
144            version,
145            path: final_path.clone(),
146            size: Some(final_size),
147            naming_scheme,
148            e_tag: final_e_tag.clone(),
149        };
150
151        if !copied {
152            return Ok(location);
153        }
154
155        // Step 3: Update external store to final path
156        self.put_if_exists(
157            base_path.as_ref(),
158            version,
159            final_path.as_ref(),
160            final_size,
161            final_e_tag,
162        )
163        .await?;
164
165        // Step 4: Delete staging manifest
166        match object_store.delete(staging_path).await {
167            Ok(_) => {}
168            Err(ObjectStoreError::NotFound { .. }) => {}
169            Err(e) => return Err(e.into()),
170        }
171        info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = staging_path.as_ref());
172
173        Ok(location)
174    }
175
176    /// Put the manifest path for a given base_uri and version, should fail if the version already exists
177    async fn put_if_not_exists(
178        &self,
179        base_uri: &str,
180        version: u64,
181        path: &str,
182        size: u64,
183        e_tag: Option<String>,
184    ) -> Result<()>;
185
186    /// Put the manifest path for a given base_uri and version, should fail if the version **does not** already exist
187    async fn put_if_exists(
188        &self,
189        base_uri: &str,
190        version: u64,
191        path: &str,
192        size: u64,
193        e_tag: Option<String>,
194    ) -> Result<()>;
195
196    /// Delete the manifest information for given base_uri from the store
197    async fn delete(&self, _base_uri: &str) -> Result<()> {
198        Ok(())
199    }
200}
201
202pub(crate) fn detect_naming_scheme_from_path(path: &Path) -> Result<ManifestNamingScheme> {
203    path.filename()
204        .and_then(|name| {
205            ManifestNamingScheme::detect_scheme(name)
206                .or_else(|| Some(ManifestNamingScheme::detect_scheme_staging(name)))
207        })
208        .ok_or_else(|| {
209            Error::corrupt_file(
210                path.clone(),
211                "Path does not follow known manifest naming convention.",
212            )
213        })
214}
215
216/// The most conservative server-side-copy size limit across the object
217/// stores we support. This is not S3-specific: S3's `CopyObject` and GCS's
218/// single-shot `Objects: copy` both reject sources above ~5 GiB, so we use
219/// 5 GiB as a backend-agnostic threshold. Above it we stream the source
220/// through the client and re-upload via multipart instead of relying on a
221/// server-side copy. Stores that have no such cap (e.g. local filesystem)
222/// also take the fallback above this size — correctness is preserved; only
223/// the rare >5 GiB copy is slower than a native copy would be.
224const MAX_SERVER_SIDE_COPY_BYTES: u64 = 5 * 1024 * 1024 * 1024;
225
226/// Part size for the read+rewrite fallback. Multipart-capable stores
227/// (S3, GCS) require every part except the last to be ≥5 MB and allow up to
228/// 10,000 parts. 100 MB sits comfortably inside both bounds and keeps the
229/// part count low (~140 parts for a 14 GB manifest) without large per-part
230/// RAM.
231const COPY_REWRITE_PART_SIZE: usize = 100 * 1024 * 1024;
232
233/// Copy `from` to `to`, falling back to a multipart-equivalent read+rewrite
234/// when the source exceeds the server-side-copy size limit
235/// (`MAX_SERVER_SIDE_COPY_BYTES`).
236///
237/// For sources below the limit, this is the same fast server-side
238/// `store.copy()` as before. For larger sources, the source is streamed
239/// through the client and re-uploaded as a multipart upload at `to`. This
240/// doubles bytes-on-the-wire for the rare large case while preserving the
241/// cheap fast path for the common small case.
242///
243/// `size` is the known source size. It is required: the only caller already
244/// has it, and the alternative (an extra `head(from)` round-trip) is work
245/// the caller can avoid by passing what it already knows.
246///
247/// `NotFound` errors on `from` propagate unchanged so callers can keep
248/// existing `Err(NotFound { .. })` arms.
249///
250/// This is a workaround for the missing `UploadPartCopy` primitive in the
251/// upstream `object_store` crate. Once that lands, this helper can be
252/// deleted and the call sites can go back to plain `store.copy()`.
253async fn copy_size_aware(
254    store: &dyn OSObjectStore,
255    from: &Path,
256    to: &Path,
257    size: u64,
258) -> std::result::Result<(), ObjectStoreError> {
259    if size < MAX_SERVER_SIDE_COPY_BYTES {
260        store.copy(from, to).await
261    } else {
262        copy_via_read_rewrite(store, from, to).await
263    }
264}
265
266// NOTE: parts are uploaded sequentially. This could be parallelized (a
267// bounded JoinSet, like lance-io/src/object_writer.rs's
268// LANCE_UPLOAD_CONCURRENCY) or sidestepped entirely by switching to
269// `object_store::WriteMultipart` (which also handles abort-on-drop). Left
270// sequential here: this is a cold path (only >5 GiB manifests) and the
271// helper is itself a stopgap until `object_store` exposes UploadPartCopy.
272async fn copy_via_read_rewrite(
273    store: &dyn OSObjectStore,
274    from: &Path,
275    to: &Path,
276) -> std::result::Result<(), ObjectStoreError> {
277    // NotFound here propagates upward unchanged.
278    let mut stream = store.get(from).await?.into_stream();
279
280    // From here on, errors must `abort()` the upload to avoid leaving an
281    // orphan multipart upload on stores that support them (e.g. S3, GCS),
282    // which would otherwise incur storage charges until the bucket's
283    // lifecycle policy cleans it up.
284    //
285    // Note: this does NOT cover task cancellation — `MultipartUpload`'s
286    // upstream Drop is documented as a no-op for S3/GCS. Callers that
287    // need cancellation cleanliness should run this with a guard or
288    // switch to `object_store::WriteMultipart` (planned follow-up).
289    let mut upload = store.put_multipart(to).await?;
290    let mut part_buf: Vec<u8> = Vec::with_capacity(COPY_REWRITE_PART_SIZE);
291
292    while let Some(chunk) = stream.next().await {
293        let chunk = match chunk {
294            Ok(b) => b,
295            Err(e) => {
296                let _ = upload.abort().await;
297                return Err(e);
298            }
299        };
300        // Append the chunk in COPY_REWRITE_PART_SIZE-bounded slices so a
301        // single oversized chunk (e.g., LocalFileSystem returning a whole
302        // file) cannot push part_buf past the backend's per-part size limit
303        // (5 GiB on S3/GCS). COPY_REWRITE_PART_SIZE is well under every
304        // backend's cap, so each flushed part is always valid.
305        let mut offset = 0;
306        while offset < chunk.len() {
307            let want = COPY_REWRITE_PART_SIZE - part_buf.len();
308            let take = want.min(chunk.len() - offset);
309            part_buf.extend_from_slice(&chunk[offset..offset + take]);
310            offset += take;
311
312            if part_buf.len() >= COPY_REWRITE_PART_SIZE {
313                let payload =
314                    std::mem::replace(&mut part_buf, Vec::with_capacity(COPY_REWRITE_PART_SIZE));
315                if let Err(e) = upload.put_part(Bytes::from(payload).into()).await {
316                    let _ = upload.abort().await;
317                    return Err(e);
318                }
319            }
320        }
321    }
322
323    // Flush the final (possibly-short) part. The last part of a multipart
324    // upload is exempt from the per-part minimum on S3/GCS.
325    if !part_buf.is_empty()
326        && let Err(e) = upload.put_part(Bytes::from(part_buf).into()).await
327    {
328        let _ = upload.abort().await;
329        return Err(e);
330    }
331
332    if let Err(e) = upload.complete().await {
333        let _ = upload.abort().await;
334        return Err(e);
335    }
336    Ok(())
337}
338
339/// External manifest commit handler
340/// This handler is used to commit a manifest to an external store
341/// for detailed design, see <https://github.com/lance-format/lance/issues/1183>
342#[derive(Debug)]
343pub struct ExternalManifestCommitHandler {
344    pub external_manifest_store: Arc<dyn ExternalManifestStore>,
345}
346
347impl ExternalManifestCommitHandler {
348    async fn verify_finalized_manifest_location(
349        &self,
350        base_path: &Path,
351        location: ManifestLocation,
352        object_store: &dyn OSObjectStore,
353    ) -> std::result::Result<ManifestLocation, Error> {
354        match object_store.head(&location.path).await {
355            Ok(ObjectMeta { size, e_tag, .. }) => {
356                let ManifestLocation {
357                    version,
358                    path,
359                    size: expected_size,
360                    naming_scheme,
361                    e_tag: expected_e_tag,
362                } = location;
363
364                let size = match expected_size {
365                    Some(expected_size) if expected_size != size => {
366                        return Err(Error::corrupt_file(
367                            path,
368                            format!(
369                                "Manifest size mismatch for version {}: external store expected {}, object store returned {}",
370                                version, expected_size, size
371                            ),
372                        ));
373                    }
374                    Some(expected_size) => Some(expected_size),
375                    None => Some(size),
376                };
377
378                let e_tag = match expected_e_tag {
379                    Some(expected_e_tag) => {
380                        if e_tag.as_ref() != Some(&expected_e_tag) {
381                            return Err(Error::corrupt_file(
382                                path,
383                                format!(
384                                    "Manifest e_tag mismatch for version {}: external store expected {:?}, object store returned {:?}",
385                                    version, expected_e_tag, e_tag
386                                ),
387                            ));
388                        }
389                        Some(expected_e_tag)
390                    }
391                    None => e_tag,
392                };
393
394                Ok(ManifestLocation {
395                    version,
396                    path,
397                    size,
398                    naming_scheme,
399                    e_tag,
400                })
401            }
402            Err(ObjectStoreError::NotFound { .. }) => {
403                // The external store may hold a stale finalized V2 path while
404                // the object store still has the manifest at the V1 location.
405                default_resolve_version(base_path, location.version, object_store).await
406            }
407            Err(e) => Err(e.into()),
408        }
409    }
410
411    /// The manifest is considered committed once the staging manifest is written
412    /// to object store and that path is committed to the external store.
413    ///
414    /// However, to fully complete this, the staging manifest should be materialized
415    /// into the final path, the final path should be committed to the external store
416    /// and the staging manifest should be deleted. These steps may be completed
417    /// by any number of readers or writers, so care should be taken to ensure
418    /// that the manifest is not lost nor any errors occur due to duplicate
419    /// operations.
420    #[allow(clippy::too_many_arguments)]
421    async fn finalize_manifest(
422        &self,
423        base_path: &Path,
424        staging_manifest_path: &Path,
425        version: u64,
426        size: u64,
427        store: &dyn OSObjectStore,
428        naming_scheme: ManifestNamingScheme,
429    ) -> std::result::Result<ManifestLocation, Error> {
430        // step 1: copy the manifest to the final location
431        let final_manifest_path = naming_scheme.manifest_path(base_path, version);
432
433        let copied =
434            match copy_size_aware(store, staging_manifest_path, &final_manifest_path, size).await {
435                Ok(_) => true,
436                Err(ObjectStoreError::NotFound { .. }) => false, // Another writer beat us to it.
437                Err(e) => return Err(e.into()),
438            };
439        if copied {
440            info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_CREATE, r#type=AUDIT_TYPE_MANIFEST, path = final_manifest_path.as_ref());
441        }
442
443        // A copy creates a new object whose metadata may differ from the source.
444        // Read the destination metadata before publishing the final path.
445        let final_meta = store.head(&final_manifest_path).await?;
446        let final_size = final_meta.size;
447        let final_e_tag = final_meta.e_tag;
448
449        let location = ManifestLocation {
450            version,
451            path: final_manifest_path,
452            size: Some(final_size),
453            naming_scheme,
454            e_tag: final_e_tag,
455        };
456
457        if !copied {
458            return Ok(location);
459        }
460
461        // step 2: flip the external store to point to the final location
462        self.external_manifest_store
463            .put_if_exists(
464                base_path.as_ref(),
465                version,
466                location.path.as_ref(),
467                final_size,
468                location.e_tag.clone(),
469            )
470            .await?;
471
472        // step 3: delete the staging manifest
473        match store.delete(staging_manifest_path).await {
474            Ok(_) => {}
475            Err(ObjectStoreError::NotFound { .. }) => {}
476            Err(e) => return Err(e.into()),
477        }
478        info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = staging_manifest_path.as_ref());
479
480        Ok(location)
481    }
482}
483
484#[async_trait]
485impl CommitHandler for ExternalManifestCommitHandler {
486    async fn resolve_latest_location(
487        &self,
488        base_path: &Path,
489        object_store: &ObjectStore,
490    ) -> std::result::Result<ManifestLocation, Error> {
491        let location = self
492            .external_manifest_store
493            .get_latest_manifest_location(base_path.as_ref())
494            .await?;
495
496        match location {
497            Some(location) => {
498                if location.path.extension() == Some(MANIFEST_EXTENSION) {
499                    return self
500                        .verify_finalized_manifest_location(
501                            base_path,
502                            location,
503                            object_store.inner.as_ref(),
504                        )
505                        .await;
506                }
507
508                let ManifestLocation {
509                    version,
510                    path,
511                    size,
512                    naming_scheme,
513                    e_tag: _,
514                } = location;
515
516                let size = if let Some(size) = size {
517                    size
518                } else {
519                    match object_store.inner.head(&path).await {
520                        Ok(meta) => meta.size,
521                        Err(ObjectStoreError::NotFound { .. }) => {
522                            // there may be other threads that have finished executing finalize_manifest.
523                            let new_location = self
524                                .external_manifest_store
525                                .get_manifest_location(base_path.as_ref(), version)
526                                .await?;
527                            return Ok(new_location);
528                        }
529                        Err(e) => return Err(e.into()),
530                    }
531                };
532
533                let final_location = self
534                    .finalize_manifest(
535                        base_path,
536                        &path,
537                        version,
538                        size,
539                        &object_store.inner,
540                        naming_scheme,
541                    )
542                    .await?;
543
544                Ok(final_location)
545            }
546            // Dataset not found in the external store, this could be because the dataset did not
547            // use external store for commit before. In this case, we search for the latest manifest
548            None => current_manifest_path(object_store, base_path).await,
549        }
550    }
551
552    async fn resolve_version_location(
553        &self,
554        base_path: &Path,
555        version: u64,
556        object_store: &dyn OSObjectStore,
557    ) -> std::result::Result<ManifestLocation, Error> {
558        let location_res = self
559            .external_manifest_store
560            .get_manifest_location(base_path.as_ref(), version)
561            .await;
562
563        let location = match location_res {
564            Ok(p) => p,
565            // not board external manifest yet, direct to object store
566            Err(Error::NotFound { .. }) => {
567                let path = default_resolve_version(base_path, version, object_store)
568                    .await
569                    .map_err(|_| Error::not_found(format!("{}@{}", base_path, version)))?
570                    .path;
571                match object_store.head(&path).await {
572                    Ok(ObjectMeta { size, e_tag, .. }) => {
573                        let res = self
574                            .external_manifest_store
575                            .put_if_not_exists(
576                                base_path.as_ref(),
577                                version,
578                                path.as_ref(),
579                                size,
580                                e_tag.clone(),
581                            )
582                            .await;
583                        if let Err(e) = res {
584                            warn!(
585                                "could not update external manifest store during load, with error: {}",
586                                e
587                            );
588                        }
589                        let naming_scheme =
590                            ManifestNamingScheme::detect_scheme_staging(path.filename().unwrap());
591                        return Ok(ManifestLocation {
592                            version,
593                            path,
594                            size: Some(size),
595                            naming_scheme,
596                            e_tag,
597                        });
598                    }
599                    Err(ObjectStoreError::NotFound { .. }) => {
600                        return Err(Error::not_found(path.to_string()));
601                    }
602                    Err(e) => return Err(e.into()),
603                }
604            }
605            Err(e) => return Err(e),
606        };
607
608        if location.path.extension() == Some(MANIFEST_EXTENSION) {
609            return self
610                .verify_finalized_manifest_location(base_path, location, object_store)
611                .await;
612        }
613
614        let naming_scheme =
615            ManifestNamingScheme::detect_scheme_staging(location.path.filename().unwrap());
616
617        let size = if let Some(size) = location.size {
618            size
619        } else {
620            let meta = object_store.head(&location.path).await?;
621            meta.size
622        };
623
624        self.finalize_manifest(
625            base_path,
626            &location.path,
627            version,
628            size,
629            object_store,
630            naming_scheme,
631        )
632        .await
633    }
634
635    async fn version_exists(
636        &self,
637        base_path: &Path,
638        version: u64,
639        object_store: &dyn OSObjectStore,
640        naming_scheme: ManifestNamingScheme,
641    ) -> Result<bool> {
642        match self
643            .external_manifest_store
644            .get_manifest_location(base_path.as_ref(), version)
645            .await
646        {
647            Ok(_) => Ok(true),
648            Err(Error::NotFound { .. }) => {
649                let path = naming_scheme.manifest_path(base_path, version);
650                match object_store.head(&path).await {
651                    Ok(_) => Ok(true),
652                    Err(ObjectStoreError::NotFound { .. }) => Ok(false),
653                    Err(e) => Err(e.into()),
654                }
655            }
656            Err(e) => Err(e),
657        }
658    }
659
660    async fn commit(
661        &self,
662        manifest: &mut Manifest,
663        indices: Option<Vec<IndexMetadata>>,
664        base_path: &Path,
665        object_store: &ObjectStore,
666        manifest_writer: super::ManifestWriter,
667        naming_scheme: ManifestNamingScheme,
668        transaction: Option<Transaction>,
669    ) -> std::result::Result<ManifestLocation, CommitError> {
670        // path we get here is the path to the manifest we want to write
671        // use object_store.base_path.as_ref() for getting the root of the dataset
672
673        // step 1: Write the manifest we want to commit to object store with a temporary name
674        let path = naming_scheme.manifest_path(base_path, manifest.version);
675        let staging_path = make_staging_manifest_path(&path)?;
676        let write_res =
677            manifest_writer(object_store, manifest, indices, &staging_path, transaction).await?;
678
679        // step 2 & 3: Put the manifest to external store
680        let result = self
681            .external_manifest_store
682            .put(
683                base_path,
684                manifest.version,
685                &staging_path,
686                write_res.size as u64,
687                write_res.e_tag,
688                &object_store.inner,
689                naming_scheme,
690            )
691            .await;
692
693        match result {
694            Ok(location) => {
695                write_version_hint(object_store, base_path, manifest.version).await;
696                Ok(location)
697            }
698            Err(_) => {
699                // delete the staging manifest
700                match object_store.inner.delete(&staging_path).await {
701                    Ok(_) => {}
702                    Err(ObjectStoreError::NotFound { .. }) => {}
703                    Err(e) => return Err(CommitError::OtherError(e.into())),
704                }
705                info!(target: TRACE_FILE_AUDIT, mode=AUDIT_MODE_DELETE, r#type=AUDIT_TYPE_MANIFEST, path = staging_path.as_ref());
706                Err(CommitError::CommitConflict {})
707            }
708        }
709    }
710
711    async fn delete(&self, base_path: &Path) -> Result<()> {
712        self.external_manifest_store
713            .delete(base_path.as_ref())
714            .await
715    }
716}