1#![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#[derive(Clone, Debug)]
61pub struct RouteLayer {
62 router: Arc<RouteRouter>,
63}
64
65impl RouteLayer {
66 pub fn builder() -> RouteLayerBuilder {
68 RouteLayerBuilder::default()
69 }
70}
71
72#[derive(Default)]
74pub struct RouteLayerBuilder {
75 routes: Vec<RouteEntry>,
76}
77
78impl RouteLayerBuilder {
79 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 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
200 fn info(&self) -> ServiceInfo {
201 self.inner.info()
202 }
203
204 fn capability(&self) -> Capability {
205 self.inner.capability()
206 }
207
208 async fn create_dir(
209 &self,
210 ctx: &OperationContext,
211 path: &str,
212 args: OpCreateDir,
213 ) -> Result<RpCreateDir> {
214 match self.select(path) {
215 RouteSelected::Default(srv) => srv.create_dir(ctx, path, args).await,
216 RouteSelected::Target(target) => target.srv.create_dir(&target.ctx, path, args).await,
217 }
218 }
219
220 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<oio::Reader> {
221 match self.select(path) {
222 RouteSelected::Default(srv) => srv.read(ctx, path, args),
223 RouteSelected::Target(target) => target.srv.read(&target.ctx, path, args),
224 }
225 }
226
227 fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<oio::Writer> {
228 match self.select(path) {
229 RouteSelected::Default(srv) => srv.write(ctx, path, args),
230 RouteSelected::Target(target) => target.srv.write(&target.ctx, path, args),
231 }
232 }
233
234 fn copy(
235 &self,
236 ctx: &OperationContext,
237 from: &str,
238 to: &str,
239 args: OpCopy,
240 opts: OpCopier,
241 ) -> Result<oio::Copier> {
242 match self.select(from) {
243 RouteSelected::Default(srv) => srv.copy(ctx, from, to, args, opts),
244 RouteSelected::Target(target) => target.srv.copy(&target.ctx, from, to, args, opts),
245 }
246 }
247
248 async fn rename(
249 &self,
250 ctx: &OperationContext,
251 from: &str,
252 to: &str,
253 args: OpRename,
254 ) -> Result<RpRename> {
255 match self.select(from) {
256 RouteSelected::Default(srv) => srv.rename(ctx, from, to, args).await,
257 RouteSelected::Target(target) => target.srv.rename(&target.ctx, from, to, args).await,
258 }
259 }
260
261 async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
262 match self.select(path) {
263 RouteSelected::Default(srv) => srv.stat(ctx, path, args).await,
264 RouteSelected::Target(target) => target.srv.stat(&target.ctx, path, args).await,
265 }
266 }
267
268 fn delete(&self, ctx: &OperationContext) -> Result<oio::Deleter> {
269 Ok(Box::new(RouteDeleter::new(
270 self.inner.clone(),
271 self.router.clone(),
272 ctx.clone(),
273 )) as oio::Deleter)
274 }
275
276 fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<oio::Lister> {
277 match self.select(path) {
278 RouteSelected::Default(srv) => srv.list(ctx, path, args),
279 RouteSelected::Target(target) => target.srv.list(&target.ctx, path, args),
280 }
281 }
282
283 async fn presign(
284 &self,
285 ctx: &OperationContext,
286 path: &str,
287 args: OpPresign,
288 ) -> Result<RpPresign> {
289 match self.select(path) {
290 RouteSelected::Default(srv) => srv.presign(ctx, path, args).await,
291 RouteSelected::Target(target) => target.srv.presign(&target.ctx, path, args).await,
292 }
293 }
294}
295
296#[doc(hidden)]
297pub struct RouteDeleter {
298 default: Servicer,
299 router: Arc<RouteRouter>,
300 ctx: OperationContext,
301 default_deleter: Option<oio::Deleter>,
302 target_deleters: Vec<Option<oio::Deleter>>,
303}
304
305impl RouteDeleter {
306 fn new(default: Servicer, router: Arc<RouteRouter>, ctx: OperationContext) -> Self {
307 let mut target_deleters = Vec::with_capacity(router.targets.len());
308 target_deleters.resize_with(router.targets.len(), || None);
309 Self {
310 default,
311 router,
312 ctx,
313 default_deleter: None,
314 target_deleters,
315 }
316 }
317
318 async fn get_deleter(&mut self, key: RouteKey) -> Result<&mut oio::Deleter> {
319 match key {
320 RouteKey::Default => {
321 if self.default_deleter.is_none() {
322 let deleter = self.default.delete(&self.ctx)?;
323 self.default_deleter = Some(deleter);
324 }
325 Ok(self
326 .default_deleter
327 .as_mut()
328 .expect("default deleter must exist"))
329 }
330 RouteKey::Target(idx) => {
331 if idx >= self.target_deleters.len() {
332 if self.default_deleter.is_none() {
333 let deleter = self.default.delete(&self.ctx)?;
334 self.default_deleter = Some(deleter);
335 }
336 return Ok(self
337 .default_deleter
338 .as_mut()
339 .expect("default deleter must exist"));
340 }
341 if self.target_deleters[idx].is_none() {
342 let Some(target) = self.router.target(idx) else {
343 let deleter = self.default.delete(&self.ctx)?;
344 self.default_deleter = Some(deleter);
345 return Ok(self
346 .default_deleter
347 .as_mut()
348 .expect("default deleter must exist"));
349 };
350 let deleter = target.srv.delete(&target.ctx)?;
351 self.target_deleters[idx] = Some(deleter);
352 }
353 Ok(self.target_deleters[idx]
354 .as_mut()
355 .expect("target deleter must exist"))
356 }
357 }
358 }
359}
360
361impl oio::Delete for RouteDeleter {
362 async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
363 let key = match self.router.match_index(path) {
364 Some(idx) => RouteKey::Target(idx),
365 None => RouteKey::Default,
366 };
367 let deleter = self.get_deleter(key).await?;
368 deleter.delete(path, args).await
369 }
370
371 async fn close(&mut self) -> Result<()> {
372 let mut first_err = None;
373 if let Some(deleter) = self.default_deleter.as_mut()
374 && let Err(err) = deleter.close().await
375 {
376 first_err = Some(err);
377 }
378 for deleter in self.target_deleters.iter_mut().flatten() {
379 if let Err(err) = deleter.close().await
380 && first_err.is_none()
381 {
382 first_err = Some(err);
383 }
384 }
385
386 match first_err {
387 Some(err) => Err(err),
388 None => Ok(()),
389 }
390 }
391}
392
393#[derive(Debug, Hash, PartialEq, Eq)]
394enum RouteKey {
395 Default,
396 Target(usize),
397}
398
399#[cfg(test)]
400mod tests {
401 use std::collections::HashSet;
402 use std::path::Path;
403 use std::path::PathBuf;
404 use std::sync::Mutex;
405 use std::time::SystemTime;
406 use std::time::UNIX_EPOCH;
407
408 use opendal_service_fs::Fs;
409
410 use super::*;
411
412 fn build_memory_operator() -> Result<Operator> {
413 Operator::new(services::Memory::default())
414 }
415
416 fn build_mock_operator(name: &'static str) -> Operator {
417 Operator::from_parts(
418 OperationContext::default(),
419 Arc::new(MockService {
420 info: ServiceInfo::new("mock", "", name),
421 paths: Arc::new(Mutex::new(HashSet::new())),
422 }),
423 )
424 }
425
426 async fn build_fs_operator(label: &str) -> Result<(Operator, PathBuf)> {
427 let root = fs_root(label);
428 tokio::fs::create_dir_all(&root)
429 .await
430 .map_err(new_std_io_error)?;
431 let op = Operator::new(Fs::default().root(&root.to_string_lossy()))?;
432 Ok((op, root))
433 }
434
435 fn fs_root(label: &str) -> PathBuf {
436 let nanos = SystemTime::now()
437 .duration_since(UNIX_EPOCH)
438 .expect("system time before UNIX_EPOCH")
439 .as_nanos();
440 let mut root = std::env::temp_dir();
441 root.push(format!(
442 "opendal-route-{label}-{nanos}-{}",
443 std::process::id()
444 ));
445 root
446 }
447
448 async fn cleanup_fs_root(root: &Path) {
449 let _ = tokio::fs::remove_dir_all(root).await;
450 }
451
452 async fn assert_missing(op: &Operator, path: &str) -> Result<()> {
453 let err = op.stat(path).await.unwrap_err();
454 assert_eq!(err.kind(), ErrorKind::NotFound);
455 Ok(())
456 }
457
458 #[derive(Debug)]
459 struct MockService {
460 info: ServiceInfo,
461 paths: Arc<Mutex<HashSet<String>>>,
462 }
463
464 impl Service for MockService {
465 type Reader = ();
466 type Writer = MockWriter;
467 type Lister = ();
468 type Deleter = ();
469 type Copier = ();
470
471 fn info(&self) -> ServiceInfo {
472 self.info.clone()
473 }
474
475 fn capability(&self) -> Capability {
476 Capability {
477 stat: true,
478 write: true,
479 copy: true,
480 rename: true,
481 ..Default::default()
482 }
483 }
484
485 async fn create_dir(
486 &self,
487 _: &OperationContext,
488 _: &str,
489 _: OpCreateDir,
490 ) -> Result<RpCreateDir> {
491 Err(Error::new(
492 ErrorKind::Unsupported,
493 "operation is not supported",
494 ))
495 }
496
497 async fn stat(&self, _: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
498 if self.paths.lock().unwrap().contains(path) {
499 Ok(RpStat::new(Metadata::new(EntryMode::FILE)))
500 } else {
501 Err(Error::new(ErrorKind::NotFound, "path not found"))
502 }
503 }
504
505 fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
506 Err(Error::new(
507 ErrorKind::Unsupported,
508 "operation is not supported",
509 ))
510 }
511
512 fn write(&self, _: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> {
513 Ok(MockWriter {
514 paths: self.paths.clone(),
515 path: path.to_string(),
516 })
517 }
518
519 fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
520 Err(Error::new(
521 ErrorKind::Unsupported,
522 "operation is not supported",
523 ))
524 }
525
526 fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
527 Err(Error::new(
528 ErrorKind::Unsupported,
529 "operation is not supported",
530 ))
531 }
532
533 fn copy(
534 &self,
535 _: &OperationContext,
536 from: &str,
537 to: &str,
538 _: OpCopy,
539 _: OpCopier,
540 ) -> Result<Self::Copier> {
541 if !self.paths.lock().unwrap().contains(from) {
542 return Err(Error::new(ErrorKind::NotFound, "source not found"));
543 }
544 self.paths.lock().unwrap().insert(to.to_string());
545 Ok(())
546 }
547
548 async fn rename(
549 &self,
550 _: &OperationContext,
551 from: &str,
552 to: &str,
553 _: OpRename,
554 ) -> Result<RpRename> {
555 let mut paths = self.paths.lock().unwrap();
556 if !paths.remove(from) {
557 return Err(Error::new(ErrorKind::NotFound, "source not found"));
558 }
559 paths.insert(to.to_string());
560 Ok(RpRename::default())
561 }
562
563 async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
564 Err(Error::new(
565 ErrorKind::Unsupported,
566 "operation is not supported",
567 ))
568 }
569 }
570
571 struct MockWriter {
572 paths: Arc<Mutex<HashSet<String>>>,
573 path: String,
574 }
575
576 impl oio::Write for MockWriter {
577 async fn write(&mut self, _: Buffer) -> Result<()> {
578 Ok(())
579 }
580
581 async fn close(&mut self) -> Result<Metadata> {
582 self.paths.lock().unwrap().insert(self.path.clone());
583 Ok(Metadata::new(EntryMode::FILE))
584 }
585
586 async fn abort(&mut self) -> Result<()> {
587 Ok(())
588 }
589 }
590
591 #[tokio::test]
592 async fn test_first_match_wins() -> Result<()> {
593 let default_op = build_memory_operator()?;
594 let (fast_op, fast_root) = build_fs_operator("first-match-fast").await?;
595 let slow_op = build_memory_operator()?;
596
597 let routed = default_op.clone().layer(
598 RouteLayer::builder()
599 .route("data/*.txt", fast_op.clone())
600 .route("data/**", slow_op.clone())
601 .build()?,
602 );
603
604 routed.write("data/file.txt", "v").await?;
605
606 assert!(fast_op.stat("data/file.txt").await.is_ok());
607 assert_missing(&slow_op, "data/file.txt").await?;
608 assert_missing(&default_op, "data/file.txt").await?;
609
610 cleanup_fs_root(&fast_root).await;
611 Ok(())
612 }
613
614 #[tokio::test]
615 async fn test_fallback_to_default() -> Result<()> {
616 let default_op = build_memory_operator()?;
617 let (hot_op, hot_root) = build_fs_operator("fallback-hot").await?;
618
619 let routed = default_op.clone().layer(
620 RouteLayer::builder()
621 .route("hot/**", hot_op.clone())
622 .build()?,
623 );
624
625 routed.write("cold/file.txt", "v").await?;
626
627 assert!(default_op.stat("cold/file.txt").await.is_ok());
628 assert_missing(&hot_op, "cold/file.txt").await?;
629
630 cleanup_fs_root(&hot_root).await;
631 Ok(())
632 }
633
634 #[tokio::test]
635 async fn test_copy_and_rename_route_by_from() -> Result<()> {
636 let default_op = build_mock_operator("default");
637 let hot_op = build_mock_operator("hot");
638
639 let routed = default_op.clone().layer(
640 RouteLayer::builder()
641 .route("hot/**", hot_op.clone())
642 .build()?,
643 );
644
645 routed.write("hot/src.txt", "v").await?;
646 routed.copy("hot/src.txt", "cold/copied.txt").await?;
647
648 assert!(hot_op.stat("hot/src.txt").await.is_ok());
649 assert!(hot_op.stat("cold/copied.txt").await.is_ok());
650 assert_missing(&default_op, "cold/copied.txt").await?;
651
652 routed.write("hot/src2.txt", "v").await?;
653 routed.rename("hot/src2.txt", "cold/renamed.txt").await?;
654
655 assert_missing(&hot_op, "hot/src2.txt").await?;
656 assert!(hot_op.stat("cold/renamed.txt").await.is_ok());
657 assert_missing(&default_op, "cold/renamed.txt").await?;
658
659 Ok(())
660 }
661
662 #[tokio::test]
663 async fn test_delete_iter_routes_per_path() -> Result<()> {
664 let default_op = build_memory_operator()?;
665 let (hot_op, hot_root) = build_fs_operator("delete-hot").await?;
666
667 let routed = default_op.clone().layer(
668 RouteLayer::builder()
669 .route("hot/**", hot_op.clone())
670 .build()?,
671 );
672
673 routed.write("hot/a.txt", "v").await?;
674 routed.write("cold/b.txt", "v").await?;
675
676 routed.delete_iter(["hot/a.txt", "cold/b.txt"]).await?;
677
678 assert_missing(&hot_op, "hot/a.txt").await?;
679 assert_missing(&default_op, "cold/b.txt").await?;
680
681 cleanup_fs_root(&hot_root).await;
682 Ok(())
683 }
684}