1use sealed::sealed;
14use std::fs::File;
15use std::io::{BufRead, BufReader, BufWriter, Write};
16use std::path::{Path, PathBuf};
17use std::sync::{Arc, RwLock};
18
19use crate::config::{Config, Configurable};
20use crate::error::StamError;
21use crate::types::*;
22
23const KNOWN_EXTENSIONS: &[&str; 14] = &[
24 ".store.stam.json",
25 ".annotationset.stam.json",
26 ".stam.json",
27 ".store.stam.cbor",
28 ".stam.cbor",
29 ".store.stam.csv",
30 ".annotationset.stam.csv",
31 ".annotations.stam.csv",
32 ".stam.csv",
33 ".json",
34 ".cbor",
35 ".csv",
36 ".txt",
37 ".md",
38];
39
40pub(crate) fn get_filepath(filename: &str, workdir: Option<&Path>) -> Result<PathBuf, StamError> {
43 if filename == "-" {
44 return Ok(filename.into());
46 }
47 if filename.starts_with("https://") || filename.starts_with("http://") {
48 return Err(StamError::OtherError("Loading URLs is not supported yet"));
50 }
51
52 let path = if filename.starts_with("file://") {
53 PathBuf::from(&filename[7..])
55 } else {
56 PathBuf::from(filename)
57 };
58 if path.is_absolute() {
59 Ok(path)
60 } else {
61 if let Some(workdir) = workdir {
63 let path = workdir.join(&path);
64 Ok(path)
66 } else {
67 Ok(path)
70 }
71 }
72}
73
74pub(crate) fn open_file(filename: &str, config: &Config) -> Result<File, StamError> {
76 let found_filename = get_filepath(filename, config.workdir())?;
77 debug(config, || {
78 format!("open_file: {:?} at {:?}", filename, found_filename)
79 });
80 File::open(found_filename.as_path()).map_err(|e| {
81 StamError::IOError(
82 e,
83 found_filename
84 .as_path()
85 .to_str()
86 .expect("path must be valid unicode")
87 .to_owned(),
88 "Opening file for reading failed",
89 )
90 })
91}
92
93pub(crate) fn create_file(filename: &str, config: &Config) -> Result<File, StamError> {
95 let found_filename = get_filepath(filename, config.workdir())?;
96 debug(config, || {
97 format!(
98 "create_file: {:?}, workdir: {:?}",
99 found_filename,
100 config.workdir()
101 )
102 });
103 File::create(found_filename.as_path()).map_err(|e| {
104 StamError::IOError(
105 e,
106 found_filename
107 .as_path()
108 .to_str()
109 .expect("path must be valid unicode")
110 .to_owned(),
111 "Opening file for reading failed",
112 )
113 })
114}
115
116pub(crate) fn open_file_reader(
118 filename: &str,
119 config: &Config,
120) -> Result<Box<dyn BufRead>, StamError> {
121 if filename == "-" {
122 Ok(Box::new(std::io::stdin().lock()))
124 } else {
125 Ok(Box::new(BufReader::new(open_file(filename, config)?)))
126 }
127}
128
129pub(crate) fn open_file_writer(
131 filename: &str,
132 config: &Config,
133) -> Result<Box<dyn Write>, StamError> {
134 if filename == "-" {
135 Ok(Box::new(std::io::stdout()))
136 } else {
137 Ok(Box::new(BufWriter::new(create_file(filename, config)?)))
138 }
139}
140
141pub(crate) fn strip_known_extension(s: &str) -> &str {
143 for extension in KNOWN_EXTENSIONS.iter() {
144 if s.ends_with(extension) {
145 return &s[0..s.len() - extension.len()];
146 }
147 }
148 s
149}
150
151pub(crate) fn sanitize_id_to_filename(id: &str) -> String {
155 let mut id = id.replace("://", ".").replace(&['/', '\\', ':', '?'], ".");
156 for extension in KNOWN_EXTENSIONS.iter() {
157 if id.ends_with(extension) {
158 id.truncate(id.len() - extension.len());
159 }
160 }
161 id
162}
163
164pub(crate) fn filename_without_workdir<'a>(filename: &'a str, config: &Config) -> &'a str {
165 if let Some(workdir) = config.workdir().map(|x| x.to_str().expect("valid utf-8")) {
167 if filename.starts_with(workdir) {
168 let filename = &filename[workdir.len()..];
169 if filename.starts_with(&['/', '\\']) {
170 return &filename[1..];
171 } else {
172 return filename;
173 }
174 }
175 }
176 filename
177}
178
179#[sealed(pub(crate))] #[allow(private_bounds)]
181pub trait AssociatedFile: Configurable + ChangeMarker {
182 fn filename(&self) -> Option<&str>;
183
184 fn set_filename(&mut self, filename: &str) -> &mut Self;
186
187 fn with_filename(mut self, filename: &str) -> Self
189 where
190 Self: Sized,
191 {
192 self.set_filename(filename);
193 self
194 }
195
196 fn dirname(&self) -> Option<PathBuf> {
199 if let Some(mut storedir) = self.filename().map(|s| {
200 let pb: PathBuf = s.into();
201 pb
202 }) {
203 storedir.pop();
204 if let Some(workdir) = self.config().workdir.as_ref() {
205 let mut workdir = workdir.clone();
206 workdir.extend(&storedir);
207 debug(self.config(), || {
208 format!("dirname(): workdir + storedir = {:?}", workdir)
209 });
210 return Some(workdir);
211 } else {
212 debug(self.config(), || {
213 format!("dirname(): storedir = {:?}", storedir)
214 });
215 return Some(storedir);
216 }
217 } else if let Some(workdir) = self.config().workdir.as_ref() {
218 debug(self.config(), || {
219 format!("dirname(): workdir = {:?}", workdir)
220 });
221 return Some(workdir.clone());
222 }
223 debug(self.config(), || format!("dirname(): none"));
224 None
225 }
226
227 fn filename_without_extension(&self) -> Option<&str> {
229 if let Some(filename) = self.filename() {
230 Some(strip_known_extension(filename))
231 } else {
232 None
233 }
234 }
235
236 fn filename_without_workdir(&self) -> Option<&str> {
239 if let Some(filename) = self.filename() {
240 Some(filename_without_workdir(filename, self.config()))
241 } else {
242 None
243 }
244 }
245}
246
247#[sealed(pub(crate))] pub(crate) trait ChangeMarker {
249 fn change_marker(&self) -> &Arc<RwLock<bool>>;
250
251 fn changed(&self) -> bool {
252 let mut result = true;
253 if let Ok(changed) = self.change_marker().read() {
254 result = *changed;
255 }
256 result
257 }
258
259 fn mark_changed(&self) {
260 if let Ok(mut changed) = self.change_marker().write() {
261 *changed = true;
262 }
263 }
264
265 fn mark_unchanged(&self) {
266 if let Ok(mut changed) = self.change_marker().write() {
267 *changed = false;
268 }
269 }
270}