cranpose_services/content/
file.rs1use std::{
2 cell::RefCell,
3 fs::File,
4 io::{Read, Write},
5 path::{Path, PathBuf},
6 rc::Rc,
7 time::UNIX_EPOCH,
8};
9
10use super::{
11 Content, ContentEntry, ContentError, ContentFolder, ContentFolderRef, ContentFuture,
12 ContentHandle, ContentMetadata, ContentReader, ContentReaderRef, ContentSink, ContentSinkRef,
13 DEFAULT_CHUNK_LEN,
14};
15
16fn io_error(path: &Path, error: std::io::Error) -> ContentError {
17 match error.kind() {
18 std::io::ErrorKind::NotFound => ContentError::NotFound(path.display().to_string()),
19 std::io::ErrorKind::PermissionDenied => {
20 ContentError::PermissionDenied(path.display().to_string())
21 }
22 _ => ContentError::Io(format!("{}: {error}", path.display())),
23 }
24}
25
26fn metadata_for(path: &Path) -> ContentMetadata {
27 let name = path.file_name().map_or_else(
28 || path.display().to_string(),
29 |name| name.to_string_lossy().into_owned(),
30 );
31 let mut metadata = ContentMetadata {
32 name,
33 mime_type: None,
34 len: None,
35 modified_millis: None,
36 identifier: path.display().to_string(),
37 };
38 if let Ok(stat) = std::fs::metadata(path) {
39 if stat.is_file() {
40 metadata.len = Some(stat.len());
41 }
42 metadata.modified_millis = stat
43 .modified()
44 .ok()
45 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
46 .map(|since| since.as_millis() as u64);
47 }
48 metadata
49}
50
51pub struct FileContent {
53 path: PathBuf,
54}
55
56impl FileContent {
57 pub fn new(path: impl Into<PathBuf>) -> Self {
59 Self { path: path.into() }
60 }
61
62 pub fn path(&self) -> &Path {
64 &self.path
65 }
66}
67
68impl Content for FileContent {
69 fn metadata(&self) -> ContentMetadata {
70 metadata_for(&self.path)
71 }
72
73 fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>> {
74 let path = self.path.clone();
75 Box::pin(async move {
76 let file = File::open(&path).map_err(|error| io_error(&path, error))?;
77 Ok(Rc::new(FileReader {
78 path,
79 file: RefCell::new(Some(file)),
80 }) as ContentReaderRef)
81 })
82 }
83
84 fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
85 let path = self.path.clone();
86 Box::pin(async move { std::fs::read(&path).map_err(|error| io_error(&path, error)) })
87 }
88}
89
90pub fn file_content(path: impl Into<PathBuf>) -> ContentHandle {
92 Rc::new(FileContent::new(path))
93}
94
95struct FileReader {
96 path: PathBuf,
97 file: RefCell<Option<File>>,
98}
99
100impl ContentReader for FileReader {
101 fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>> {
102 Box::pin(async move {
103 let mut slot = self.file.borrow_mut();
104 let Some(file) = slot.as_mut() else {
105 return Ok(None);
106 };
107 let mut buffer = vec![0u8; DEFAULT_CHUNK_LEN];
108 let mut filled = 0;
109 while filled < buffer.len() {
110 match file.read(&mut buffer[filled..]) {
111 Ok(0) => break,
112 Ok(read) => filled += read,
113 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
114 Err(error) => return Err(io_error(&self.path, error)),
115 }
116 }
117 if filled == 0 {
118 *slot = None;
119 return Ok(None);
120 }
121 buffer.truncate(filled);
122 Ok(Some(buffer))
123 })
124 }
125}
126
127pub struct FileFolder {
129 path: PathBuf,
130}
131
132impl FileFolder {
133 pub fn new(path: impl Into<PathBuf>) -> Self {
135 Self { path: path.into() }
136 }
137
138 pub fn path(&self) -> &Path {
140 &self.path
141 }
142}
143
144impl ContentFolder for FileFolder {
145 fn metadata(&self) -> ContentMetadata {
146 metadata_for(&self.path)
147 }
148
149 fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
150 let path = self.path.clone();
151 Box::pin(async move {
152 let listing = std::fs::read_dir(&path).map_err(|error| io_error(&path, error))?;
153 let mut entries = Vec::new();
154 for child in listing {
155 let child = child.map_err(|error| io_error(&path, error))?;
156 let child_path = child.path();
157 let is_dir = child
158 .file_type()
159 .map_or_else(|_| child_path.is_dir(), |kind| kind.is_dir());
160 if is_dir {
161 entries.push(ContentEntry::Folder(
162 Rc::new(FileFolder::new(child_path)) as ContentFolderRef
163 ));
164 } else {
165 entries.push(ContentEntry::File(
166 Rc::new(FileContent::new(child_path)) as ContentHandle
167 ));
168 }
169 }
170 entries.sort_by_key(|entry| entry.metadata().name);
171 Ok(entries)
172 })
173 }
174}
175
176pub fn file_folder(path: impl Into<PathBuf>) -> ContentFolderRef {
178 Rc::new(FileFolder::new(path))
179}
180
181pub struct FileSink {
184 destination: PathBuf,
185 staging: PathBuf,
186 file: RefCell<Option<File>>,
187}
188
189impl FileSink {
190 pub fn create(destination: impl Into<PathBuf>) -> Result<Self, ContentError> {
192 let destination = destination.into();
193 if let Some(parent) = destination.parent() {
194 std::fs::create_dir_all(parent).map_err(|error| io_error(parent, error))?;
195 }
196 let staging = staging_path(&destination);
197 let file = File::create(&staging).map_err(|error| io_error(&staging, error))?;
198 Ok(Self {
199 destination,
200 staging,
201 file: RefCell::new(Some(file)),
202 })
203 }
204
205 pub fn handle(self) -> ContentSinkRef {
207 Rc::new(self)
208 }
209
210 pub fn destination(&self) -> &Path {
212 &self.destination
213 }
214}
215
216fn staging_path(destination: &Path) -> PathBuf {
217 let mut name = destination.file_name().map_or_else(
218 || "content".to_string(),
219 |name| name.to_string_lossy().into_owned(),
220 );
221 name.push_str(".partial");
222 destination.with_file_name(name)
223}
224
225impl ContentSink for FileSink {
226 fn write_chunk(&self, bytes: Vec<u8>) -> ContentFuture<'_, Result<(), ContentError>> {
227 Box::pin(async move {
228 let mut slot = self.file.borrow_mut();
229 let file = slot
230 .as_mut()
231 .ok_or_else(|| ContentError::Io("sink is already finished".into()))?;
232 file.write_all(&bytes)
233 .map_err(|error| io_error(&self.staging, error))
234 })
235 }
236
237 fn finish(&self) -> ContentFuture<'_, Result<(), ContentError>> {
238 Box::pin(async move {
239 let Some(mut file) = self.file.borrow_mut().take() else {
240 return Ok(());
241 };
242 file.flush()
243 .map_err(|error| io_error(&self.staging, error))?;
244 file.sync_all()
245 .map_err(|error| io_error(&self.staging, error))?;
246 drop(file);
247 std::fs::rename(&self.staging, &self.destination)
248 .map_err(|error| io_error(&self.destination, error))
249 })
250 }
251}
252
253impl Drop for FileSink {
254 fn drop(&mut self) {
255 if self.file.borrow().is_some() {
256 let _ = std::fs::remove_file(&self.staging);
257 }
258 }
259}
260
261#[cfg(test)]
262#[path = "tests/file_tests.rs"]
263mod tests;