| // 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. |
| |
| #![doc = include_str!("../README.md")] |
| #![cfg_attr(docsrs, feature(doc_cfg))] |
| #![cfg_attr(docsrs, doc(auto_cfg))] |
| #![deny(missing_docs)] |
| use std::fmt::Debug; |
| use std::fmt::Formatter; |
| use std::sync::Arc; |
| |
| use globset::Glob; |
| use globset::GlobSet; |
| use globset::GlobSetBuilder; |
| use opendal_core::raw::*; |
| use opendal_core::*; |
| |
| /// `RouteLayer` routes operations to different operators by matching paths |
| /// against glob patterns. |
| /// |
| /// Routes are evaluated in insertion order, and the first matching route wins. |
| /// An unmatched path uses the operator wrapped by this layer. Copy and rename |
| /// operations select a route from the source path, so this layer does not move |
| /// data between two routed services. A batched delete routes each path |
| /// independently. |
| /// |
| /// [`OperatorInfo::capability`] continues to describe the wrapped operator; it |
| /// does not aggregate the capabilities of route targets. |
| /// |
| /// # Example |
| /// |
| /// ```no_run |
| /// use opendal_core::services::Memory; |
| /// use opendal_core::{Operator, Result}; |
| /// use opendal_layer_route::RouteLayer; |
| /// |
| /// fn build_operator() -> Result<Operator> { |
| /// let archive = Operator::new(Memory::default())?; |
| /// let routes = RouteLayer::builder() |
| /// .route("archive/**", archive) |
| /// .build()?; |
| /// |
| /// Ok(Operator::new(Memory::default())?.layer(routes)) |
| /// } |
| /// ``` |
| #[derive(Clone, Debug)] |
| pub struct RouteLayer { |
| router: Arc<RouteRouter>, |
| } |
| |
| impl RouteLayer { |
| /// Create a builder for `RouteLayer`. |
| pub fn builder() -> RouteLayerBuilder { |
| RouteLayerBuilder::default() |
| } |
| } |
| |
| /// Builder for `RouteLayer`. |
| #[derive(Default)] |
| pub struct RouteLayerBuilder { |
| routes: Vec<RouteEntry>, |
| } |
| |
| impl RouteLayerBuilder { |
| /// Add a route for paths that match `pattern`. |
| /// |
| /// If multiple patterns match, the route added first wins. The target |
| /// operator contributes both its service and its operation context. |
| pub fn route(mut self, pattern: impl AsRef<str>, op: Operator) -> Self { |
| let (ctx, srv) = op.into_parts(); |
| self.routes.push(RouteEntry { |
| pattern: pattern.as_ref().to_string(), |
| target: RouteTarget { srv, ctx }, |
| }); |
| self |
| } |
| |
| /// Build the route layer. |
| /// |
| /// This method returns [`ErrorKind::ConfigInvalid`] if a glob pattern is |
| /// invalid. |
| pub fn build(self) -> Result<RouteLayer> { |
| let mut builder = GlobSetBuilder::new(); |
| let mut targets = Vec::with_capacity(self.routes.len()); |
| |
| for entry in self.routes { |
| let glob = Glob::new(&entry.pattern).map_err(|err| { |
| Error::new(ErrorKind::ConfigInvalid, "invalid route glob pattern") |
| .with_context("pattern", entry.pattern.clone()) |
| .with_context("source", err.to_string()) |
| })?; |
| builder.add(glob); |
| targets.push(entry.target); |
| } |
| |
| let glob = builder.build().map_err(|err| { |
| Error::new(ErrorKind::ConfigInvalid, "failed to build route glob set") |
| .with_context("source", err.to_string()) |
| })?; |
| |
| Ok(RouteLayer { |
| router: Arc::new(RouteRouter { glob, targets }), |
| }) |
| } |
| } |
| |
| struct RouteEntry { |
| pattern: String, |
| target: RouteTarget, |
| } |
| |
| #[derive(Debug)] |
| struct RouteRouter { |
| glob: GlobSet, |
| targets: Vec<RouteTarget>, |
| } |
| |
| #[derive(Clone, Debug)] |
| struct RouteTarget { |
| srv: Servicer, |
| ctx: OperationContext, |
| } |
| |
| enum RouteSelected { |
| Default(Servicer), |
| Target(RouteTarget), |
| } |
| |
| impl RouteRouter { |
| fn match_index(&self, path: &str) -> Option<usize> { |
| self.glob.matches(path).into_iter().min() |
| } |
| |
| fn select(&self, path: &str, default: &Servicer) -> RouteSelected { |
| self.match_index(path) |
| .and_then(|idx| self.targets.get(idx).cloned()) |
| .map(RouteSelected::Target) |
| .unwrap_or_else(|| RouteSelected::Default(default.clone())) |
| } |
| |
| fn target(&self, idx: usize) -> Option<RouteTarget> { |
| self.targets.get(idx).cloned() |
| } |
| } |
| |
| impl Layer for RouteLayer { |
| fn apply_service(&self, inner: Servicer) -> Servicer { |
| Arc::new(self.layer(inner)) |
| } |
| } |
| |
| impl RouteLayer { |
| fn layer(&self, inner: Servicer) -> RouteAccessor { |
| RouteAccessor { |
| inner: Arc::new(inner), |
| router: self.router.clone(), |
| } |
| } |
| } |
| |
| #[doc(hidden)] |
| pub struct RouteAccessor { |
| inner: Servicer, |
| router: Arc<RouteRouter>, |
| } |
| |
| impl Debug for RouteAccessor { |
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| self.inner.fmt(f) |
| } |
| } |
| |
| impl RouteAccessor { |
| fn select(&self, path: &str) -> RouteSelected { |
| self.router.select(path, &self.inner) |
| } |
| } |
| |
| impl Service for RouteAccessor { |
| type Reader = oio::Reader; |
| type Writer = oio::Writer; |
| type Lister = oio::Lister; |
| type Deleter = oio::Deleter; |
| type Copier = oio::Copier; |
| |
| fn info(&self) -> ServiceInfo { |
| self.inner.info() |
| } |
| |
| fn capability(&self) -> Capability { |
| self.inner.capability() |
| } |
| |
| async fn create_dir( |
| &self, |
| ctx: &OperationContext, |
| path: &str, |
| args: OpCreateDir, |
| ) -> Result<RpCreateDir> { |
| match self.select(path) { |
| RouteSelected::Default(srv) => srv.create_dir(ctx, path, args).await, |
| RouteSelected::Target(target) => target.srv.create_dir(&target.ctx, path, args).await, |
| } |
| } |
| |
| fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<oio::Reader> { |
| match self.select(path) { |
| RouteSelected::Default(srv) => srv.read(ctx, path, args), |
| RouteSelected::Target(target) => target.srv.read(&target.ctx, path, args), |
| } |
| } |
| |
| fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<oio::Writer> { |
| match self.select(path) { |
| RouteSelected::Default(srv) => srv.write(ctx, path, args), |
| RouteSelected::Target(target) => target.srv.write(&target.ctx, path, args), |
| } |
| } |
| |
| fn copy( |
| &self, |
| ctx: &OperationContext, |
| from: &str, |
| to: &str, |
| args: OpCopy, |
| opts: OpCopier, |
| ) -> Result<oio::Copier> { |
| match self.select(from) { |
| RouteSelected::Default(srv) => srv.copy(ctx, from, to, args, opts), |
| RouteSelected::Target(target) => target.srv.copy(&target.ctx, from, to, args, opts), |
| } |
| } |
| |
| async fn rename( |
| &self, |
| ctx: &OperationContext, |
| from: &str, |
| to: &str, |
| args: OpRename, |
| ) -> Result<RpRename> { |
| match self.select(from) { |
| RouteSelected::Default(srv) => srv.rename(ctx, from, to, args).await, |
| RouteSelected::Target(target) => target.srv.rename(&target.ctx, from, to, args).await, |
| } |
| } |
| |
| async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> { |
| match self.select(path) { |
| RouteSelected::Default(srv) => srv.stat(ctx, path, args).await, |
| RouteSelected::Target(target) => target.srv.stat(&target.ctx, path, args).await, |
| } |
| } |
| |
| fn delete(&self, ctx: &OperationContext) -> Result<oio::Deleter> { |
| Ok(Box::new(RouteDeleter::new( |
| self.inner.clone(), |
| self.router.clone(), |
| ctx.clone(), |
| )) as oio::Deleter) |
| } |
| |
| fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<oio::Lister> { |
| match self.select(path) { |
| RouteSelected::Default(srv) => srv.list(ctx, path, args), |
| RouteSelected::Target(target) => target.srv.list(&target.ctx, path, args), |
| } |
| } |
| |
| async fn presign( |
| &self, |
| ctx: &OperationContext, |
| path: &str, |
| args: OpPresign, |
| ) -> Result<RpPresign> { |
| match self.select(path) { |
| RouteSelected::Default(srv) => srv.presign(ctx, path, args).await, |
| RouteSelected::Target(target) => target.srv.presign(&target.ctx, path, args).await, |
| } |
| } |
| } |
| |
| #[doc(hidden)] |
| pub struct RouteDeleter { |
| default: Servicer, |
| router: Arc<RouteRouter>, |
| ctx: OperationContext, |
| default_deleter: Option<oio::Deleter>, |
| target_deleters: Vec<Option<oio::Deleter>>, |
| } |
| |
| impl RouteDeleter { |
| fn new(default: Servicer, router: Arc<RouteRouter>, ctx: OperationContext) -> Self { |
| let mut target_deleters = Vec::with_capacity(router.targets.len()); |
| target_deleters.resize_with(router.targets.len(), || None); |
| Self { |
| default, |
| router, |
| ctx, |
| default_deleter: None, |
| target_deleters, |
| } |
| } |
| |
| async fn get_deleter(&mut self, key: RouteKey) -> Result<&mut oio::Deleter> { |
| match key { |
| RouteKey::Default => { |
| if self.default_deleter.is_none() { |
| let deleter = self.default.delete(&self.ctx)?; |
| self.default_deleter = Some(deleter); |
| } |
| Ok(self |
| .default_deleter |
| .as_mut() |
| .expect("default deleter must exist")) |
| } |
| RouteKey::Target(idx) => { |
| if idx >= self.target_deleters.len() { |
| if self.default_deleter.is_none() { |
| let deleter = self.default.delete(&self.ctx)?; |
| self.default_deleter = Some(deleter); |
| } |
| return Ok(self |
| .default_deleter |
| .as_mut() |
| .expect("default deleter must exist")); |
| } |
| if self.target_deleters[idx].is_none() { |
| let Some(target) = self.router.target(idx) else { |
| let deleter = self.default.delete(&self.ctx)?; |
| self.default_deleter = Some(deleter); |
| return Ok(self |
| .default_deleter |
| .as_mut() |
| .expect("default deleter must exist")); |
| }; |
| let deleter = target.srv.delete(&target.ctx)?; |
| self.target_deleters[idx] = Some(deleter); |
| } |
| Ok(self.target_deleters[idx] |
| .as_mut() |
| .expect("target deleter must exist")) |
| } |
| } |
| } |
| } |
| |
| impl oio::Delete for RouteDeleter { |
| async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> { |
| let key = match self.router.match_index(path) { |
| Some(idx) => RouteKey::Target(idx), |
| None => RouteKey::Default, |
| }; |
| let deleter = self.get_deleter(key).await?; |
| deleter.delete(path, args).await |
| } |
| |
| async fn close(&mut self) -> Result<()> { |
| let mut first_err = None; |
| if let Some(deleter) = self.default_deleter.as_mut() |
| && let Err(err) = deleter.close().await |
| { |
| first_err = Some(err); |
| } |
| for deleter in self.target_deleters.iter_mut().flatten() { |
| if let Err(err) = deleter.close().await |
| && first_err.is_none() |
| { |
| first_err = Some(err); |
| } |
| } |
| |
| match first_err { |
| Some(err) => Err(err), |
| None => Ok(()), |
| } |
| } |
| } |
| |
| #[derive(Debug, Hash, PartialEq, Eq)] |
| enum RouteKey { |
| Default, |
| Target(usize), |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use std::collections::HashSet; |
| use std::path::Path; |
| use std::path::PathBuf; |
| use std::sync::Mutex; |
| use std::time::SystemTime; |
| use std::time::UNIX_EPOCH; |
| |
| use opendal_service_fs::Fs; |
| |
| use super::*; |
| |
| fn build_memory_operator() -> Result<Operator> { |
| Operator::new(services::Memory::default()) |
| } |
| |
| fn build_mock_operator(name: &'static str) -> Operator { |
| Operator::from_parts( |
| OperationContext::default(), |
| Arc::new(MockService { |
| info: ServiceInfo::new("mock", "", name), |
| paths: Arc::new(Mutex::new(HashSet::new())), |
| }), |
| ) |
| } |
| |
| async fn build_fs_operator(label: &str) -> Result<(Operator, PathBuf)> { |
| let root = fs_root(label); |
| tokio::fs::create_dir_all(&root) |
| .await |
| .map_err(new_std_io_error)?; |
| let op = Operator::new(Fs::default().root(&root.to_string_lossy()))?; |
| Ok((op, root)) |
| } |
| |
| fn fs_root(label: &str) -> PathBuf { |
| let nanos = SystemTime::now() |
| .duration_since(UNIX_EPOCH) |
| .expect("system time before UNIX_EPOCH") |
| .as_nanos(); |
| let mut root = std::env::temp_dir(); |
| root.push(format!( |
| "opendal-route-{label}-{nanos}-{}", |
| std::process::id() |
| )); |
| root |
| } |
| |
| async fn cleanup_fs_root(root: &Path) { |
| let _ = tokio::fs::remove_dir_all(root).await; |
| } |
| |
| async fn assert_missing(op: &Operator, path: &str) -> Result<()> { |
| let err = op.stat(path).await.unwrap_err(); |
| assert_eq!(err.kind(), ErrorKind::NotFound); |
| Ok(()) |
| } |
| |
| #[derive(Debug)] |
| struct MockService { |
| info: ServiceInfo, |
| paths: Arc<Mutex<HashSet<String>>>, |
| } |
| |
| impl Service for MockService { |
| type Reader = (); |
| type Writer = MockWriter; |
| type Lister = (); |
| type Deleter = (); |
| type Copier = (); |
| |
| fn info(&self) -> ServiceInfo { |
| self.info.clone() |
| } |
| |
| fn capability(&self) -> Capability { |
| Capability { |
| stat: true, |
| write: true, |
| copy: true, |
| rename: true, |
| ..Default::default() |
| } |
| } |
| |
| async fn create_dir( |
| &self, |
| _: &OperationContext, |
| _: &str, |
| _: OpCreateDir, |
| ) -> Result<RpCreateDir> { |
| Err(Error::new( |
| ErrorKind::Unsupported, |
| "operation is not supported", |
| )) |
| } |
| |
| async fn stat(&self, _: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> { |
| if self.paths.lock().unwrap().contains(path) { |
| Ok(RpStat::new(Metadata::new(EntryMode::FILE))) |
| } else { |
| Err(Error::new(ErrorKind::NotFound, "path not found")) |
| } |
| } |
| |
| fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> { |
| Err(Error::new( |
| ErrorKind::Unsupported, |
| "operation is not supported", |
| )) |
| } |
| |
| fn write(&self, _: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> { |
| Ok(MockWriter { |
| paths: self.paths.clone(), |
| path: path.to_string(), |
| }) |
| } |
| |
| fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> { |
| Err(Error::new( |
| ErrorKind::Unsupported, |
| "operation is not supported", |
| )) |
| } |
| |
| fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> { |
| Err(Error::new( |
| ErrorKind::Unsupported, |
| "operation is not supported", |
| )) |
| } |
| |
| fn copy( |
| &self, |
| _: &OperationContext, |
| from: &str, |
| to: &str, |
| _: OpCopy, |
| _: OpCopier, |
| ) -> Result<Self::Copier> { |
| if !self.paths.lock().unwrap().contains(from) { |
| return Err(Error::new(ErrorKind::NotFound, "source not found")); |
| } |
| self.paths.lock().unwrap().insert(to.to_string()); |
| Ok(()) |
| } |
| |
| async fn rename( |
| &self, |
| _: &OperationContext, |
| from: &str, |
| to: &str, |
| _: OpRename, |
| ) -> Result<RpRename> { |
| let mut paths = self.paths.lock().unwrap(); |
| if !paths.remove(from) { |
| return Err(Error::new(ErrorKind::NotFound, "source not found")); |
| } |
| paths.insert(to.to_string()); |
| Ok(RpRename::default()) |
| } |
| |
| async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> { |
| Err(Error::new( |
| ErrorKind::Unsupported, |
| "operation is not supported", |
| )) |
| } |
| } |
| |
| struct MockWriter { |
| paths: Arc<Mutex<HashSet<String>>>, |
| path: String, |
| } |
| |
| impl oio::Write for MockWriter { |
| async fn write(&mut self, _: Buffer) -> Result<()> { |
| Ok(()) |
| } |
| |
| async fn close(&mut self) -> Result<Metadata> { |
| self.paths.lock().unwrap().insert(self.path.clone()); |
| Ok(Metadata::new(EntryMode::FILE)) |
| } |
| |
| async fn abort(&mut self) -> Result<()> { |
| Ok(()) |
| } |
| } |
| |
| #[tokio::test] |
| async fn test_first_match_wins() -> Result<()> { |
| let default_op = build_memory_operator()?; |
| let (fast_op, fast_root) = build_fs_operator("first-match-fast").await?; |
| let slow_op = build_memory_operator()?; |
| |
| let routed = default_op.clone().layer( |
| RouteLayer::builder() |
| .route("data/*.txt", fast_op.clone()) |
| .route("data/**", slow_op.clone()) |
| .build()?, |
| ); |
| |
| routed.write("data/file.txt", "v").await?; |
| |
| assert!(fast_op.stat("data/file.txt").await.is_ok()); |
| assert_missing(&slow_op, "data/file.txt").await?; |
| assert_missing(&default_op, "data/file.txt").await?; |
| |
| cleanup_fs_root(&fast_root).await; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_fallback_to_default() -> Result<()> { |
| let default_op = build_memory_operator()?; |
| let (hot_op, hot_root) = build_fs_operator("fallback-hot").await?; |
| |
| let routed = default_op.clone().layer( |
| RouteLayer::builder() |
| .route("hot/**", hot_op.clone()) |
| .build()?, |
| ); |
| |
| routed.write("cold/file.txt", "v").await?; |
| |
| assert!(default_op.stat("cold/file.txt").await.is_ok()); |
| assert_missing(&hot_op, "cold/file.txt").await?; |
| |
| cleanup_fs_root(&hot_root).await; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_copy_and_rename_route_by_from() -> Result<()> { |
| let default_op = build_mock_operator("default"); |
| let hot_op = build_mock_operator("hot"); |
| |
| let routed = default_op.clone().layer( |
| RouteLayer::builder() |
| .route("hot/**", hot_op.clone()) |
| .build()?, |
| ); |
| |
| routed.write("hot/src.txt", "v").await?; |
| routed.copy("hot/src.txt", "cold/copied.txt").await?; |
| |
| assert!(hot_op.stat("hot/src.txt").await.is_ok()); |
| assert!(hot_op.stat("cold/copied.txt").await.is_ok()); |
| assert_missing(&default_op, "cold/copied.txt").await?; |
| |
| routed.write("hot/src2.txt", "v").await?; |
| routed.rename("hot/src2.txt", "cold/renamed.txt").await?; |
| |
| assert_missing(&hot_op, "hot/src2.txt").await?; |
| assert!(hot_op.stat("cold/renamed.txt").await.is_ok()); |
| assert_missing(&default_op, "cold/renamed.txt").await?; |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_delete_iter_routes_per_path() -> Result<()> { |
| let default_op = build_memory_operator()?; |
| let (hot_op, hot_root) = build_fs_operator("delete-hot").await?; |
| |
| let routed = default_op.clone().layer( |
| RouteLayer::builder() |
| .route("hot/**", hot_op.clone()) |
| .build()?, |
| ); |
| |
| routed.write("hot/a.txt", "v").await?; |
| routed.write("cold/b.txt", "v").await?; |
| |
| routed.delete_iter(["hot/a.txt", "cold/b.txt"]).await?; |
| |
| assert_missing(&hot_op, "hot/a.txt").await?; |
| assert_missing(&default_op, "cold/b.txt").await?; |
| |
| cleanup_fs_root(&hot_root).await; |
| Ok(()) |
| } |
| } |