Skip to main content

opendal_layer_immutable_index/
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::collections::HashSet;
23use std::sync::Arc;
24use std::vec::IntoIter;
25
26use opendal_core::raw::*;
27use opendal_core::*;
28
29/// `ImmutableIndexLayer` adds an immutable in-memory index to a storage service.
30///
31/// Especially useful for services without list capability like HTTP.
32///
33/// # Examples
34///
35/// ```no_run
36/// # use std::collections::HashMap;
37/// #
38/// # use opendal_core::services;
39/// # use opendal_core::Operator;
40/// # use opendal_core::Result;
41/// # use opendal_layer_immutable_index::ImmutableIndexLayer;
42/// #
43/// # fn main() -> Result<()> {
44/// let mut iil = ImmutableIndexLayer::new();
45///
46/// for i in ["file", "dir/", "dir/file", "dir_without_prefix/file"] {
47///     iil.insert(i.to_string())
48/// }
49///
50/// let op = Operator::from_iter::<services::Memory>(HashMap::<_, _>::default())?
51///     .layer(iil);
52/// # Ok(())
53/// # }
54/// ```
55#[derive(Clone, Debug, Default)]
56pub struct ImmutableIndexLayer {
57    vec: Vec<String>,
58}
59
60impl ImmutableIndexLayer {
61    /// Create a new [`ImmutableIndexLayer`].
62    pub fn new() -> Self {
63        Self::default()
64    }
65}
66
67impl ImmutableIndexLayer {
68    /// Insert a key into index.
69    pub fn insert(&mut self, key: String) {
70        self.vec.push(key);
71    }
72
73    /// Insert keys from iter.
74    pub fn extend_iter<I>(&mut self, iter: I)
75    where
76        I: IntoIterator<Item = String>,
77    {
78        self.vec.extend(iter);
79    }
80}
81
82impl Layer for ImmutableIndexLayer {
83    fn apply_service(&self, inner: Servicer) -> Servicer {
84        Arc::new(self.layer(inner))
85    }
86}
87
88impl ImmutableIndexLayer {
89    fn layer(&self, inner: Servicer) -> ImmutableIndexService {
90        ImmutableIndexService {
91            inner,
92            vec: self.vec.clone(),
93        }
94    }
95}
96
97#[doc(hidden)]
98#[derive(Debug)]
99pub struct ImmutableIndexService {
100    inner: Servicer,
101    vec: Vec<String>,
102}
103
104impl ImmutableIndexService {
105    fn children_flat(&self, path: &str) -> Vec<String> {
106        self.vec
107            .iter()
108            .filter(|v| v.starts_with(path) && v.as_str() != path)
109            .cloned()
110            .collect()
111    }
112
113    fn children_hierarchy(&self, path: &str) -> Vec<String> {
114        let mut res = HashSet::new();
115
116        for i in self.vec.iter() {
117            // `/xyz` should not belong to `/abc`
118            if !i.starts_with(path) {
119                continue;
120            }
121
122            // remove `/abc` if self
123            if i == path {
124                continue;
125            }
126
127            match i[path.len()..].find('/') {
128                // File `/abc/def.csv` must belong to `/abc`
129                None => {
130                    res.insert(i.to_string());
131                }
132                Some(idx) => {
133                    // The index of first `/` after `/abc`.
134                    let dir_idx = idx + 1 + path.len();
135
136                    if dir_idx == i.len() {
137                        // Dir `/abc/def/` belongs to `/abc/`
138                        res.insert(i.to_string());
139                    } else {
140                        // File/Dir `/abc/def/xyz` doesn't belong to `/abc`.
141                        // But we need to list `/abc/def` out so that we can walk down.
142                        res.insert(i[..dir_idx].to_string());
143                    }
144                }
145            }
146        }
147
148        res.into_iter().collect()
149    }
150}
151
152impl Service for ImmutableIndexService {
153    type Reader = oio::Reader;
154    type Writer = oio::Writer;
155    type Lister = ImmutableDir;
156    type Deleter = oio::Deleter;
157    type Copier = oio::Copier;
158
159    fn info(&self) -> ServiceInfo {
160        self.inner.info()
161    }
162
163    fn capability(&self) -> Capability {
164        let mut capability = self.inner.capability();
165        capability.list = true;
166        capability.list_with_recursive = true;
167        capability
168    }
169
170    async fn create_dir(
171        &self,
172        ctx: &OperationContext,
173        path: &str,
174        args: OpCreateDir,
175    ) -> Result<RpCreateDir> {
176        self.inner.create_dir(ctx, path, args).await
177    }
178
179    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
180        self.inner.stat(ctx, path, args).await
181    }
182
183    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
184        self.inner.read(ctx, path, args)
185    }
186
187    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
188        self.inner.write(ctx, path, args)
189    }
190
191    fn copy(
192        &self,
193        ctx: &OperationContext,
194        from: &str,
195        to: &str,
196        args: OpCopy,
197        opts: OpCopier,
198    ) -> Result<Self::Copier> {
199        self.inner.copy(ctx, from, to, args, opts)
200    }
201
202    fn list(&self, _ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
203        let mut path = path;
204        if path == "/" {
205            path = ""
206        }
207
208        let idx = if args.recursive() {
209            self.children_flat(path)
210        } else {
211            self.children_hierarchy(path)
212        };
213
214        Ok(ImmutableDir::new(idx))
215    }
216
217    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
218        self.inner.delete(ctx)
219    }
220
221    async fn rename(
222        &self,
223        ctx: &OperationContext,
224        from: &str,
225        to: &str,
226        args: OpRename,
227    ) -> Result<RpRename> {
228        self.inner.rename(ctx, from, to, args).await
229    }
230
231    async fn presign(
232        &self,
233        ctx: &OperationContext,
234        path: &str,
235        args: OpPresign,
236    ) -> Result<RpPresign> {
237        self.inner.presign(ctx, path, args).await
238    }
239}
240
241#[doc(hidden)]
242pub struct ImmutableDir {
243    idx: IntoIter<String>,
244}
245
246impl ImmutableDir {
247    fn new(idx: Vec<String>) -> Self {
248        Self {
249            idx: idx.into_iter(),
250        }
251    }
252
253    fn inner_next(&mut self) -> Option<oio::Entry> {
254        self.idx.next().map(|v| {
255            let mode = if v.ends_with('/') {
256                EntryMode::DIR
257            } else {
258                EntryMode::FILE
259            };
260            let meta = Metadata::new(mode);
261            oio::Entry::with(v, meta)
262        })
263    }
264}
265
266impl oio::List for ImmutableDir {
267    async fn next(&mut self) -> Result<Option<oio::Entry>> {
268        Ok(self.inner_next())
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use std::collections::HashMap;
275    use std::sync::Arc;
276
277    use super::*;
278    use futures::TryStreamExt;
279    use log::debug;
280    use logforth::append::Testing;
281    use logforth::filter::rustlog::RustLogFilterBuilder;
282    use logforth::layout::TextLayout;
283
284    #[derive(Debug)]
285    struct MockService;
286
287    impl Service for MockService {
288        type Reader = ();
289        type Writer = ();
290        type Lister = ();
291        type Deleter = ();
292        type Copier = ();
293
294        fn info(&self) -> ServiceInfo {
295            ServiceInfo::with_scheme("mock")
296        }
297
298        fn capability(&self) -> Capability {
299            Capability {
300                list: true,
301                list_with_recursive: true,
302                ..Default::default()
303            }
304        }
305
306        async fn create_dir(
307            &self,
308            _: &OperationContext,
309            _: &str,
310            _: OpCreateDir,
311        ) -> Result<RpCreateDir> {
312            Err(Error::new(
313                ErrorKind::Unsupported,
314                "operation is not supported",
315            ))
316        }
317
318        async fn stat(&self, _: &OperationContext, _: &str, _: OpStat) -> Result<RpStat> {
319            Err(Error::new(
320                ErrorKind::Unsupported,
321                "operation is not supported",
322            ))
323        }
324
325        fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
326            Err(Error::new(
327                ErrorKind::Unsupported,
328                "operation is not supported",
329            ))
330        }
331
332        fn write(&self, _ctx: &OperationContext, _: &str, _: OpWrite) -> Result<Self::Writer> {
333            Err(Error::new(
334                ErrorKind::Unsupported,
335                "operation is not supported",
336            ))
337        }
338
339        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
340            Err(Error::new(
341                ErrorKind::Unsupported,
342                "operation is not supported",
343            ))
344        }
345
346        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
347            Err(Error::new(
348                ErrorKind::Unsupported,
349                "operation is not supported",
350            ))
351        }
352
353        fn copy(
354            &self,
355            _: &OperationContext,
356            _: &str,
357            _: &str,
358            _: OpCopy,
359            _: OpCopier,
360        ) -> Result<Self::Copier> {
361            Err(Error::new(
362                ErrorKind::Unsupported,
363                "operation is not supported",
364            ))
365        }
366
367        async fn rename(
368            &self,
369            _: &OperationContext,
370            _: &str,
371            _: &str,
372            _: OpRename,
373        ) -> Result<RpRename> {
374            Err(Error::new(
375                ErrorKind::Unsupported,
376                "operation is not supported",
377            ))
378        }
379
380        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
381            Err(Error::new(
382                ErrorKind::Unsupported,
383                "operation is not supported",
384            ))
385        }
386    }
387
388    fn build_operator(layer: ImmutableIndexLayer) -> Operator {
389        Operator::from_parts(OperationContext::default(), Arc::new(MockService)).layer(layer)
390    }
391
392    fn setup() {
393        let _ = logforth::starter_log::builder()
394            .dispatch(|d| {
395                d.filter(RustLogFilterBuilder::from_default_env().build())
396                    .append(Testing::default().with_layout(TextLayout::default()))
397            })
398            .try_apply();
399    }
400
401    #[tokio::test]
402    async fn test_list() -> Result<()> {
403        setup();
404
405        let mut iil = ImmutableIndexLayer::default();
406        for i in ["file", "dir/", "dir/file", "dir_without_prefix/file"] {
407            iil.insert(i.to_string())
408        }
409
410        let op = build_operator(iil);
411
412        let mut map = HashMap::new();
413        let mut set = HashSet::new();
414        let mut ds = op.lister("").await?;
415        while let Some(entry) = ds.try_next().await? {
416            debug!("got entry: {}", entry.path());
417            assert!(
418                set.insert(entry.path().to_string()),
419                "duplicated value: {}",
420                entry.path()
421            );
422            map.insert(entry.path().to_string(), entry.metadata().mode());
423        }
424
425        assert_eq!(map["file"], EntryMode::FILE);
426        assert_eq!(map["dir/"], EntryMode::DIR);
427        assert_eq!(map["dir_without_prefix/"], EntryMode::DIR);
428        Ok(())
429    }
430
431    #[tokio::test]
432    async fn test_scan() -> Result<()> {
433        setup();
434
435        let mut iil = ImmutableIndexLayer::default();
436        for i in ["file", "dir/", "dir/file", "dir_without_prefix/file"] {
437            iil.insert(i.to_string())
438        }
439
440        let op = build_operator(iil);
441
442        let mut ds = op.lister_with("/").recursive(true).await?;
443        let mut set = HashSet::new();
444        let mut map = HashMap::new();
445        while let Some(entry) = ds.try_next().await? {
446            debug!("got entry: {}", entry.path());
447            assert!(
448                set.insert(entry.path().to_string()),
449                "duplicated value: {}",
450                entry.path()
451            );
452            map.insert(entry.path().to_string(), entry.metadata().mode());
453        }
454
455        debug!("current files: {map:?}");
456
457        assert_eq!(map["file"], EntryMode::FILE);
458        assert_eq!(map["dir/"], EntryMode::DIR);
459        assert_eq!(map["dir_without_prefix/file"], EntryMode::FILE);
460        Ok(())
461    }
462
463    #[tokio::test]
464    async fn test_list_dir() -> Result<()> {
465        setup();
466
467        let mut iil = ImmutableIndexLayer::default();
468        for i in [
469            "dataset/stateful/ontime_2007_200.csv",
470            "dataset/stateful/ontime_2008_200.csv",
471            "dataset/stateful/ontime_2009_200.csv",
472        ] {
473            iil.insert(i.to_string())
474        }
475
476        let op = build_operator(iil);
477
478        //  List /
479        let mut map = HashMap::new();
480        let mut set = HashSet::new();
481        let mut ds = op.lister("/").await?;
482        while let Some(entry) = ds.try_next().await? {
483            assert!(
484                set.insert(entry.path().to_string()),
485                "duplicated value: {}",
486                entry.path()
487            );
488            map.insert(entry.path().to_string(), entry.metadata().mode());
489        }
490
491        assert_eq!(map.len(), 1);
492        assert_eq!(map["dataset/"], EntryMode::DIR);
493
494        //  List dataset/stateful/
495        let mut map = HashMap::new();
496        let mut set = HashSet::new();
497        let mut ds = op.lister("dataset/stateful/").await?;
498        while let Some(entry) = ds.try_next().await? {
499            assert!(
500                set.insert(entry.path().to_string()),
501                "duplicated value: {}",
502                entry.path()
503            );
504            map.insert(entry.path().to_string(), entry.metadata().mode());
505        }
506
507        assert_eq!(map["dataset/stateful/ontime_2007_200.csv"], EntryMode::FILE);
508        assert_eq!(map["dataset/stateful/ontime_2008_200.csv"], EntryMode::FILE);
509        assert_eq!(map["dataset/stateful/ontime_2009_200.csv"], EntryMode::FILE);
510        Ok(())
511    }
512
513    #[tokio::test]
514    async fn test_walk_top_down_dir() -> Result<()> {
515        setup();
516
517        let mut iil = ImmutableIndexLayer::default();
518        for i in [
519            "dataset/stateful/ontime_2007_200.csv",
520            "dataset/stateful/ontime_2008_200.csv",
521            "dataset/stateful/ontime_2009_200.csv",
522        ] {
523            iil.insert(i.to_string())
524        }
525
526        let op = build_operator(iil);
527
528        let mut ds = op.lister_with("/").recursive(true).await?;
529
530        let mut map = HashMap::new();
531        let mut set = HashSet::new();
532        while let Some(entry) = ds.try_next().await? {
533            assert!(
534                set.insert(entry.path().to_string()),
535                "duplicated value: {}",
536                entry.path()
537            );
538            map.insert(entry.path().to_string(), entry.metadata().mode());
539        }
540
541        debug!("current files: {map:?}");
542
543        assert_eq!(map["dataset/stateful/ontime_2007_200.csv"], EntryMode::FILE);
544        assert_eq!(map["dataset/stateful/ontime_2008_200.csv"], EntryMode::FILE);
545        assert_eq!(map["dataset/stateful/ontime_2009_200.csv"], EntryMode::FILE);
546        Ok(())
547    }
548}