1use crate::{normalize_key, normalize_prefix, BlobStore, BoxFuture, PutOpts, StorageError};
4use bytes::Bytes;
5use std::path::{Path, PathBuf};
6
7#[derive(Clone)]
8pub struct LocalStore {
9 root: PathBuf,
10}
11
12impl LocalStore {
13 pub fn new(root: impl Into<PathBuf>) -> Self {
14 Self { root: root.into() }
15 }
16
17 fn path_for(&self, key: &str) -> Result<PathBuf, StorageError> {
18 let key = normalize_key(key)?;
19 Ok(self.root.join(key))
20 }
21}
22
23fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> Result<(), StorageError> {
24 let entries = match std::fs::read_dir(dir) {
25 Ok(e) => e,
26 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
27 Err(e) => return Err(e.into()),
28 };
29 for entry in entries {
30 let entry = entry?;
31 let path = entry.path();
32 if path.is_dir() {
33 collect_files(root, &path, out)?;
34 } else if path.is_file() {
35 let rel = path
36 .strip_prefix(root)
37 .map_err(|e| StorageError::Msg(format!("list strip_prefix: {e}")))?;
38 out.push(rel.to_string_lossy().replace('\\', "/"));
39 }
40 }
41 Ok(())
42}
43
44impl BlobStore for LocalStore {
45 fn put<'a>(
46 &'a self,
47 key: &'a str,
48 data: Bytes,
49 _opts: PutOpts,
50 ) -> BoxFuture<'a, Result<(), StorageError>> {
51 Box::pin(async move {
52 let path = self.path_for(key)?;
53 if let Some(parent) = path.parent() {
54 if !parent.as_os_str().is_empty() {
55 tokio::fs::create_dir_all(parent).await?;
56 }
57 }
58 tokio::fs::write(&path, &data).await?;
59 Ok(())
60 })
61 }
62
63 fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Bytes>, StorageError>> {
64 Box::pin(async move {
65 let path = self.path_for(key)?;
66 match tokio::fs::read(&path).await {
67 Ok(bytes) => Ok(Some(Bytes::from(bytes))),
68 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
69 Err(e) => Err(e.into()),
70 }
71 })
72 }
73
74 fn delete<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<(), StorageError>> {
75 Box::pin(async move {
76 let path = self.path_for(key)?;
77 match tokio::fs::remove_file(&path).await {
78 Ok(()) => Ok(()),
79 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
80 Err(e) => Err(e.into()),
81 }
82 })
83 }
84
85 fn exists<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool, StorageError>> {
86 Box::pin(async move {
87 let path = self.path_for(key)?;
88 Ok(Path::new(&path).exists())
89 })
90 }
91
92 fn list<'a>(&'a self, prefix: &'a str) -> BoxFuture<'a, Result<Vec<String>, StorageError>> {
93 Box::pin(async move {
94 let prefix = normalize_prefix(prefix)?;
95 let start = if prefix.is_empty() {
96 self.root.clone()
97 } else {
98 self.root.join(&prefix)
99 };
100 let mut keys = Vec::new();
101 if start.is_file() {
102 keys.push(prefix);
103 return Ok(keys);
104 }
105 collect_files(&self.root, &start, &mut keys)?;
106 if !prefix.is_empty() && start.is_dir() {
108 } else if !prefix.is_empty() && !start.exists() {
110 keys.clear();
112 collect_files(&self.root, &self.root, &mut keys)?;
113 keys.retain(|k| k == &prefix || k.starts_with(&format!("{prefix}/")));
114 }
115 keys.sort();
116 Ok(keys)
117 })
118 }
119}