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    type Composer = oio::Composer;
159
160    fn info(&self) -> ServiceInfo {
161        self.inner.info()
162    }
163
164    fn capability(&self) -> Capability {
165        let mut capability = self.inner.capability();
166        capability.list = true;
167        capability.list_with_recursive = true;
168        capability
169    }
170
171    fn compose(&self, ctx: &OperationContext, to: &str, args: OpCompose) -> Result<Self::Composer> {
172        self.inner.compose(ctx, to, args)
173    }
174
175    async fn create_dir(
176        &self,
177        ctx: &OperationContext,
178        path: &str,
179        args: OpCreateDir,
180    ) -> Result<RpCreateDir> {
181        self.inner.create_dir(ctx, path, args).await
182    }
183
184    async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
185        self.inner.stat(ctx, path, args).await
186    }
187
188    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
189        self.inner.read(ctx, path, args)
190    }
191
192    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
193        self.inner.write(ctx, path, args)
194    }
195
196    fn copy(
197        &self,
198        ctx: &OperationContext,
199        from: &str,
200        to: &str,
201        args: OpCopy,
202    ) -> Result<Self::Copier> {
203        self.inner.copy(ctx, from, to, args)
204    }
205
206    fn list(&self, _ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
207        let mut path = path;
208        if path == "/" {
209            path = ""
210        }
211
212        let idx = if args.recursive() {
213            self.children_flat(path)
214        } else {
215            self.children_hierarchy(path)
216        };
217
218        Ok(ImmutableDir::new(idx))
219    }
220
221    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
222        self.inner.delete(ctx)
223    }
224
225    async fn rename(
226        &self,
227        ctx: &OperationContext,
228        from: &str,
229        to: &str,
230        args: OpRename,
231    ) -> Result<RpRename> {
232        self.inner.rename(ctx, from, to, args).await
233    }
234
235    async fn restore(
236        &self,
237        ctx: &OperationContext,
238        path: &str,
239        args: OpRestore,
240    ) -> Result<RpRestore> {
241        self.inner.restore(ctx, path, args).await
242    }
243
244    async fn presign(
245        &self,
246        ctx: &OperationContext,
247        path: &str,
248        args: OpPresign,
249    ) -> Result<RpPresign> {
250        self.inner.presign(ctx, path, args).await
251    }
252}
253
254#[doc(hidden)]
255pub struct ImmutableDir {
256    idx: IntoIter<String>,
257}
258
259impl ImmutableDir {
260    fn new(idx: Vec<String>) -> Self {
261        Self {
262            idx: idx.into_iter(),
263        }
264    }
265
266    fn inner_next(&mut self) -> Option<oio::Entry> {
267        self.idx.next().map(|v| {
268            let metadata = if v.ends_with('/') {
269                MetadataBuilder::dir()
270            } else {
271                MetadataBuilder::unknown()
272            };
273            oio::Entry::with(v, metadata.build())
274        })
275    }
276}
277
278impl oio::List for ImmutableDir {
279    async fn next(&mut self) -> Result<Option<oio::Entry>> {
280        Ok(self.inner_next())
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use std::collections::HashMap;
287    use std::sync::Arc;
288
289    use super::*;
290    use futures::TryStreamExt;
291    use log::debug;
292    use logforth::append::Testing;
293    use logforth::filter::rustlog::RustLogFilterBuilder;
294    use logforth::layout::TextLayout;
295
296    #[derive(Debug)]
297    struct MockService;
298
299    impl Service for MockService {
300        type Reader = ();
301        type Writer = ();
302        type Lister = ();
303        type Deleter = ();
304        type Copier = ();
305        type Composer = ();
306
307        fn info(&self) -> ServiceInfo {
308            ServiceInfo::with_scheme("mock")
309        }
310
311        fn capability(&self) -> Capability {
312            Capability {
313                list: true,
314                list_with_recursive: true,
315                ..Default::default()
316            }
317        }
318
319        async fn create_dir(
320            &self,
321            _: &OperationContext,
322            _: &str,
323            _: OpCreateDir,
324        ) -> Result<RpCreateDir> {
325            Err(Error::new(
326                ErrorKind::Unsupported,
327                "operation is not supported",
328            ))
329        }
330
331        async fn stat(&self, _: &OperationContext, _: &str, _: OpStat) -> Result<RpStat> {
332            Err(Error::new(
333                ErrorKind::Unsupported,
334                "operation is not supported",
335            ))
336        }
337
338        fn read(&self, _ctx: &OperationContext, _: &str, _: OpRead) -> Result<Self::Reader> {
339            Err(Error::new(
340                ErrorKind::Unsupported,
341                "operation is not supported",
342            ))
343        }
344
345        fn write(&self, _ctx: &OperationContext, _: &str, _: OpWrite) -> Result<Self::Writer> {
346            Err(Error::new(
347                ErrorKind::Unsupported,
348                "operation is not supported",
349            ))
350        }
351
352        fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
353            Err(Error::new(
354                ErrorKind::Unsupported,
355                "operation is not supported",
356            ))
357        }
358
359        fn list(&self, _ctx: &OperationContext, _: &str, _: OpList) -> Result<Self::Lister> {
360            Err(Error::new(
361                ErrorKind::Unsupported,
362                "operation is not supported",
363            ))
364        }
365
366        fn copy(&self, _: &OperationContext, _: &str, _: &str, _: OpCopy) -> Result<Self::Copier> {
367            Err(Error::new(
368                ErrorKind::Unsupported,
369                "operation is not supported",
370            ))
371        }
372
373        async fn rename(
374            &self,
375            _: &OperationContext,
376            _: &str,
377            _: &str,
378            _: OpRename,
379        ) -> Result<RpRename> {
380            Err(Error::new(
381                ErrorKind::Unsupported,
382                "operation is not supported",
383            ))
384        }
385
386        async fn presign(&self, _: &OperationContext, _: &str, _: OpPresign) -> Result<RpPresign> {
387            Err(Error::new(
388                ErrorKind::Unsupported,
389                "operation is not supported",
390            ))
391        }
392    }
393
394    fn build_operator(layer: ImmutableIndexLayer) -> Operator {
395        Operator::from_parts(OperationContext::default(), Arc::new(MockService)).layer(layer)
396    }
397
398    fn setup() {
399        let _ = logforth::starter_log::builder()
400            .dispatch(|d| {
401                d.filter(RustLogFilterBuilder::from_default_env().build())
402                    .append(Testing::default().with_layout(TextLayout::default()))
403            })
404            .try_apply();
405    }
406
407    #[tokio::test]
408    async fn test_list() -> Result<()> {
409        setup();
410
411        let mut iil = ImmutableIndexLayer::default();
412        for i in ["file", "dir/", "dir/file", "dir_without_prefix/file"] {
413            iil.insert(i.to_string())
414        }
415
416        let op = build_operator(iil);
417
418        let mut map = HashMap::new();
419        let mut set = HashSet::new();
420        let mut ds = op.lister("").await?;
421        while let Some(entry) = ds.try_next().await? {
422            debug!("got entry: {}", entry.path());
423            assert!(
424                set.insert(entry.path().to_string()),
425                "duplicated value: {}",
426                entry.path()
427            );
428            map.insert(entry.path().to_string(), entry.metadata().mode());
429        }
430
431        assert_eq!(map["file"], EntryMode::Unknown);
432        assert_eq!(map["dir/"], EntryMode::DIR);
433        assert_eq!(map["dir_without_prefix/"], EntryMode::DIR);
434        Ok(())
435    }
436
437    #[tokio::test]
438    async fn test_scan() -> Result<()> {
439        setup();
440
441        let mut iil = ImmutableIndexLayer::default();
442        for i in ["file", "dir/", "dir/file", "dir_without_prefix/file"] {
443            iil.insert(i.to_string())
444        }
445
446        let op = build_operator(iil);
447
448        let mut ds = op.lister_with("/").recursive(true).await?;
449        let mut set = HashSet::new();
450        let mut map = HashMap::new();
451        while let Some(entry) = ds.try_next().await? {
452            debug!("got entry: {}", entry.path());
453            assert!(
454                set.insert(entry.path().to_string()),
455                "duplicated value: {}",
456                entry.path()
457            );
458            map.insert(entry.path().to_string(), entry.metadata().mode());
459        }
460
461        debug!("current files: {map:?}");
462
463        assert_eq!(map["file"], EntryMode::Unknown);
464        assert_eq!(map["dir/"], EntryMode::DIR);
465        assert_eq!(map["dir_without_prefix/file"], EntryMode::Unknown);
466        Ok(())
467    }
468
469    #[tokio::test]
470    async fn test_list_dir() -> Result<()> {
471        setup();
472
473        let mut iil = ImmutableIndexLayer::default();
474        for i in [
475            "dataset/stateful/ontime_2007_200.csv",
476            "dataset/stateful/ontime_2008_200.csv",
477            "dataset/stateful/ontime_2009_200.csv",
478        ] {
479            iil.insert(i.to_string())
480        }
481
482        let op = build_operator(iil);
483
484        //  List /
485        let mut map = HashMap::new();
486        let mut set = HashSet::new();
487        let mut ds = op.lister("/").await?;
488        while let Some(entry) = ds.try_next().await? {
489            assert!(
490                set.insert(entry.path().to_string()),
491                "duplicated value: {}",
492                entry.path()
493            );
494            map.insert(entry.path().to_string(), entry.metadata().mode());
495        }
496
497        assert_eq!(map.len(), 1);
498        assert_eq!(map["dataset/"], EntryMode::DIR);
499
500        //  List dataset/stateful/
501        let mut map = HashMap::new();
502        let mut set = HashSet::new();
503        let mut ds = op.lister("dataset/stateful/").await?;
504        while let Some(entry) = ds.try_next().await? {
505            assert!(
506                set.insert(entry.path().to_string()),
507                "duplicated value: {}",
508                entry.path()
509            );
510            map.insert(entry.path().to_string(), entry.metadata().mode());
511        }
512
513        assert_eq!(
514            map["dataset/stateful/ontime_2007_200.csv"],
515            EntryMode::Unknown
516        );
517        assert_eq!(
518            map["dataset/stateful/ontime_2008_200.csv"],
519            EntryMode::Unknown
520        );
521        assert_eq!(
522            map["dataset/stateful/ontime_2009_200.csv"],
523            EntryMode::Unknown
524        );
525        Ok(())
526    }
527
528    #[tokio::test]
529    async fn test_walk_top_down_dir() -> Result<()> {
530        setup();
531
532        let mut iil = ImmutableIndexLayer::default();
533        for i in [
534            "dataset/stateful/ontime_2007_200.csv",
535            "dataset/stateful/ontime_2008_200.csv",
536            "dataset/stateful/ontime_2009_200.csv",
537        ] {
538            iil.insert(i.to_string())
539        }
540
541        let op = build_operator(iil);
542
543        let mut ds = op.lister_with("/").recursive(true).await?;
544
545        let mut map = HashMap::new();
546        let mut set = HashSet::new();
547        while let Some(entry) = ds.try_next().await? {
548            assert!(
549                set.insert(entry.path().to_string()),
550                "duplicated value: {}",
551                entry.path()
552            );
553            map.insert(entry.path().to_string(), entry.metadata().mode());
554        }
555
556        debug!("current files: {map:?}");
557
558        assert_eq!(
559            map["dataset/stateful/ontime_2007_200.csv"],
560            EntryMode::Unknown
561        );
562        assert_eq!(
563            map["dataset/stateful/ontime_2008_200.csv"],
564            EntryMode::Unknown
565        );
566        assert_eq!(
567            map["dataset/stateful/ontime_2009_200.csv"],
568            EntryMode::Unknown
569        );
570        Ok(())
571    }
572}