use crate::path::{ClioPathEnum, InOut};
use crate::{
assert_is_dir, assert_not_dir, assert_writeable, impl_try_from, is_fifo, ClioPath, Error,
Result,
};
use is_terminal::IsTerminal;
use std::convert::TryFrom;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Display};
use std::fs::{File, OpenOptions};
use std::io::{self, Result as IoResult, Seek, Stderr, Stdout, Write};
use std::path::Path;
use tempfile::NamedTempFile;
#[derive(Debug)]
enum OutputStream {
Stdout(Stdout),
Stderr(Stderr),
Pipe(File),
File(File),
AtomicFile(NamedTempFile),
#[cfg(feature = "http")]
#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
Http(Box<HttpWriter>),
}
#[cfg(feature = "http")]
use crate::http::HttpWriter;
#[derive(Debug)]
pub struct Output {
path: ClioPath,
stream: OutputStream,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OutputPath {
path: ClioPath,
}
impl OutputStream {
fn new(path: &ClioPath, size: Option<u64>) -> Result<Self> {
Ok(match &path.path {
ClioPathEnum::Std(_) => OutputStream::Stdout(io::stdout()),
ClioPathEnum::Local(local_path) => {
if path.atomic && !path.is_fifo() {
assert_not_dir(path)?;
if let Some(parent) = path.safe_parent() {
assert_is_dir(parent)?;
let tmp = tempfile::Builder::new()
.prefix(".atomicwrite")
.tempfile_in(parent)?;
OutputStream::AtomicFile(tmp)
} else {
return Err(Error::not_found_error());
}
} else {
let file = open_rw(local_path)?;
if is_fifo(&file.metadata()?) {
OutputStream::Pipe(file)
} else {
if let Some(size) = size {
file.set_len(size)?;
}
OutputStream::File(file)
}
}
}
#[cfg(feature = "http")]
ClioPathEnum::Http(url) => {
OutputStream::Http(Box::new(HttpWriter::new(url.as_str(), size)?))
}
})
}
}
impl Output {
pub fn new<S: TryInto<ClioPath>>(path: S) -> Result<Self>
where
crate::Error: From<<S as TryInto<ClioPath>>::Error>,
{
Output::maybe_with_len(path.try_into()?, None)
}
pub(crate) fn maybe_with_len(path: ClioPath, size: Option<u64>) -> Result<Self> {
Ok(Output {
stream: OutputStream::new(&path, size)?,
path,
})
}
pub fn std() -> Self {
Output {
path: ClioPath::std().with_direction(InOut::Out),
stream: OutputStream::Stdout(io::stdout()),
}
}
pub fn std_err() -> Self {
Output {
path: ClioPath::std().with_direction(InOut::Out),
stream: OutputStream::Stderr(io::stderr()),
}
}
pub fn is_std(&self) -> bool {
matches!(self.stream, OutputStream::Stdout(_))
}
pub fn is_tty(&self) -> bool {
self.is_std() && std::io::stdout().is_terminal()
}
pub fn is_local(&self) -> bool {
self.path.is_local()
}
pub fn try_from_os_str(path: &OsStr) -> std::result::Result<Self, std::ffi::OsString> {
TryFrom::try_from(path).map_err(|e: Error| e.to_os_string(path))
}
pub fn finish(mut self) -> Result<()> {
self.flush()?;
match self.stream {
OutputStream::Stdout(_) => Ok(()),
OutputStream::Stderr(_) => Ok(()),
OutputStream::Pipe(_) => Ok(()),
OutputStream::File(file) => Ok(file.sync_data()?),
OutputStream::AtomicFile(tmp) => {
tmp.persist(self.path.path())?;
Ok(())
}
#[cfg(feature = "http")]
OutputStream::Http(http) => Ok(http.finish()?),
}
}
pub fn lock<'a>(&'a mut self) -> Box<dyn Write + 'a> {
match &mut self.stream {
OutputStream::Stdout(stdout) => Box::new(stdout.lock()),
OutputStream::Stderr(stderr) => Box::new(stderr.lock()),
OutputStream::Pipe(pipe) => Box::new(pipe),
OutputStream::File(file) => Box::new(file),
OutputStream::AtomicFile(file) => Box::new(file),
#[cfg(feature = "http")]
OutputStream::Http(http) => Box::new(http),
}
}
pub fn get_file(&mut self) -> Option<&mut File> {
match &mut self.stream {
OutputStream::File(file) => Some(file),
OutputStream::AtomicFile(file) => Some(file.as_file_mut()),
_ => None,
}
}
pub fn path(&self) -> &ClioPath {
&self.path
}
pub fn can_seek(&self) -> bool {
matches!(
self.stream,
OutputStream::File(_) | OutputStream::AtomicFile(_)
)
}
}
impl_try_from!(Output);
impl Write for Output {
fn flush(&mut self) -> IoResult<()> {
match &mut self.stream {
OutputStream::Stdout(stdout) => stdout.flush(),
OutputStream::Stderr(stderr) => stderr.flush(),
OutputStream::Pipe(pipe) => pipe.flush(),
OutputStream::File(file) => file.flush(),
OutputStream::AtomicFile(file) => file.flush(),
#[cfg(feature = "http")]
OutputStream::Http(http) => http.flush(),
}
}
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
match &mut self.stream {
OutputStream::Stdout(stdout) => stdout.write(buf),
OutputStream::Stderr(stderr) => stderr.write(buf),
OutputStream::Pipe(pipe) => pipe.write(buf),
OutputStream::File(file) => file.write(buf),
OutputStream::AtomicFile(file) => file.write(buf),
#[cfg(feature = "http")]
OutputStream::Http(http) => http.write(buf),
}
}
}
impl Seek for Output {
fn seek(&mut self, pos: io::SeekFrom) -> IoResult<u64> {
match &mut self.stream {
OutputStream::File(file) => file.seek(pos),
OutputStream::AtomicFile(file) => file.seek(pos),
_ => Err(Error::seek_error().into()),
}
}
}
impl OutputPath {
pub fn new<S: TryInto<ClioPath>>(path: S) -> Result<Self>
where
crate::Error: From<<S as TryInto<ClioPath>>::Error>,
{
let path: ClioPath = path.try_into()?.with_direction(InOut::Out);
if path.is_local() {
if path.is_file() && !path.atomic {
assert_writeable(&path)?;
} else {
#[cfg(target_os = "linux")]
if path.ends_with_slash() {
return Err(Error::dir_error());
}
assert_not_dir(&path)?;
if let Some(parent) = path.safe_parent() {
assert_is_dir(parent)?;
assert_writeable(parent)?;
} else {
return Err(Error::not_found_error());
}
}
}
Ok(OutputPath { path })
}
pub fn std() -> Self {
OutputPath {
path: ClioPath::std().with_direction(InOut::Out),
}
}
pub fn maybe_with_len(self, size: Option<u64>) -> Result<Output> {
Output::maybe_with_len(self.path, size)
}
pub fn create_with_len(self, size: u64) -> Result<Output> {
self.maybe_with_len(Some(size))
}
pub fn create(self) -> Result<Output> {
self.maybe_with_len(None)
}
pub fn path(&self) -> &ClioPath {
&self.path
}
pub fn is_std(&self) -> bool {
self.path.is_std()
}
pub fn is_tty(&self) -> bool {
self.is_std() && std::io::stdout().is_terminal()
}
pub fn is_local(&self) -> bool {
self.path.is_local()
}
pub fn can_seek(&self) -> bool {
self.path.is_local() && !self.path.is_fifo()
}
}
impl_try_from!(OutputPath: Clone);
fn open_rw(path: &Path) -> io::Result<File> {
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.or_else(|_| File::create(path))
}