use super::common::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostVolume {
Path(PathBuf),
UserRelativePath(PathBuf),
Name(String),
}
impl fmt::Display for HostVolume {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HostVolume::Path(path) => {
let p = path_str_to_docker(path.to_str().ok_or(fmt::Error)?);
if path.is_absolute() || p.starts_with("./") || p.starts_with("../") {
write!(f, "{}", p)
} else {
write!(f, "./{}", p)
}
}
HostVolume::UserRelativePath(path) => {
let p = path.to_str().ok_or(fmt::Error)?;
if path.is_absolute() {
return Err(fmt::Error);
}
write!(f, "~/{}", p)
}
HostVolume::Name(name) => write!(f, "{}", name),
}
}
}
#[cfg(not(windows))]
fn path_str_to_docker(s: &str) -> String {
s.to_owned()
}
#[cfg(windows)]
fn path_str_to_docker(s: &str) -> String {
lazy_static! {
static ref DRIVE_LETTER: Regex =
Regex::new(r#"^(?P<letter>[A-Za-z]):\\"#).unwrap();
}
DRIVE_LETTER
.replace(s, |caps: &Captures| {
format!("/{}/", caps.name("letter").unwrap().as_str().to_lowercase())
})
.replace("\\", "/")
}
impl FromStr for HostVolume {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
lazy_static! {
static ref HOST_VOLUME: Regex =
Regex::new(r#"^(\.{0,2}/.*)|~/(.+)|([^./~].*)$"#).unwrap();
}
let caps = HOST_VOLUME
.captures(s)
.ok_or_else(|| Error::invalid_value("host volume", s))?;
if let Some(path) = caps.get(1) {
let fixed_path = path_str_from_docker(path.as_str())?;
Ok(HostVolume::Path(Path::new(&fixed_path).to_owned()))
} else if let Some(path) = caps.get(2) {
let fixed_path = path_str_from_docker(path.as_str())?;
Ok(HostVolume::UserRelativePath(
Path::new(&fixed_path).to_owned(),
))
} else if let Some(name) = caps.get(3) {
Ok(HostVolume::Name(name.as_str().to_owned()))
} else {
unreachable!()
}
}
}
#[cfg(not(windows))]
fn path_str_from_docker(s: &str) -> Result<String> {
Ok(s.to_owned())
}
#[cfg(windows)]
fn path_str_from_docker(s: &str) -> Result<String> {
if s.starts_with("/") {
lazy_static! {
static ref DRIVE_LETTER: Regex =
Regex::new(r#"/(?P<letter>[A-Za-z])/"#).unwrap();
}
if DRIVE_LETTER.is_match(s) {
Ok(DRIVE_LETTER.replace(s, "$letter:\\").replace("/", "\\"))
} else {
Err(Error::ConvertMountedPathToWindows(s.to_owned()))
}
} else {
Ok(s.replace("/", "\\"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeMount {
pub host: Option<HostVolume>,
pub container: String,
pub mode: VolumeModes,
#[doc(hidden)]
pub _hidden: (),
}
impl VolumeMount {
pub fn host<P1, P2>(host: P1, container: P2) -> VolumeMount
where
P1: Into<PathBuf>,
P2: Into<String>,
{
VolumeMount {
host: Some(HostVolume::Path(host.into())),
container: container.into(),
mode: Default::default(),
_hidden: (),
}
}
pub fn named<S, P>(name: S, container: P) -> VolumeMount
where
S: Into<String>,
P: Into<String>,
{
VolumeMount {
host: Some(HostVolume::Name(name.into())),
container: container.into(),
mode: Default::default(),
_hidden: (),
}
}
pub fn anonymous<P>(container: P) -> VolumeMount
where
P: Into<String>,
{
VolumeMount {
host: None,
container: container.into(),
mode: Default::default(),
_hidden: (),
}
}
}
impl_interpolatable_value!(VolumeMount);
impl fmt::Display for VolumeMount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.host.is_none() && self.mode != Default::default() {
return Err(fmt::Error);
}
match &self.host {
Some(host) => write!(f, "{}:", host)?,
None => {}
}
write!(f, "{}", &self.container)?;
if self.mode != Default::default() {
write!(f, ":{}", self.mode)?
}
Ok(())
}
}
impl FromStr for VolumeMount {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let items = s.split(':').collect::<Vec<_>>();
match items.len() {
1 => Ok(VolumeMount {
host: None,
container: items[0].to_owned(),
mode: Default::default(),
_hidden: (),
}),
2 => Ok(VolumeMount {
host: Some(FromStr::from_str(items[0])?),
container: items[1].to_owned(),
mode: Default::default(),
_hidden: (),
}),
3 => Ok(VolumeMount {
host: Some(FromStr::from_str(items[0])?),
container: items[1].to_owned(),
mode: FromStr::from_str(items[2])?,
_hidden: (),
}),
_ => Err(Error::invalid_value("volume", s)),
}
}
}
#[test]
fn portable_volume_mounts_should_have_string_representations() {
let vol1 = VolumeMount::anonymous("/var/lib");
let vol2 = VolumeMount::named("named", "/var/lib");
let pairs = vec![(vol1, "/var/lib"), (vol2, "named:/var/lib")];
for (mode, s) in pairs {
assert_eq!(mode.to_string(), s);
assert_eq!(mode, VolumeMount::from_str(s).unwrap());
}
}
#[test]
#[cfg(not(windows))]
fn unix_windows_volume_mounts_should_have_string_representations() {
let vol3 = VolumeMount {
mode: VolumeModes::ReadOnly,
..VolumeMount::host("/etc/foo", "/etc/myfoo")
};
let pairs = vec![(vol3, "/etc/foo:/etc/myfoo:ro")];
for (mode, s) in pairs {
assert_eq!(mode.to_string(), s);
assert_eq!(mode, VolumeMount::from_str(s).unwrap());
}
}
#[test]
#[cfg(windows)]
fn windows_volume_mounts_should_have_string_representations() {
let vol3 = VolumeMount {
mode: VolumeModes::ReadOnly,
..VolumeMount::host("c:\\home\\smith\\foo", "/etc/myfoo")
};
let vol4 = VolumeMount::host(".\\foo", "/etc/myfoo");
let pairs = vec![
(vol3, "/c/home/smith/foo:/etc/myfoo:ro"),
(vol4, "./foo:/etc/myfoo"),
];
for (mode, s) in pairs {
assert_eq!(mode.to_string(), s);
assert_eq!(mode, VolumeMount::from_str(s).unwrap());
}
}