1use crate::path::{ClioPathEnum, InOut};
2use crate::{
3 assert_is_dir, assert_not_dir, assert_writeable, impl_try_from, is_fifo, ClioPath, Error,
4 Result,
5};
6
7use is_terminal::IsTerminal;
8use std::convert::TryFrom;
9use std::ffi::OsStr;
10use std::fmt::{self, Debug, Display};
11use std::fs::{File, OpenOptions};
12use std::io::{self, Result as IoResult, Seek, Stderr, Stdout, Write};
13use std::path::Path;
14use tempfile::NamedTempFile;
15
16#[derive(Debug)]
17enum OutputStream {
18 Stdout(Stdout),
20 Stderr(Stderr),
22 Pipe(File),
24 File(File),
26 AtomicFile(NamedTempFile),
28 #[cfg(feature = "http")]
29 #[cfg_attr(docsrs, doc(cfg(feature = "http")))]
30 Http(Box<HttpWriter>),
32}
33
34#[cfg(feature = "http")]
35use crate::http::HttpWriter;
36#[derive(Debug)]
63pub struct Output {
64 path: ClioPath,
65 stream: OutputStream,
66}
67
68#[derive(Debug, PartialEq, Eq, Clone)]
90pub struct OutputPath {
91 path: ClioPath,
92}
93
94impl OutputStream {
95 fn new(path: &ClioPath, size: Option<u64>) -> Result<Self> {
97 Ok(match &path.path {
98 ClioPathEnum::Std(_) => OutputStream::Stdout(io::stdout()),
99 ClioPathEnum::Local(local_path) => {
100 if path.atomic && !path.is_fifo() {
101 assert_not_dir(path)?;
102 if let Some(parent) = path.safe_parent() {
103 assert_is_dir(parent)?;
104 let tmp = tempfile::Builder::new()
105 .prefix(".atomicwrite")
106 .tempfile_in(parent)?;
107 OutputStream::AtomicFile(tmp)
108 } else {
109 return Err(Error::not_found_error());
110 }
111 } else {
112 let file = open_rw(local_path)?;
113 if is_fifo(&file.metadata()?) {
114 OutputStream::Pipe(file)
115 } else {
116 if let Some(size) = size {
117 file.set_len(size)?;
118 }
119 OutputStream::File(file)
120 }
121 }
122 }
123 #[cfg(feature = "http")]
124 ClioPathEnum::Http(url) => {
125 OutputStream::Http(Box::new(HttpWriter::new(url.as_str(), size)?))
126 }
127 })
128 }
129}
130
131impl Output {
132 pub fn new<S: TryInto<ClioPath>>(path: S) -> Result<Self>
134 where
135 crate::Error: From<<S as TryInto<ClioPath>>::Error>,
136 {
137 Output::maybe_with_len(path.try_into()?, None)
138 }
139
140 pub(crate) fn maybe_with_len(path: ClioPath, size: Option<u64>) -> Result<Self> {
142 Ok(Output {
143 stream: OutputStream::new(&path, size)?,
144 path,
145 })
146 }
147
148 pub fn std() -> Self {
150 Output {
151 path: ClioPath::std().with_direction(InOut::Out),
152 stream: OutputStream::Stdout(io::stdout()),
153 }
154 }
155
156 pub fn std_err() -> Self {
158 Output {
159 path: ClioPath::std().with_direction(InOut::Out),
160 stream: OutputStream::Stderr(io::stderr()),
161 }
162 }
163
164 pub fn is_std(&self) -> bool {
166 matches!(self.stream, OutputStream::Stdout(_))
167 }
168
169 pub fn is_tty(&self) -> bool {
171 self.is_std() && std::io::stdout().is_terminal()
172 }
173
174 pub fn is_local(&self) -> bool {
177 self.path.is_local()
178 }
179
180 pub fn try_from_os_str(path: &OsStr) -> std::result::Result<Self, std::ffi::OsString> {
186 TryFrom::try_from(path).map_err(|e: Error| e.to_os_string(path))
187 }
188
189 pub fn finish(mut self) -> Result<()> {
193 self.flush()?;
194 match self.stream {
195 OutputStream::Stdout(_) => Ok(()),
196 OutputStream::Stderr(_) => Ok(()),
197 OutputStream::Pipe(_) => Ok(()),
198 OutputStream::File(file) => Ok(file.sync_data()?),
199 OutputStream::AtomicFile(tmp) => {
200 tmp.persist(self.path.path())?;
201 Ok(())
202 }
203 #[cfg(feature = "http")]
204 OutputStream::Http(http) => Ok(http.finish()?),
205 }
206 }
207
208 pub fn lock<'a>(&'a mut self) -> Box<dyn Write + 'a> {
222 match &mut self.stream {
223 OutputStream::Stdout(stdout) => Box::new(stdout.lock()),
224 OutputStream::Stderr(stderr) => Box::new(stderr.lock()),
225 OutputStream::Pipe(pipe) => Box::new(pipe),
226 OutputStream::File(file) => Box::new(file),
227 OutputStream::AtomicFile(file) => Box::new(file),
228 #[cfg(feature = "http")]
229 OutputStream::Http(http) => Box::new(http),
230 }
231 }
232
233 pub fn get_file(&mut self) -> Option<&mut File> {
236 match &mut self.stream {
237 OutputStream::File(file) => Some(file),
238 OutputStream::AtomicFile(file) => Some(file.as_file_mut()),
239 _ => None,
240 }
241 }
242
243 pub fn path(&self) -> &ClioPath {
245 &self.path
246 }
247
248 pub fn can_seek(&self) -> bool {
251 matches!(
252 self.stream,
253 OutputStream::File(_) | OutputStream::AtomicFile(_)
254 )
255 }
256}
257
258impl_try_from!(Output);
259
260impl Write for Output {
261 fn flush(&mut self) -> IoResult<()> {
262 match &mut self.stream {
263 OutputStream::Stdout(stdout) => stdout.flush(),
264 OutputStream::Stderr(stderr) => stderr.flush(),
265 OutputStream::Pipe(pipe) => pipe.flush(),
266 OutputStream::File(file) => file.flush(),
267 OutputStream::AtomicFile(file) => file.flush(),
268 #[cfg(feature = "http")]
269 OutputStream::Http(http) => http.flush(),
270 }
271 }
272 fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
273 match &mut self.stream {
274 OutputStream::Stdout(stdout) => stdout.write(buf),
275 OutputStream::Stderr(stderr) => stderr.write(buf),
276 OutputStream::Pipe(pipe) => pipe.write(buf),
277 OutputStream::File(file) => file.write(buf),
278 OutputStream::AtomicFile(file) => file.write(buf),
279 #[cfg(feature = "http")]
280 OutputStream::Http(http) => http.write(buf),
281 }
282 }
283}
284
285impl Seek for Output {
286 fn seek(&mut self, pos: io::SeekFrom) -> IoResult<u64> {
287 match &mut self.stream {
288 OutputStream::File(file) => file.seek(pos),
289 OutputStream::AtomicFile(file) => file.seek(pos),
290 _ => Err(Error::seek_error().into()),
291 }
292 }
293}
294
295impl OutputPath {
296 pub fn new<S: TryInto<ClioPath>>(path: S) -> Result<Self>
300 where
301 crate::Error: From<<S as TryInto<ClioPath>>::Error>,
302 {
303 let path: ClioPath = path.try_into()?.with_direction(InOut::Out);
304 if path.is_local() {
305 if path.is_file() && !path.atomic {
306 assert_writeable(&path)?;
307 } else {
308 #[cfg(target_os = "linux")]
309 if path.ends_with_slash() {
310 return Err(Error::dir_error());
311 }
312 assert_not_dir(&path)?;
313 if let Some(parent) = path.safe_parent() {
314 assert_is_dir(parent)?;
315 assert_writeable(parent)?;
316 } else {
317 return Err(Error::not_found_error());
318 }
319 }
320 }
321 Ok(OutputPath { path })
322 }
323
324 pub fn std() -> Self {
326 OutputPath {
327 path: ClioPath::std().with_direction(InOut::Out),
328 }
329 }
330
331 pub fn maybe_with_len(self, size: Option<u64>) -> Result<Output> {
333 Output::maybe_with_len(self.path, size)
334 }
335
336 pub fn create_with_len(self, size: u64) -> Result<Output> {
338 self.maybe_with_len(Some(size))
339 }
340
341 pub fn create(self) -> Result<Output> {
343 self.maybe_with_len(None)
344 }
345
346 pub fn path(&self) -> &ClioPath {
348 &self.path
349 }
350
351 pub fn is_std(&self) -> bool {
353 self.path.is_std()
354 }
355
356 pub fn is_tty(&self) -> bool {
358 self.is_std() && std::io::stdout().is_terminal()
359 }
360
361 pub fn is_local(&self) -> bool {
364 self.path.is_local()
365 }
366
367 pub fn can_seek(&self) -> bool {
372 self.path.is_local() && !self.path.is_fifo()
373 }
374}
375
376impl_try_from!(OutputPath: Clone);
377
378fn open_rw(path: &Path) -> io::Result<File> {
379 OpenOptions::new()
380 .read(true)
381 .write(true)
382 .create(true)
383 .truncate(true)
384 .open(path)
385 .or_else(|_| File::create(path))
386}