blob: 8ab67e40d64a87574467a71a826ab94565e16a5d [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::borrow::Cow;
use std::collections::HashMap;
#[cfg(any(
feature = "storage-azdls",
feature = "storage-cos",
feature = "storage-gcs",
feature = "storage-oss",
feature = "storage-obs",
feature = "storage-s3",
feature = "storage-hdfs"
))]
use std::sync::Mutex;
#[cfg(any(
feature = "storage-azdls",
feature = "storage-cos",
feature = "storage-gcs",
feature = "storage-oss",
feature = "storage-obs",
feature = "storage-s3"
))]
use std::sync::MutexGuard;
#[cfg(feature = "storage-azdls")]
use super::AzdlsStorageConfig;
#[cfg(feature = "storage-cos")]
use opendal::services::CosConfig;
#[cfg(feature = "storage-gcs")]
use opendal::services::GcsConfig;
#[cfg(feature = "storage-hdfs")]
use opendal::services::HdfsNativeConfig;
#[cfg(feature = "storage-obs")]
use opendal::services::ObsConfig;
#[cfg(feature = "storage-oss")]
use opendal::services::OssConfig;
#[cfg(feature = "storage-s3")]
use opendal::services::S3Config;
use opendal::{Operator, Scheme};
#[cfg(any(
feature = "storage-cos",
feature = "storage-gcs",
feature = "storage-oss",
feature = "storage-obs",
feature = "storage-s3"
))]
use url::Url;
use crate::error;
use super::FileIOBuilder;
/// The storage carries all supported storage services in paimon
#[derive(Debug)]
pub enum Storage {
#[cfg(feature = "storage-memory")]
Memory { op: Operator },
#[cfg(feature = "storage-fs")]
LocalFs { op: Operator },
#[cfg(feature = "storage-oss")]
Oss {
config: Box<OssConfig>,
operators: Mutex<HashMap<String, Operator>>,
},
#[cfg(feature = "storage-s3")]
S3 {
config: Box<S3Config>,
operators: Mutex<HashMap<String, Operator>>,
},
#[cfg(feature = "storage-cos")]
Cos {
config: Box<CosConfig>,
operators: Mutex<HashMap<String, Operator>>,
},
#[cfg(feature = "storage-azdls")]
Azdls {
config: Box<AzdlsStorageConfig>,
operators: Mutex<HashMap<String, Operator>>,
},
#[cfg(feature = "storage-obs")]
Obs {
config: Box<ObsConfig>,
operators: Mutex<HashMap<String, Operator>>,
},
#[cfg(feature = "storage-gcs")]
Gcs {
config: Box<GcsConfig>,
operators: Mutex<HashMap<String, Operator>>,
},
#[cfg(feature = "storage-hdfs")]
Hdfs {
config: Box<HdfsNativeConfig>,
op: Mutex<Option<Operator>>,
},
}
impl Storage {
pub(crate) fn build(file_io_builder: FileIOBuilder) -> crate::Result<Self> {
let (scheme_str, props) = file_io_builder.into_parts();
let scheme = Self::parse_scheme(&scheme_str)?;
match scheme {
#[cfg(feature = "storage-memory")]
Scheme::Memory => Ok(Self::Memory {
op: super::memory_config_build()?,
}),
#[cfg(feature = "storage-fs")]
Scheme::Fs => Ok(Self::LocalFs {
op: super::fs_config_build()?,
}),
#[cfg(feature = "storage-oss")]
Scheme::Oss => {
let config = super::oss_config_parse(props)?;
Ok(Self::Oss {
config: Box::new(config),
operators: Mutex::new(HashMap::new()),
})
}
#[cfg(feature = "storage-s3")]
Scheme::S3 => {
let config = super::s3_config_parse(props)?;
Ok(Self::S3 {
config: Box::new(config),
operators: Mutex::new(HashMap::new()),
})
}
#[cfg(feature = "storage-cos")]
Scheme::Cos => {
let config = super::cos_config_parse(props)?;
Ok(Self::Cos {
config: Box::new(config),
operators: Mutex::new(HashMap::new()),
})
}
#[cfg(feature = "storage-azdls")]
Scheme::Azdls => {
let config = super::azdls_config_parse(props)?;
Ok(Self::Azdls {
config: Box::new(config),
operators: Mutex::new(HashMap::new()),
})
}
#[cfg(feature = "storage-obs")]
Scheme::Obs => {
let config = super::obs_config_parse(props)?;
Ok(Self::Obs {
config: Box::new(config),
operators: Mutex::new(HashMap::new()),
})
}
#[cfg(feature = "storage-gcs")]
Scheme::Gcs => {
let config = super::gcs_config_parse(props)?;
Ok(Self::Gcs {
config: Box::new(config),
operators: Mutex::new(HashMap::new()),
})
}
#[cfg(feature = "storage-hdfs")]
Scheme::HdfsNative => {
let config = super::hdfs_config_parse(props)?;
Ok(Self::Hdfs {
config: Box::new(config),
op: Mutex::new(None),
})
}
_ => Err(error::Error::IoUnsupported {
message: "Unsupported storage feature".to_string(),
}),
}
}
pub(crate) fn create<'a>(&self, path: &'a str) -> crate::Result<(Operator, Cow<'a, str>)> {
match self {
#[cfg(feature = "storage-memory")]
Storage::Memory { op } => {
Ok((op.clone(), Cow::Borrowed(Self::memory_relative_path(path)?)))
}
#[cfg(feature = "storage-fs")]
Storage::LocalFs { op } => Ok((op.clone(), Self::fs_relative_path(path)?)),
#[cfg(feature = "storage-oss")]
Storage::Oss { config, operators } => {
let (bucket, relative_path) =
Self::bucket_and_relative_path(path, "OSS", &["oss"])?;
let op = Self::cached_oss_operator(config, operators, path, &bucket)?;
Ok((op, Cow::Borrowed(relative_path)))
}
#[cfg(feature = "storage-s3")]
Storage::S3 { config, operators } => {
let (bucket, relative_path) =
Self::bucket_and_relative_path(path, "S3", &["s3", "s3a"])?;
let op = Self::cached_s3_operator(config, operators, path, &bucket)?;
Ok((op, Cow::Borrowed(relative_path)))
}
#[cfg(feature = "storage-cos")]
Storage::Cos { config, operators } => {
let (bucket, relative_path) =
Self::bucket_and_relative_path(path, "COS", &["cos", "cosn"])?;
let op = Self::cached_operator(operators, "COS", &bucket, || {
super::cos_config_build(config, path)
})?;
Ok((op, Cow::Borrowed(relative_path)))
}
#[cfg(feature = "storage-azdls")]
Storage::Azdls { config, operators } => {
let relative_path = super::azdls_relative_path(path)?;
let cache_key = super::azdls_operator_cache_key(config, path)?;
let op = Self::cached_operator(operators, "Azure", &cache_key, || {
super::azdls_config_build(config, path)
})?;
Ok((op, Cow::Borrowed(relative_path)))
}
#[cfg(feature = "storage-obs")]
Storage::Obs { config, operators } => {
let (bucket, relative_path) =
Self::bucket_and_relative_path(path, "OBS", &["obs"])?;
let op = Self::cached_operator(operators, "OBS", &bucket, || {
super::obs_config_build(config, path)
})?;
Ok((op, Cow::Borrowed(relative_path)))
}
#[cfg(feature = "storage-gcs")]
Storage::Gcs { config, operators } => {
let (bucket, relative_path) =
Self::bucket_and_relative_path(path, "GCS", &["gcs", "gs"])?;
let op = Self::cached_operator(operators, "GCS", &bucket, || {
super::gcs_config_build(config, path)
})?;
Ok((op, Cow::Borrowed(relative_path)))
}
#[cfg(feature = "storage-hdfs")]
Storage::Hdfs { config, op } => {
let relative_path = super::hdfs_relative_path(path)?;
let mut guard = op.lock().map_err(|_| error::Error::UnexpectedError {
message: "Failed to lock HDFS operator".to_string(),
source: None,
})?;
// HDFS uses a single operator per Storage instance (unlike S3/OSS
// which cache per bucket). The operator is lazily initialized from
// the first path's NameNode if not set in config. One FileIO
// instance should target exactly one HDFS cluster.
if guard.is_none() {
*guard = Some(super::hdfs_config_build(config, path)?);
}
Ok((
guard.as_ref().unwrap().clone(),
Cow::Borrowed(relative_path),
))
}
}
}
#[cfg(feature = "storage-memory")]
fn memory_relative_path(path: &str) -> crate::Result<&str> {
if let Some(stripped) = path.strip_prefix("memory:/") {
Ok(stripped)
} else {
path.get(1..).ok_or_else(|| error::Error::ConfigInvalid {
message: format!("Invalid memory path: {path}"),
})
}
}
/// Turn an absolute local path into the relative path that opendal's `fs`
/// service joins onto its `/` root.
///
/// On POSIX an absolute path `/tmp/wh` becomes `tmp/wh`: dropping the single
/// leading separator lets opendal rebuild `/tmp/wh` from its `/` root.
///
/// A bare drop-the-first-char would corrupt a Windows path such as
/// `C:\dir` into `:\dir` (the drive letter is lost — the historical source
/// of the "invalid filename" failures on Windows). Instead we keep the
/// drive specifier and only normalize separators to `/`, mirroring how Java
/// Paimon's `Path` (modeled on Hadoop's) handles Windows paths. opendal then
/// does `PathBuf::from("/").join("C:/dir")`, and because the argument
/// carries a drive prefix `Path::join` replaces the base, yielding the real
/// `C:\dir` on Windows.
#[cfg(feature = "storage-fs")]
fn fs_relative_path(path: &str) -> crate::Result<Cow<'_, str>> {
// A `file://` / `file:/` URL is already in scheme-relative form.
if let Some(stripped) = path.strip_prefix("file:/") {
return Ok(if stripped.contains('\\') {
Cow::Owned(stripped.replace('\\', "/"))
} else {
Cow::Borrowed(stripped)
});
}
if super::looks_like_windows_drive_path(path) {
return Ok(Cow::Owned(path.replace('\\', "/")));
}
path.get(1..)
.map(Cow::Borrowed)
.ok_or_else(|| error::Error::ConfigInvalid {
message: format!("Invalid file path: {path}"),
})
}
#[cfg(any(
feature = "storage-cos",
feature = "storage-gcs",
feature = "storage-obs",
feature = "storage-oss",
feature = "storage-s3"
))]
fn bucket_and_relative_path<'a>(
path: &'a str,
storage_name: &str,
allowed_schemes: &[&str],
) -> crate::Result<(String, &'a str)> {
let url = Url::parse(path).map_err(|_| error::Error::ConfigInvalid {
message: format!("Invalid {storage_name} url: {path}"),
})?;
let bucket = url
.host_str()
.ok_or_else(|| error::Error::ConfigInvalid {
message: format!("Invalid {storage_name} url: {path}, missing bucket"),
})?
.to_string();
let scheme = url.scheme();
if !allowed_schemes.contains(&scheme) {
return Err(error::Error::ConfigInvalid {
message: format!("Invalid {storage_name} url: {path}, unsupported scheme {scheme}"),
});
}
let prefix = format!("{scheme}://{bucket}/");
let relative_path =
path.strip_prefix(&prefix)
.ok_or_else(|| error::Error::ConfigInvalid {
message: format!(
"Invalid {storage_name} url: {path}, should start with {prefix}"
),
})?;
Ok((bucket, relative_path))
}
#[cfg(any(
feature = "storage-azdls",
feature = "storage-cos",
feature = "storage-gcs",
feature = "storage-oss",
feature = "storage-obs",
feature = "storage-s3"
))]
fn lock_operator_cache<'a>(
operators: &'a Mutex<HashMap<String, Operator>>,
storage_name: &str,
) -> crate::Result<MutexGuard<'a, HashMap<String, Operator>>> {
operators.lock().map_err(|_| error::Error::UnexpectedError {
message: format!("Failed to lock {storage_name} operator cache"),
source: None,
})
}
#[cfg(any(
feature = "storage-azdls",
feature = "storage-cos",
feature = "storage-gcs",
feature = "storage-oss",
feature = "storage-obs",
feature = "storage-s3"
))]
fn cached_operator(
operators: &Mutex<HashMap<String, Operator>>,
storage_name: &str,
cache_key: &str,
build: impl FnOnce() -> crate::Result<Operator>,
) -> crate::Result<Operator> {
let mut operators = Self::lock_operator_cache(operators, storage_name)?;
if let Some(op) = operators.get(cache_key) {
return Ok(op.clone());
}
let op = build()?;
operators.insert(cache_key.to_string(), op.clone());
Ok(op)
}
#[cfg(feature = "storage-oss")]
fn cached_oss_operator(
config: &OssConfig,
operators: &Mutex<HashMap<String, Operator>>,
path: &str,
bucket: &str,
) -> crate::Result<Operator> {
Self::cached_operator(operators, "OSS", bucket, || {
super::oss_config_build(config, path)
})
}
#[cfg(feature = "storage-s3")]
fn cached_s3_operator(
config: &S3Config,
operators: &Mutex<HashMap<String, Operator>>,
path: &str,
bucket: &str,
) -> crate::Result<Operator> {
Self::cached_operator(operators, "S3", bucket, || {
super::s3_config_build(config, path)
})
}
fn parse_scheme(scheme: &str) -> crate::Result<Scheme> {
match scheme {
"memory" => Ok(Scheme::Memory),
"file" | "" => Ok(Scheme::Fs),
"s3" | "s3a" => Ok(Scheme::S3),
"cosn" => Ok(Scheme::Cos),
"abfs" | "abfss" | "az" | "azure" => Ok(Scheme::Azdls),
"gs" => Ok(Scheme::Gcs),
"hdfs" => Ok(Scheme::HdfsNative),
s => Ok(s.parse::<Scheme>()?),
}
}
}
#[cfg(all(test, feature = "storage-fs"))]
mod fs_relative_path_tests {
use super::Storage;
fn rel(path: &str) -> String {
Storage::fs_relative_path(path).unwrap().into_owned()
}
#[test]
fn posix_absolute_path_drops_leading_separator() {
// opendal joins the result onto its `/` root, rebuilding `/tmp/wh`.
assert_eq!(rel("/tmp/wh"), "tmp/wh");
assert_eq!(rel("/tmp/wh/db.db/t"), "tmp/wh/db.db/t");
}
#[test]
fn file_scheme_is_stripped() {
assert_eq!(rel("file:/tmp/wh"), "tmp/wh");
// `file://` keeps the leading authority slash, matching prior behavior.
assert_eq!(rel("file:///tmp/wh"), "//tmp/wh");
}
#[test]
fn windows_drive_path_keeps_drive_and_normalizes_separators() {
// The historical bug dropped the drive letter (`C:\wh` -> `:\wh`); we
// must keep it and only switch `\` to `/` so opendal's
// `PathBuf::from("/").join(..)` rebuilds the real `C:\wh` on Windows.
assert_eq!(rel(r"C:\Users\wh"), "C:/Users/wh");
assert_eq!(rel("C:/Users/wh"), "C:/Users/wh");
assert_eq!(rel(r"D:\a\b\c"), "D:/a/b/c");
}
#[test]
fn windows_mixed_separators_are_normalized() {
// make_path concatenates with `/`, so a Windows warehouse yields a
// mixed-separator path that must still normalize cleanly.
assert_eq!(rel(r"C:\Users\wh/db.db/t"), "C:/Users/wh/db.db/t");
}
}