cranpose_services/content/
file.rs1use std::{
2 fs::File,
3 io::Write,
4 path::{Path, PathBuf},
5 rc::Rc,
6 time::UNIX_EPOCH,
7};
8
9use super::{
10 Content, ContentEntry, ContentError, ContentFolder, ContentFolderRef, ContentFuture,
11 ContentHandle, ContentMetadata, ContentReaderRef, ContentSinkRef,
12 threaded::{BlockingSink, open_threaded, run_threaded, threaded_sink},
13};
14
15fn io_error(path: &Path, error: std::io::Error) -> ContentError {
16 match error.kind() {
17 std::io::ErrorKind::NotFound => ContentError::NotFound(path.display().to_string()),
18 std::io::ErrorKind::PermissionDenied => {
19 ContentError::PermissionDenied(path.display().to_string())
20 }
21 _ => ContentError::Io(format!("{}: {error}", path.display())),
22 }
23}
24
25fn metadata_for(path: &Path) -> ContentMetadata {
26 let name = path.file_name().map_or_else(
27 || path.display().to_string(),
28 |name| name.to_string_lossy().into_owned(),
29 );
30 let mut metadata = ContentMetadata {
31 name,
32 mime_type: None,
33 len: None,
34 modified_millis: None,
35 identifier: path.display().to_string(),
36 };
37 if let Ok(stat) = std::fs::metadata(path) {
38 if stat.is_file() {
39 metadata.len = Some(stat.len());
40 }
41 metadata.modified_millis = stat
42 .modified()
43 .ok()
44 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
45 .map(|since| since.as_millis() as u64);
46 }
47 metadata
48}
49
50pub struct FileContent {
52 path: PathBuf,
53}
54
55impl FileContent {
56 pub fn new(path: impl Into<PathBuf>) -> Self {
58 Self { path: path.into() }
59 }
60
61 pub fn path(&self) -> &Path {
63 &self.path
64 }
65}
66
67impl Content for FileContent {
68 fn metadata(&self) -> ContentMetadata {
69 metadata_for(&self.path)
70 }
71
72 fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>> {
73 let opened = self.path.clone();
74 let path = self.path.clone();
75 open_threaded(
76 move || File::open(&opened).map_err(|error| io_error(&opened, error)),
77 move |error| io_error(&path, error),
78 )
79 }
80
81 fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
82 let path = self.path.clone();
83 run_threaded(move || std::fs::read(&path).map_err(|error| io_error(&path, error)))
84 }
85}
86
87pub fn file_content(path: impl Into<PathBuf>) -> ContentHandle {
89 Rc::new(FileContent::new(path))
90}
91
92pub struct FileFolder {
94 path: PathBuf,
95}
96
97impl FileFolder {
98 pub fn new(path: impl Into<PathBuf>) -> Self {
100 Self { path: path.into() }
101 }
102
103 pub fn path(&self) -> &Path {
105 &self.path
106 }
107}
108
109impl ContentFolder for FileFolder {
110 fn metadata(&self) -> ContentMetadata {
111 metadata_for(&self.path)
112 }
113
114 fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
115 let path = self.path.clone();
116 Box::pin(async move {
117 let listing = std::fs::read_dir(&path).map_err(|error| io_error(&path, error))?;
118 let mut entries = Vec::new();
119 for child in listing {
120 let child = child.map_err(|error| io_error(&path, error))?;
121 let child_path = child.path();
122 let is_dir = child
123 .file_type()
124 .map_or_else(|_| child_path.is_dir(), |kind| kind.is_dir());
125 if is_dir {
126 entries.push(ContentEntry::Folder(
127 Rc::new(FileFolder::new(child_path)) as ContentFolderRef
128 ));
129 } else {
130 entries.push(ContentEntry::File(
131 Rc::new(FileContent::new(child_path)) as ContentHandle
132 ));
133 }
134 }
135 entries.sort_by_key(|entry| entry.metadata().name);
136 Ok(entries)
137 })
138 }
139}
140
141pub fn file_folder(path: impl Into<PathBuf>) -> ContentFolderRef {
143 Rc::new(FileFolder::new(path))
144}
145
146pub struct FileSink {
151 destination: PathBuf,
152 staging: PathBuf,
153 file: Option<File>,
154}
155
156impl FileSink {
157 pub fn create(destination: impl Into<PathBuf>) -> Result<Self, ContentError> {
159 let destination = destination.into();
160 if let Some(parent) = destination.parent() {
161 std::fs::create_dir_all(parent).map_err(|error| io_error(parent, error))?;
162 }
163 let staging = staging_path(&destination);
164 let file = File::create(&staging).map_err(|error| io_error(&staging, error))?;
165 Ok(Self {
166 destination,
167 staging,
168 file: Some(file),
169 })
170 }
171
172 pub fn handle(self) -> ContentSinkRef {
174 threaded_sink(self)
175 }
176
177 pub fn destination(&self) -> &Path {
179 &self.destination
180 }
181}
182
183fn staging_path(destination: &Path) -> PathBuf {
184 let mut name = destination.file_name().map_or_else(
185 || "content".to_string(),
186 |name| name.to_string_lossy().into_owned(),
187 );
188 name.push_str(".partial");
189 destination.with_file_name(name)
190}
191
192impl BlockingSink for FileSink {
193 fn write(&mut self, bytes: &[u8]) -> Result<(), ContentError> {
194 let file = self
195 .file
196 .as_mut()
197 .ok_or_else(|| ContentError::Io("sink is already finished".into()))?;
198 file.write_all(bytes)
199 .map_err(|error| io_error(&self.staging, error))
200 }
201
202 fn finish(&mut self) -> Result<(), ContentError> {
203 let Some(mut file) = self.file.take() else {
204 return Ok(());
205 };
206 file.flush()
207 .map_err(|error| io_error(&self.staging, error))?;
208 file.sync_all()
209 .map_err(|error| io_error(&self.staging, error))?;
210 drop(file);
211 std::fs::rename(&self.staging, &self.destination)
212 .map_err(|error| io_error(&self.destination, error))
213 }
214}
215
216impl Drop for FileSink {
217 fn drop(&mut self) {
218 if self.file.is_some() {
219 let _ = std::fs::remove_file(&self.staging);
220 }
221 }
222}
223
224#[cfg(test)]
225#[path = "tests/file_tests.rs"]
226mod tests;