use std::fs::{File, Metadata};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use super::protocol::{read_ack, write_header, write_payload_term, Header, ScpError};
#[derive(Default, Clone, Copy)]
pub struct ScpSendOptions {
pub recursive: bool,
pub preserve_times: bool,
}
pub struct Sender<S: Read + Write> {
stream: S,
}
impl<S: Read + Write> Sender<S> {
pub fn new(mut stream: S) -> Result<Self, ScpError> {
read_ack(&mut stream)?;
Ok(Self { stream })
}
pub fn send_path(&mut self, source: &Path, opts: &ScpSendOptions) -> Result<(), ScpError> {
let md = std::fs::metadata(source).map_err(ScpError::Io)?;
if md.is_dir() {
if !opts.recursive {
return Err(ScpError::Unexpected("source is a directory but -r not set"));
}
self.send_dir(source, opts)
} else if md.is_file() {
self.send_file(source, &md, opts)
} else {
Err(ScpError::Unexpected("source is neither file nor directory"))
}
}
pub fn send_file(
&mut self,
source: &Path,
md: &Metadata,
opts: &ScpSendOptions,
) -> Result<(), ScpError> {
if opts.preserve_times {
let (mtime, atime) = times_of(md);
write_header(&mut self.stream, &Header::Times { mtime, atime })?;
read_ack(&mut self.stream)?;
}
let mode = mode_of(md);
let name = basename_or(source)?;
let size = md.len();
write_header(
&mut self.stream,
&Header::File {
mode,
size,
name: name.clone(),
},
)?;
read_ack(&mut self.stream)?;
let mut f = File::open(source).map_err(ScpError::Io)?;
write_payload_term(&mut self.stream, &mut f, size)?;
read_ack(&mut self.stream)?;
Ok(())
}
pub fn send_dir(&mut self, source: &Path, opts: &ScpSendOptions) -> Result<(), ScpError> {
let md = std::fs::metadata(source).map_err(ScpError::Io)?;
if opts.preserve_times {
let (mtime, atime) = times_of(&md);
write_header(&mut self.stream, &Header::Times { mtime, atime })?;
read_ack(&mut self.stream)?;
}
let mode = mode_of(&md);
let name = basename_or(source)?;
write_header(
&mut self.stream,
&Header::Dir {
mode,
name: name.clone(),
},
)?;
read_ack(&mut self.stream)?;
let mut entries: Vec<PathBuf> = std::fs::read_dir(source)
.map_err(ScpError::Io)?
.filter_map(|r| r.ok().map(|e| e.path()))
.collect();
entries.sort();
for child in entries {
let cmd = std::fs::metadata(&child).map_err(ScpError::Io)?;
if cmd.is_dir() {
self.send_dir(&child, opts)?;
} else if cmd.is_file() {
self.send_file(&child, &cmd, opts)?;
} else {
continue;
}
}
write_header(&mut self.stream, &Header::EndDir)?;
read_ack(&mut self.stream)?;
Ok(())
}
pub fn stream_mut(&mut self) -> &mut S {
&mut self.stream
}
}
fn basename_or(p: &Path) -> Result<String, ScpError> {
p.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.ok_or(ScpError::BadName("non-UTF-8 basename"))
}
#[cfg(unix)]
fn mode_of(md: &Metadata) -> u32 {
use std::os::unix::fs::PermissionsExt;
md.permissions().mode() & 0o7777
}
#[cfg(not(unix))]
fn mode_of(md: &Metadata) -> u32 {
if md.permissions().readonly() {
0o444
} else if md.is_dir() {
0o755
} else {
0o644
}
}
fn times_of(md: &Metadata) -> (i64, i64) {
let mtime = md
.modified()
.ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let atime = md
.accessed()
.ok()
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(mtime);
(mtime, atime)
}