millipede_storage_fs/
dataset.rs1use futures_util::{StreamExt, stream, stream::BoxStream};
2use millipede_core::storage::{
3 Dataset, DatasetInfo, ListOptions, Page, StorageError, StorageResult,
4};
5use serde_json::Value;
6use std::{collections::BTreeSet, path::Path, path::PathBuf, sync::Arc, time::SystemTime};
7use time::OffsetDateTime;
8use tokio::sync::{Mutex, RwLock};
9
10use crate::layout::{is_temporary_file, temporary_suffix};
11
12const MAX_SEQUENCE: u64 = 999_999_999;
13
14pub struct FsDataset {
16 name: String,
17 path: PathBuf,
18 operations: Arc<RwLock<()>>,
19 next_sequence: Mutex<u64>,
20}
21
22impl FsDataset {
23 pub(crate) async fn open(
24 name: String,
25 path: PathBuf,
26 operations: Arc<RwLock<()>>,
27 ) -> StorageResult<Self> {
28 let next_sequence = max_sequence(&path).await?.saturating_add(1).max(1);
29 Ok(Self {
30 name,
31 path,
32 operations,
33 next_sequence: Mutex::new(next_sequence),
34 })
35 }
36
37 pub(crate) async fn reset_sequence(&self) {
38 *self.next_sequence.lock().await = 1;
39 }
40
41 async fn read_items(&self) -> StorageResult<Vec<Value>> {
42 tokio::fs::create_dir_all(&self.path).await?;
43 let files = item_files(&self.path).await?;
44 let mut items = Vec::with_capacity(files.len());
45 for (_, path) in files {
46 let bytes = tokio::fs::read(&path).await?;
47 match serde_json::from_slice(&bytes) {
48 Ok(item) => items.push(item),
49 Err(error) => {
50 tracing::warn!(
54 dataset = %self.name,
55 path = %path.display(),
56 %error,
57 "skipping corrupt dataset item"
58 );
59 }
60 }
61 }
62 Ok(items)
63 }
64
65 async fn append_locked(&self, next: &mut u64, item: &Value) -> StorageResult<()> {
66 if *next > MAX_SEQUENCE {
67 return Err(StorageError::Backend(anyhow::anyhow!(
68 "dataset {} exhausted its nine-digit sequence space",
69 self.name
70 )));
71 }
72 tokio::fs::create_dir_all(&self.path).await?;
73 let destination = self.path.join(format!("{next:09}.json"));
74 let temporary = self
75 .path
76 .join(format!("{next:09}.json.{}", temporary_suffix()));
77 tokio::fs::write(&temporary, serde_json::to_vec_pretty(item)?).await?;
78 if let Err(error) = tokio::fs::rename(&temporary, &destination).await {
79 let _ = tokio::fs::remove_file(&temporary).await;
80 return Err(error.into());
81 }
82 *next += 1;
83 Ok(())
84 }
85}
86
87#[async_trait::async_trait]
88impl Dataset for FsDataset {
89 async fn push_json(&self, item: Value) -> StorageResult<()> {
90 let _operation = self.operations.read().await;
91 let mut next = self.next_sequence.lock().await;
92 self.append_locked(&mut next, &item).await
93 }
94
95 async fn push_json_batch(&self, items: Vec<Value>) -> StorageResult<()> {
96 let _operation = self.operations.read().await;
97 let mut next = self.next_sequence.lock().await;
98 for item in items {
99 self.append_locked(&mut next, &item).await?;
100 }
101 Ok(())
102 }
103
104 async fn list_raw(&self, opts: ListOptions) -> StorageResult<Page<Value>> {
105 let _operation = self.operations.read().await;
106 let _guard = self.next_sequence.lock().await;
107 let mut items = self.read_items().await?;
108 let total = items.len() as u64;
109 if opts.desc {
110 items.reverse();
111 }
112 let items = items
113 .into_iter()
114 .skip(usize::try_from(opts.offset).unwrap_or(usize::MAX))
115 .take(opts.limit.map_or(usize::MAX, |limit| {
116 usize::try_from(limit).unwrap_or(usize::MAX)
117 }))
118 .collect();
119 Ok(Page {
120 items,
121 total,
122 offset: opts.offset,
123 limit: opts.limit,
124 })
125 }
126
127 fn stream_raw(&self, opts: ListOptions) -> BoxStream<'_, StorageResult<Value>> {
128 let page = async move {
129 match self.list_raw(opts).await {
130 Ok(page) => page.items.into_iter().map(Ok).collect::<Vec<_>>(),
131 Err(error) => vec![Err(error)],
132 }
133 };
134 Box::pin(stream::once(page).flat_map(stream::iter))
135 }
136
137 async fn export_json(&self, path: &Path) -> StorageResult<()> {
138 let _operation = self.operations.read().await;
139 let _guard = self.next_sequence.lock().await;
140 let items = self.read_items().await?;
141 tokio::fs::write(path, serde_json::to_vec_pretty(&items)?).await?;
142 Ok(())
143 }
144
145 async fn export_csv(&self, path: &Path) -> StorageResult<()> {
146 let _operation = self.operations.read().await;
147 let _guard = self.next_sequence.lock().await;
148 let items = self.read_items().await?;
149 let mut columns = BTreeSet::new();
150 for item in &items {
151 let object = item.as_object().ok_or(StorageError::Unsupported(
152 "export_csv requires object items",
153 ))?;
154 columns.extend(object.keys().cloned());
155 }
156 let columns: Vec<_> = columns.into_iter().collect();
157 let mut rows = vec![
158 columns
159 .iter()
160 .map(|key| csv_field(key))
161 .collect::<Vec<_>>()
162 .join(","),
163 ];
164 for item in &items {
165 let object = item.as_object().expect("objects validated above");
166 rows.push(
167 columns
168 .iter()
169 .map(|key| match object.get(key) {
170 None => String::new(),
171 Some(Value::String(value)) => csv_field(value),
172 Some(value) => csv_field(&value.to_string()),
173 })
174 .collect::<Vec<_>>()
175 .join(","),
176 );
177 }
178 tokio::fs::write(path, rows.join("\r\n")).await?;
179 Ok(())
180 }
181
182 async fn info(&self) -> StorageResult<DatasetInfo> {
188 let _operation = self.operations.read().await;
189 let _guard = self.next_sequence.lock().await;
190 tokio::fs::create_dir_all(&self.path).await?;
191 let directory = tokio::fs::metadata(&self.path).await?;
192 let directory_modified = directory.modified()?;
193 let created = directory.created().unwrap_or(directory_modified);
194 let files = item_files(&self.path).await?;
195 let mut modified = directory_modified;
196 for (_, path) in &files {
197 let candidate = tokio::fs::metadata(path).await?.modified()?;
198 modified = modified.max(candidate);
199 }
200 Ok(DatasetInfo::new(
201 self.name.clone(),
202 files.len() as u64,
203 system_time(created),
204 system_time(modified),
205 ))
206 }
207}
208
209async fn max_sequence(path: &Path) -> StorageResult<u64> {
210 Ok(item_files(path)
211 .await?
212 .last()
213 .map_or(0, |(sequence, _)| *sequence))
214}
215
216async fn item_files(path: &Path) -> StorageResult<Vec<(u64, PathBuf)>> {
217 let mut entries = tokio::fs::read_dir(path).await?;
218 let mut files = Vec::new();
219 while let Some(entry) = entries.next_entry().await? {
220 if !entry.file_type().await?.is_file() {
221 continue;
222 }
223 let name = entry.file_name();
224 let Some(name) = name.to_str() else {
225 continue;
226 };
227 if is_temporary_file(name) {
228 continue;
229 }
230 let Some(sequence) = dataset_sequence(name) else {
231 continue;
232 };
233 files.push((sequence, entry.path()));
234 }
235 files.sort_unstable_by_key(|(sequence, _)| *sequence);
236 Ok(files)
237}
238
239fn dataset_sequence(name: &str) -> Option<u64> {
240 if name.len() != 14 || !name.ends_with(".json") {
241 return None;
242 }
243 let digits = &name[..9];
244 if !digits.bytes().all(|byte| byte.is_ascii_digit()) {
245 return None;
246 }
247 digits.parse().ok().filter(|sequence| *sequence > 0)
248}
249
250fn csv_field(value: &str) -> String {
251 if value.contains([',', '"', '\r', '\n']) {
252 format!("\"{}\"", value.replace('"', "\"\""))
253 } else {
254 value.to_owned()
255 }
256}
257
258fn system_time(value: SystemTime) -> OffsetDateTime {
259 OffsetDateTime::from(value)
260}