use itertools::Itertools;
use unitycatalog_common::models::external_locations::v1::ExternalLocation;
use unitycatalog_common::models::tables::v1::Table;
use unitycatalog_common::models::volumes::v1::Volume;
use super::ProvidesLocalStoragePolicy;
use super::location::StorageLocationUrl;
use crate::api::staging_tables::MANAGED_STORAGE_PREFIX;
use crate::store::ResourceStore;
use crate::{Error, Result};
pub(crate) async fn find_external_location_for_url(
location: &StorageLocationUrl,
handler: &(impl ResourceStore + ?Sized),
) -> Result<ExternalLocation> {
let locations = list_external_locations(handler).await?;
locations
.into_iter()
.filter(|l| {
let Ok(ext_loc_url) = StorageLocationUrl::parse(&l.url) else {
return false;
};
is_path_prefix(location.raw().as_str(), ext_loc_url.raw().as_str())
|| is_path_prefix(
location.location().as_str(),
ext_loc_url.location().as_str(),
)
})
.max_by(|l, r| l.url.len().cmp(&r.url.len()))
.ok_or(Error::NotFound)
}
pub(crate) async fn list_external_locations(
handler: &(impl ResourceStore + ?Sized),
) -> Result<Vec<ExternalLocation>> {
let (locations, _) = handler
.list(
&unitycatalog_common::ObjectLabel::ExternalLocation,
None,
None,
None,
)
.await?;
Ok(locations.into_iter().map(|l| l.try_into()).try_collect()?)
}
pub(crate) async fn list_table_volume_locations(
handler: &(impl ResourceStore + ?Sized),
) -> Result<Vec<StorageLocationUrl>> {
let mut out = Vec::new();
let (tables, _) = handler
.list(&unitycatalog_common::ObjectLabel::Table, None, None, None)
.await?;
for resource in tables {
let table: Table = resource.try_into()?;
if let Some(loc) = table.storage_location.filter(|s| !s.is_empty())
&& let Ok(url) = StorageLocationUrl::parse(&loc)
{
out.push(url);
}
}
let (volumes, _) = handler
.list(&unitycatalog_common::ObjectLabel::Volume, None, None, None)
.await?;
for resource in volumes {
let volume: Volume = resource.try_into()?;
if !volume.storage_location.is_empty()
&& let Ok(url) = StorageLocationUrl::parse(&volume.storage_location)
{
out.push(url);
}
}
Ok(out)
}
pub(crate) async fn validate_external_storage_location(
handler: &(impl ResourceStore + ProvidesLocalStoragePolicy + ?Sized),
location: &StorageLocationUrl,
) -> Result<()> {
handler.local_storage_policy().check(location)?;
if path_contains_managed_segment(location) {
return Err(Error::invalid_argument(format!(
"storage location '{}' lies within a reserved managed-storage \
('{}') region; external tables and volumes must use a location \
outside managed storage",
location.raw(),
MANAGED_STORAGE_PREFIX,
)));
}
if let Err(Error::NotFound) = find_external_location_for_url(location, handler).await {
return Err(Error::invalid_argument(format!(
"storage location '{}' is not contained within any registered external location",
location.raw()
)));
}
for existing in list_table_volume_locations(handler).await? {
if locations_overlap(location, &existing) {
return Err(Error::invalid_argument(format!(
"storage location '{}' overlaps existing table or volume location '{}'",
location.raw(),
existing.raw()
)));
}
}
Ok(())
}
pub(crate) async fn validate_managed_storage_root(
handler: &(impl ResourceStore + ProvidesLocalStoragePolicy + ?Sized),
location: &StorageLocationUrl,
) -> Result<()> {
handler.local_storage_policy().check(location)?;
if path_contains_managed_segment(location) {
return Err(Error::invalid_argument(format!(
"managed storage_root '{}' must not lie within a reserved \
managed-storage ('{}') region",
location.raw(),
MANAGED_STORAGE_PREFIX,
)));
}
if let Err(Error::NotFound) = find_external_location_for_url(location, handler).await {
return Err(Error::invalid_argument(format!(
"managed storage_root '{}' is not contained within any registered external location",
location.raw()
)));
}
Ok(())
}
pub(crate) fn path_contains_managed_segment(location: &StorageLocationUrl) -> bool {
let has_segment = |url: &str| {
object_store::path::Path::from(url)
.parts()
.any(|part| part.as_ref() == MANAGED_STORAGE_PREFIX)
};
has_segment(location.raw().as_str()) || has_segment(location.location().as_str())
}
pub(crate) fn locations_overlap(a: &StorageLocationUrl, b: &StorageLocationUrl) -> bool {
paths_overlap(a.raw().as_str(), b.raw().as_str())
|| paths_overlap(a.location().as_str(), b.location().as_str())
}
pub(crate) fn paths_overlap(a: &str, b: &str) -> bool {
is_path_prefix(a, b) || is_path_prefix(b, a)
}
pub(crate) fn is_path_prefix(url: &str, prefix: &str) -> bool {
object_store::path::Path::from(url).prefix_matches(&object_store::path::Path::from(prefix))
}
#[cfg(test)]
mod tests {
use super::{StorageLocationUrl, is_path_prefix, locations_overlap, paths_overlap};
#[test]
fn path_prefix_matches_exact_and_subpaths() {
assert!(is_path_prefix("s3://bucket/data", "s3://bucket/data"));
assert!(is_path_prefix("s3://bucket/data/file", "s3://bucket/data"));
assert!(is_path_prefix("s3://bucket/data/x/y", "s3://bucket/data/"));
}
#[test]
fn path_prefix_rejects_sibling_prefix() {
assert!(!is_path_prefix(
"s3://bucket/data-secret/file",
"s3://bucket/data"
));
assert!(!is_path_prefix("s3://bucket/database", "s3://bucket/data"));
}
#[test]
fn paths_overlap_is_symmetric_on_nesting() {
assert!(paths_overlap("s3://b/data", "s3://b/data/sub"));
assert!(paths_overlap("s3://b/data/sub", "s3://b/data"));
assert!(paths_overlap("s3://b/data", "s3://b/data"));
}
#[test]
fn paths_overlap_rejects_non_nested() {
assert!(!paths_overlap("s3://b/data", "s3://b/data-secret"));
assert!(!paths_overlap("s3://b/data", "s3://b/other"));
assert!(!paths_overlap("s3://b/data", "s3://c/data"));
}
#[test]
fn managed_segment_is_detected_segment_aware() {
use super::path_contains_managed_segment;
let parse = |s| StorageLocationUrl::parse(s).unwrap();
assert!(path_contains_managed_segment(&parse(
"s3://b/uc/__unitystorage/catalogs/cid"
)));
assert!(path_contains_managed_segment(&parse(
"s3://b/__unitystorage/tables/tid"
)));
assert!(!path_contains_managed_segment(&parse(
"s3://b/my__unitystorage_data/x"
)));
assert!(!path_contains_managed_segment(&parse(
"s3://b/uc/external/x"
)));
}
#[test]
fn locations_overlap_via_storage_location_url() {
let parse = |s| StorageLocationUrl::parse(s).unwrap();
assert!(locations_overlap(
&parse("s3://b/data"),
&parse("s3://b/data/sub")
));
assert!(!locations_overlap(
&parse("s3://b/data"),
&parse("s3://b/data-secret")
));
}
}