Skip to main content

opendal_layer_route/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22use std::fmt::Debug;
23use std::fmt::Formatter;
24use std::sync::Arc;
25
26use globset::Glob;
27use globset::GlobSet;
28use globset::GlobSetBuilder;
29use opendal_core::raw::*;
30use opendal_core::*;
31
32/// `RouteLayer` routes operations to different operators by matching paths
33/// against glob patterns.
34///
35/// Routes are evaluated in insertion order, and the first matching route wins.
36/// An unmatched path uses the operator wrapped by this layer. Copy and rename
37/// operations select a route from the source path, so this layer does not move
38/// data between two routed services. A batched delete routes each path
39/// independently.
40///
41/// [`OperatorInfo::capability`] continues to describe the wrapped operator; it
42/// does not aggregate the capabilities of route targets.
43///
44/// # Example
45///
46/// ```no_run
47/// use opendal_core::services::Memory;
48/// use opendal_core::{Operator, Result};
49/// use opendal_layer_route::RouteLayer;
50///
51/// fn build_operator() -> Result<Operator> {
52///     let archive = Operator::new(Memory::default())?;
53///     let routes = RouteLayer::builder()
54///         .route("archive/**", archive)
55///         .build()?;
56///
57///     Ok(Operator::new(Memory::default())?.layer(routes))
58/// }
59/// ```
60#[derive(Clone, Debug)]
61pub struct RouteLayer {
62    router: Arc<RouteRouter>,
63}
64
65impl RouteLayer {
66    /// Create a builder for `RouteLayer`.
67    pub fn builder() -> RouteLayerBuilder {
68        RouteLayerBuilder::default()
69    }
70}
71
72/// Builder for `RouteLayer`.
73#[derive(Default)]
74pub struct RouteLayerBuilder {
75    routes: Vec<RouteEntry>,
76}
77
78impl RouteLayerBuilder {
79    /// Add a route for paths that match `pattern`.
80    ///
81    /// If multiple patterns match, the route added first wins. The target
82    /// operator contributes both its service and its operation context.
83    pub fn route(mut self, pattern: impl AsRef<str>, op: Operator) -> Self {
84        let (ctx, srv) = op.into_parts();
85        self.routes.push(RouteEntry {
86            pattern: pattern.as_ref().to_string(),
87            target: RouteTarget { srv, ctx },
88        });
89        self
90    }
91
92    /// Build the route layer.
93    ///
94    /// This method returns [`ErrorKind::ConfigInvalid`] if a glob pattern is
95    /// invalid.
96    pub fn build(self) -> Result<RouteLayer> {
97        let mut builder = GlobSetBuilder::new();
98        let mut targets = Vec::with_capacity(self.routes.len());
99
100        for entry in self.routes {
101            let glob = Glob::new(&entry.pattern).map_err(|err| {
102                Error::new(ErrorKind::ConfigInvalid, "invalid route glob pattern")
103                    .with_context("pattern", entry.pattern.clone())
104                    .with_context("source", err.to_string())
105            })?;
106            builder.add(glob);
107            targets.push(entry.target);
108        }
109
110        let glob = builder.build().map_err(|err| {
111            Error::new(ErrorKind::ConfigInvalid, "failed to build route glob set")
112                .with_context("source", err.to_string())
113        })?;
114
115        Ok(RouteLayer {
116            router: Arc::new(RouteRouter { glob, targets }),
117        })
118    }
119}
120
121struct RouteEntry {
122    pattern: String,
123    target: RouteTarget,
124}
125
126#[derive(Debug)]
127struct RouteRouter {
128    glob: GlobSet,
129    targets: Vec<RouteTarget>,
130}
131
132#[derive(Clone, Debug)]
133struct RouteTarget {
134    srv: Servicer,
135    ctx: OperationContext,
136}
137
138enum RouteSelected {
139    Default(Servicer),
140    Target(RouteTarget),
141}
142
143impl RouteRouter {
144    fn match_index(&self, path: &str) -> Option<usize> {
145        self.glob.matches(path).into_iter().min()
146    }
147
148    fn select(&self, path: &str, default: &Servicer) -> RouteSelected {
149        self.match_index(path)
150            .and_then(|idx| self.targets.get(idx).cloned())
151            .map(RouteSelected::Target)
152            .unwrap_or_else(|| RouteSelected::Default(default.clone()))
153    }
154
155    fn target(&self, idx: usize) -> Option<RouteTarget> {
156        self.targets.get(idx).cloned()
157    }
158}
159
160impl Layer for RouteLayer {
161    fn apply_service(&self, inner: Servicer) -> Servicer {
162        Arc::new(self.layer(inner))
163    }
164}
165
166impl RouteLayer {
167    fn layer(&self, inner: Servicer) -> RouteAccessor {
168        RouteAccessor {
169            inner: Arc::new(inner),
170            router: self.router.clone(),
171        }
172    }
173}
174
175#[doc(hidden)]
176pub struct RouteAccessor {
177    inner: Servicer,
178    router: Arc<RouteRouter>,
179}
180
181impl Debug for RouteAccessor {
182    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
183        self.inner.fmt(f)
184    }
185}
186
187impl RouteAccessor {
188    fn select(&self, path: &str) -> RouteSelected {
189        self.router.select(path, &self.inner)
190    }
191}
192
193impl Service for RouteAccessor {
194    type Reader = oio::Reader;
195    type Writer = oio::Writer;
196    type Lister = oio::Lister;
197    type Deleter = oio::Deleter;
198    type Copier = oio::Copier;
199    type Composer = ();
200
201    fn info(&self) -> ServiceInfo {
202        self.inner.info()
203    }
204
205    fn capability(&self) -> Capability {
206        let mut capability = self.inner.capability();
207        capability.write_can_copy_from = false;
208        capability.compose = false;
209        capability.compose_with_content_type = false;
210        capability.compose_with_content_disposition = false;
211        capability.compose_with_content_encoding = false;
212        capability.compose_with_cache_control = false;
213        capability.compose_with_user_metadata = false;
214        capability.compose_with_if_match = false;
215        capability.compose_with_if_none_match = false;
216        capability.compose_with_if_version_match = false;
217        capability.compose_with_if_version_not_match = false;
218        capability.compose_with_if_not_exists = false;
219        capability.compose_with_source_version = false;
220        capability.compose_with_source_if_match = false;
221        capability
222    }
223
224    async fn create_dir(
225        &self,
226        ctx: &OperationContext,
227        path: &str,
228        args: OpCreateDir,
229    ) -> Result<RpCreateDir> {
230        match self.select(path) {
231            RouteSelected::Default(srv) => srv.create_dir(ctx, path, args).await,
232            RouteSelected::Target(target) => target.srv.create_dir(&target.ctx, path, args).await,
233        }
234    }
235
236    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<oio::Reader> {
237        match self.select(path) {
238            RouteSelected::Default(srv) => srv.read(ctx, path, args),
239            RouteSelected::Target(target) => target.srv.read(&target.ctx, path, args),
240        }
241    }
242
243    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<oio::Writer> {
244        match self.select(path) {
245            RouteSelected::Default(srv) => srv.write(ctx, path, args),
246            RouteSelected::Target(target) => target.srv.write(&target.ctx, path, args),
247        }
248    }
249
250    fn copy(
251        &self,
252        ctx: &OperationContext,
253        from: &str,
254        to: &str,
255        args: OpCopy,
256    ) -> Result<oio::Copier> {
257        match self.select(from) {
258            RouteSelected::Default(srv) => srv.copy(ctx, from, to, args),
259            RouteSelected::Target(target) => target.srv.copy(&target.ctx, from, to, args),
260        }
261    }
262
263    async fn rename(
264        &self,
265        ctx: &OperationContext,
266        from: &str,
267        to: &str,
268        args: OpRename,
269    ) -> Result<RpRename> {
270        match self.select(from) {
271            RouteSelected::Default(srv) => srv.rename(ctx, from, to, args).await,
272            RouteSelected::Target(target) => target.srv.rename(&target.ctx, from, to, args).await,
273        }
274    }
275
276    async fn restore(
277        &self,
278        ctx: &OperationContext,
279        path: &str,
280        args: OpRestore,
281    ) -> Result<RpRestore> {
282        match self.select(path) {
283            RouteSelected::Default(srv) => srv.restore(ctx, path, args).await,
284            RouteSelected::Target(target) => target.srv.restore(&target.ctx, path, args).await,
285        }
286    }
287
288    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
289        match self.select(path) {
290            RouteSelected::Default(srv) => srv.stat(ctx, path, args).await,
291            RouteSelected::Target(target) => target.srv.stat(&target.ctx, path, args).await,
292        }
293    }
294
295    fn delete(&self, ctx: &OperationContext) -> Result<oio::Deleter> {
296        Ok(Box::new(RouteDeleter::new(
297            self.inner.clone(),
298            self.router.clone(),
299            ctx.clone(),
300        )) as oio::Deleter)
301    }
302
303    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<oio::Lister> {
304        match self.select(path) {
305            RouteSelected::Default(srv) => srv.list(ctx, path, args),
306            RouteSelected::Target(target) => target.srv.list(&target.ctx, path, args),
307        }
308    }
309
310    async fn presign(
311        &self,
312        ctx: &OperationContext,
313        path: &str,
314        args: OpPresign,
315    ) -> Result<RpPresign> {
316        match self.select(path) {
317            RouteSelected::Default(srv) => srv.presign(ctx, path, args).await,
318            RouteSelected::Target(target) => target.srv.presign(&target.ctx, path, args).await,
319        }
320    }
321}
322
323#[doc(hidden)]
324pub struct RouteDeleter {
325    default: Servicer,
326    router: Arc<RouteRouter>,
327    ctx: OperationContext,
328    default_deleter: Option<oio::Deleter>,
329    target_deleters: Vec<Option<oio::Deleter>>,
330}
331
332impl RouteDeleter {
333    fn new(default: Servicer, router: Arc<RouteRouter>, ctx: OperationContext) -> Self {
334        let mut target_deleters = Vec::with_capacity(router.targets.len());
335        target_deleters.resize_with(router.targets.len(), || None);
336        Self {
337            default,
338            router,
339            ctx,
340            default_deleter: None,
341            target_deleters,
342        }
343    }
344
345    async fn get_deleter(&mut self, key: RouteKey) -> Result<&mut oio::Deleter> {
346        match key {
347            RouteKey::Default => {
348                if self.default_deleter.is_none() {
349                    let deleter = self.default.delete(&self.ctx)?;
350                    self.default_deleter = Some(deleter);
351                }
352                Ok(self
353                    .default_deleter
354                    .as_mut()
355                    .expect("default deleter must exist"))
356            }
357            RouteKey::Target(idx) => {
358                if idx >= self.target_deleters.len() {
359                    if self.default_deleter.is_none() {
360                        let deleter = self.default.delete(&self.ctx)?;
361                        self.default_deleter = Some(deleter);
362                    }
363                    return Ok(self
364                        .default_deleter
365                        .as_mut()
366                        .expect("default deleter must exist"));
367                }
368                if self.target_deleters[idx].is_none() {
369                    let Some(target) = self.router.target(idx) else {
370                        let deleter = self.default.delete(&self.ctx)?;
371                        self.default_deleter = Some(deleter);
372                        return Ok(self
373                            .default_deleter
374                            .as_mut()
375                            .expect("default deleter must exist"));
376                    };
377                    let deleter = target.srv.delete(&target.ctx)?;
378                    self.target_deleters[idx] = Some(deleter);
379                }
380                Ok(self.target_deleters[idx]
381                    .as_mut()
382                    .expect("target deleter must exist"))
383            }
384        }
385    }
386}
387
388impl oio::Delete for RouteDeleter {
389    async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
390        let key = match self.router.match_index(path) {
391            Some(idx) => RouteKey::Target(idx),
392            None => RouteKey::Default,
393        };
394        let deleter = self.get_deleter(key).await?;
395        deleter.delete(path, args).await
396    }
397
398    async fn close(&mut self) -> Result<()> {
399        let mut first_err = None;
400        if let Some(deleter) = self.default_deleter.as_mut()
401            && let Err(err) = deleter.close().await
402        {
403            first_err = Some(err);
404        }
405        for deleter in self.target_deleters.iter_mut().flatten() {
406            if let Err(err) = deleter.close().await
407                && first_err.is_none()
408            {
409                first_err = Some(err);
410            }
411        }
412
413        match first_err {
414            Some(err) => Err(err),
415            None => Ok(()),
416        }
417    }
418}
419
420#[derive(Debug, Hash, PartialEq, Eq)]
421enum RouteKey {
422    Default,
423    Target(usize),
424}
425
426#[cfg(test)]
427mod tests {
428    use std::collections::HashSet;
429    use std::path::Path;
430    use std::path::PathBuf;
431    use std::sync::Mutex;
432    use std::time::SystemTime;
433    use std::time::UNIX_EPOCH;
434
435    use opendal_service_fs::Fs;
436
437    use super::*;
438
439    fn build_memory_operator() -> Result<Operator> {
440        Operator::new(services::Memory::default())
441    }
442
443    fn build_mock_operator(name: &'static str) -> Operator {
444        Operator::from_parts(
445            OperationContext::default(),
446            Arc::new(MockService {
447                info: ServiceInfo::new("mock", "", name),
448                paths: Arc::new(Mutex::new(HashSet::new())),
449            }),
450        )
451    }
452
453    async fn build_fs_operator(label: &str) -> Result<(Operator, PathBuf)> {
454        let root = fs_root(label);
455        tokio::fs::create_dir_all(&root)
456            .await
457            .map_err(new_std_io_error)?;
458        let op = Operator::new(Fs::default().root(&root.to_string_lossy()))?;
459        Ok((op, root))
460    }
461
462    fn fs_root(label: &str) -> PathBuf {
463        let nanos = SystemTime::now()
464            .duration_since(UNIX_EPOCH)
465            .expect("system time before UNIX_EPOCH")
466            .as_nanos();
467        let mut root = std::env::temp_dir();
468        root.push(format!(
469            "opendal-route-{label}-{nanos}-{}",
470            std::process::id()
471        ));
472        root
473    }
474
475    async fn cleanup_fs_root(root: &Path) {
476        let _ = tokio::fs::remove_dir_all(root).await;
477    }
478
479    async fn assert_missing(op: &Operator, path: &str) -> Result<()> {
480        let err = op.stat(path).await.unwrap_err();
481        assert_eq!(err.kind(), ErrorKind::NotFound);
482        Ok(())
483    }
484
485    #[derive(Debug)]
486    struct MockService {
487        info: ServiceInfo,
488        paths: Arc<Mutex<HashSet<String>>>,
489    }
490
491    impl Service for MockService {
492        type Reader = ();
493        type Writer = MockWriter;
494        type Lister = ();
495        type Deleter = ();
496        type Copier = ();
497        type Composer = ();
498
499        fn info(&self) -> ServiceInfo {
500            self.info.clone()
501        }
502
503        fn capability(&self) -> Capability {
504            Capability {
505                stat: true,
506                write: true,
507                copy: true,
508                rename: true,
509                ..Default::default()
510            }
511        }
512
513        async fn create_dir(
514            &self,
515            _: &OperationContext,
516            _: &str,
517            _: OpCreateDir,
518        ) -> Result<RpCreateDir> {
519            Err(Error::new(
520                ErrorKind::Unsupported,
521                "operation is not supported",
522            ))
523        }
524
525        async fn stat(&self, _: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
526            if self.paths.lock().unwrap().contains(path) {
527                Ok(RpStat::new(MetadataBuilder::file(0).build()))
528            } else {
529                Err(Error::new(ErrorKind::NotFound, "path not found"))
530            }
531        }
532
533        fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
534            Err(Error::new(
535                ErrorKind::Unsupported,
536                "operation is not supported",
537            ))
538        }
539
540        fn write(&self, _: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> {
541            Ok(MockWriter {
542                paths: self.paths.clone(),
543                path: path.to_string(),
544            })
545        }
546
547        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
548            Err(Error::new(
549                ErrorKind::Unsupported,
550                "operation is not supported",
551            ))
552        }
553
554        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
555            Err(Error::new(
556                ErrorKind::Unsupported,
557                "operation is not supported",
558            ))
559        }
560
561        fn copy(
562            &self,
563            _: &OperationContext,
564            from: &str,
565            to: &str,
566            _: OpCopy,
567        ) -> Result<Self::Copier> {
568            if !self.paths.lock().unwrap().contains(from) {
569                return Err(Error::new(ErrorKind::NotFound, "source not found"));
570            }
571            self.paths.lock().unwrap().insert(to.to_string());
572            Ok(())
573        }
574
575        async fn rename(
576            &self,
577            _: &OperationContext,
578            from: &str,
579            to: &str,
580            _: OpRename,
581        ) -> Result<RpRename> {
582            let mut paths = self.paths.lock().unwrap();
583            if !paths.remove(from) {
584                return Err(Error::new(ErrorKind::NotFound, "source not found"));
585            }
586            paths.insert(to.to_string());
587            Ok(RpRename::default())
588        }
589
590        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
591            Err(Error::new(
592                ErrorKind::Unsupported,
593                "operation is not supported",
594            ))
595        }
596    }
597
598    struct MockWriter {
599        paths: Arc<Mutex<HashSet<String>>>,
600        path: String,
601    }
602
603    impl oio::Write for MockWriter {
604        async fn write(&mut self, _: Buffer) -> Result<()> {
605            Ok(())
606        }
607
608        async fn close(&mut self) -> Result<Metadata> {
609            self.paths.lock().unwrap().insert(self.path.clone());
610            Ok(MetadataBuilder::unknown().build())
611        }
612
613        async fn abort(&mut self) -> Result<()> {
614            Ok(())
615        }
616    }
617
618    #[tokio::test]
619    async fn test_first_match_wins() -> Result<()> {
620        let default_op = build_memory_operator()?;
621        let (fast_op, fast_root) = build_fs_operator("first-match-fast").await?;
622        let slow_op = build_memory_operator()?;
623
624        let routed = default_op.clone().layer(
625            RouteLayer::builder()
626                .route("data/*.txt", fast_op.clone())
627                .route("data/**", slow_op.clone())
628                .build()?,
629        );
630
631        routed.write("data/file.txt", "v").await?;
632
633        assert!(fast_op.stat("data/file.txt").await.is_ok());
634        assert_missing(&slow_op, "data/file.txt").await?;
635        assert_missing(&default_op, "data/file.txt").await?;
636
637        cleanup_fs_root(&fast_root).await;
638        Ok(())
639    }
640
641    #[tokio::test]
642    async fn test_fallback_to_default() -> Result<()> {
643        let default_op = build_memory_operator()?;
644        let (hot_op, hot_root) = build_fs_operator("fallback-hot").await?;
645
646        let routed = default_op.clone().layer(
647            RouteLayer::builder()
648                .route("hot/**", hot_op.clone())
649                .build()?,
650        );
651
652        routed.write("cold/file.txt", "v").await?;
653
654        assert!(default_op.stat("cold/file.txt").await.is_ok());
655        assert_missing(&hot_op, "cold/file.txt").await?;
656
657        cleanup_fs_root(&hot_root).await;
658        Ok(())
659    }
660
661    #[tokio::test]
662    async fn test_copy_and_rename_route_by_from() -> Result<()> {
663        let default_op = build_mock_operator("default");
664        let hot_op = build_mock_operator("hot");
665
666        let routed = default_op.clone().layer(
667            RouteLayer::builder()
668                .route("hot/**", hot_op.clone())
669                .build()?,
670        );
671
672        routed.write("hot/src.txt", "v").await?;
673        routed.copy("hot/src.txt", "cold/copied.txt").await?;
674
675        assert!(hot_op.stat("hot/src.txt").await.is_ok());
676        assert!(hot_op.stat("cold/copied.txt").await.is_ok());
677        assert_missing(&default_op, "cold/copied.txt").await?;
678
679        routed.write("hot/src2.txt", "v").await?;
680        routed.rename("hot/src2.txt", "cold/renamed.txt").await?;
681
682        assert_missing(&hot_op, "hot/src2.txt").await?;
683        assert!(hot_op.stat("cold/renamed.txt").await.is_ok());
684        assert_missing(&default_op, "cold/renamed.txt").await?;
685
686        Ok(())
687    }
688
689    #[tokio::test]
690    async fn test_delete_iter_routes_per_path() -> Result<()> {
691        let default_op = build_memory_operator()?;
692        let (hot_op, hot_root) = build_fs_operator("delete-hot").await?;
693
694        let routed = default_op.clone().layer(
695            RouteLayer::builder()
696                .route("hot/**", hot_op.clone())
697                .build()?,
698        );
699
700        routed.write("hot/a.txt", "v").await?;
701        routed.write("cold/b.txt", "v").await?;
702
703        routed.delete_iter(["hot/a.txt", "cold/b.txt"]).await?;
704
705        assert_missing(&hot_op, "hot/a.txt").await?;
706        assert_missing(&default_op, "cold/b.txt").await?;
707
708        cleanup_fs_root(&hot_root).await;
709        Ok(())
710    }
711}