use std::fmt;
use std::path::{Component, Path, PathBuf};
use crate::lookups::LOOKUPS_DIR;
use crate::project::{MACROS_DIR, PROJECT_FILE, REPORTS_DIR, TARGET_DIR};
pub const ENV: &str = crate::settings::TARGET_PATH;
pub const KEY: &str = "target_path";
pub const MARKER: &str = ".dre_target";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
Flag,
Env,
Project,
Default,
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Source::Flag => "--target-path",
Source::Env => ENV,
Source::Project => "`target_path` in dre_project.yml",
Source::Default => "the default target path",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetPath {
pub dir: PathBuf,
pub source: Source,
}
impl TargetPath {
pub fn default_for(root: &Path) -> TargetPath {
TargetPath {
dir: root.join(TARGET_DIR),
source: Source::Default,
}
}
}
pub fn resolve(root: &Path, flag: Option<&str>, project_value: Option<&str>) -> Result<TargetPath, String> {
let env = crate::settings::env(ENV);
let (value, source) = match (flag, env.as_deref(), project_value) {
(Some(v), _, _) => (v, Source::Flag),
(None, Some(v), _) if !v.is_empty() => (v, Source::Env),
(None, _, Some(v)) => (v, Source::Project),
_ => return Ok(TargetPath::default_for(root)),
};
let fail = |why: String| format!("{source}: target path `{value}` {why}");
if value.trim().is_empty() {
return Err(fail("is empty".into()));
}
if looks_like_url(value) {
return Err(fail(
"is a URL; the target path must be a local or mounted path (a Databricks Volume under /Volumes/..., an NFS/EFS share, a gcsfuse mount). To copy outputs to object storage, deliver them with a destination".into(),
));
}
let expanded = expand_home(value);
let dir = clean(&if expanded.is_absolute() {
expanded
} else {
root.join(expanded)
});
check(root, &dir).map_err(fail)?;
Ok(TargetPath { dir, source })
}
fn looks_like_url(v: &str) -> bool {
let Some((scheme, _)) = v.split_once("://") else {
return false;
};
scheme.len() > 1
&& scheme.starts_with(|c: char| c.is_ascii_alphabetic())
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || "+-.".contains(c))
}
fn expand_home(v: &str) -> PathBuf {
let home = || std::env::home_dir().unwrap_or_default();
if v == "~" {
return home();
}
match v.strip_prefix("~/").or_else(|| v.strip_prefix("~\\")) {
Some(rest) => home().join(rest),
None => PathBuf::from(v),
}
}
fn clean(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
Component::CurDir => {}
Component::ParentDir => {
if !out.pop() {
out.push(c);
}
}
c => out.push(c),
}
}
out
}
fn canonical(p: &Path) -> PathBuf {
let abs = clean(&std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf()));
let mut existing = abs.as_path();
let mut rest = Vec::new();
loop {
if let Ok(c) = existing.canonicalize() {
let mut out = strip_verbatim(c);
for r in rest.iter().rev() {
out.push(r);
}
return out;
}
match (existing.parent(), existing.file_name()) {
(Some(parent), Some(name)) => {
rest.push(name.to_os_string());
existing = parent;
}
_ => return abs,
}
}
}
fn strip_verbatim(p: PathBuf) -> PathBuf {
let s = p.to_string_lossy();
match s.strip_prefix(r"\\?\UNC\") {
Some(rest) => PathBuf::from(format!(r"\\{rest}")),
None => match s.strip_prefix(r"\\?\") {
Some(rest) => PathBuf::from(rest),
None => p,
},
}
}
fn check(root: &Path, dir: &Path) -> Result<(), String> {
let (root, dir) = (canonical(root), canonical(dir));
if dir == root {
return Err("is the project root; generated files can't mix with the project's sources".into());
}
if root.starts_with(&dir) {
return Err(
"contains the project; pick a folder outside it, or one inside it such as `target`".into(),
);
}
for inner in [REPORTS_DIR, MACROS_DIR, LOOKUPS_DIR, crate::plugins::DEPS_DIR] {
if dir.starts_with(root.join(inner)) {
return Err(format!(
"is inside `{inner}/`; generated files there would be read as project files"
));
}
}
Ok(())
}
pub fn inside(root: &Path, dir: &Path) -> Option<PathBuf> {
canonical(dir)
.strip_prefix(canonical(root))
.ok()
.map(Path::to_path_buf)
}
pub fn project_value(root: &Path) -> Option<String> {
let text = std::fs::read_to_string(root.join(PROJECT_FILE)).ok()?;
let v = crate::config::node::parse(&text).ok()?;
v.get(KEY)?.as_str().map(str::to_string)
}
pub fn project_flag_u64(root: &Path, key: &str) -> Option<u64> {
let text = std::fs::read_to_string(root.join(PROJECT_FILE)).ok()?;
let v = crate::config::node::parse(&text).ok()?;
let j = v.get("flags")?.get(key)?.to_json();
j.as_u64().or_else(|| j.as_str()?.trim().parse().ok())
}
pub fn ensure(dir: &Path) -> std::io::Result<()> {
let fresh = !dir.exists();
std::fs::create_dir_all(dir)?;
if fresh {
crate::fs::write_atomic(
&dir.join(MARKER),
b"DRE writes its generated files here; `dre clean` may delete this folder.\n",
)?;
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub enum Cleaned {
Removed,
Missing,
}
pub fn clean_dir(root: &Path, t: &TargetPath) -> Result<Cleaned, String> {
if !t.dir.exists() {
return Ok(Cleaned::Missing);
}
let default = canonical(&t.dir) == canonical(&root.join(TARGET_DIR));
if !default && !t.dir.join(MARKER).is_file() {
return Err(format!(
"refusing to delete {} (from {}): DRE didn't create it (it has no {MARKER} file). Check the path, or delete the folder yourself",
t.dir.display(),
t.source
));
}
std::fs::remove_dir_all(&t.dir).map_err(|e| format!("can't remove {}: {e}", t.dir.display()))?;
Ok(Cleaned::Removed)
}