Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub enum DtooError {
#[error("Failed to read {path}: {source}")]
FileRead { path: String, source: BoxError },

#[error("Permission denied: {path}")]
PermissionDenied { path: String },

#[error("Failed to process {path}: {message}")]
FileProcess { path: String, message: String },

Expand Down
94 changes: 82 additions & 12 deletions src/fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,14 @@ pub fn fingerprint_file(path: &Path) -> Result<String, DtooError> {
path.to_path_buf()
};

let mut file = File::open(&target).map_err(|source| DtooError::FileRead {
path: target.display().to_string(),
source: Box::new(source),
})?;
let display_path = path.to_string_lossy().to_string();
let mut file = File::open(&target).map_err(|source| map_io_error(&display_path, source))?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let bytes = file
.read(&mut buffer)
.map_err(|source| DtooError::FileRead {
path: target.display().to_string(),
source: Box::new(source),
})?;
.map_err(|source| map_io_error(&display_path, source))?;
if bytes == 0 {
break;
}
Expand All @@ -43,6 +38,21 @@ pub fn fingerprint_file(path: &Path) -> Result<String, DtooError> {
Ok(format!("sha256:{}", hex::encode(hasher.finalize())))
}

/// Return the display file name used in `dtoo fingerprint` output.
pub fn display_name(path: &Path) -> String {
if is_cloud_path(path) {
let value = path.to_string_lossy();
if let Some(last) = value.rsplit('/').find(|segment| !segment.is_empty()) {
return last.to_string();
}
return value.to_string();
}

path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string())
}

fn is_cloud_path(path: &Path) -> bool {
let value = path.to_string_lossy();
value.starts_with("s3://")
Expand All @@ -68,13 +78,19 @@ fn download_cloud_blob(path: &Path) -> Result<PathBuf, DtooError> {
sql: sql.clone(),
source: Box::new(source),
})?;
let bytes: Vec<u8> = stmt
.query_row([], |row| row.get(0))
.map_err(|source| DtooError::Sql {
let bytes: Vec<u8> = stmt.query_row([], |row| row.get(0)).map_err(|source| {
let rendered = source.to_string();
if should_map_missing_credentials(&rendered) {
return DtooError::Config {
message: format!("{} credentials not configured", cloud_provider(path)),
};
}
DtooError::Sql {
context: "fingerprint".to_string(),
sql,
source: Box::new(source),
})?;
}
})?;

let temp = std::env::temp_dir().join(format!(
"dtoo-fingerprint-{}.bin",
Expand All @@ -94,6 +110,41 @@ fn escape_sql_literal(input: &str) -> String {
input.replace('\'', "''")
}

fn map_io_error(path: &str, source: std::io::Error) -> DtooError {
match source.kind() {
std::io::ErrorKind::NotFound => DtooError::FileNotFound {
path: path.to_string(),
},
std::io::ErrorKind::PermissionDenied => DtooError::PermissionDenied {
path: path.to_string(),
},
_ => DtooError::FileRead {
path: path.to_string(),
source: Box::new(source),
},
}
}

fn should_map_missing_credentials(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
lower.contains("credential")
|| lower.contains("access key")
|| lower.contains("secret key")
|| lower.contains("no provider")
|| lower.contains("authorization")
}

fn cloud_provider(path: &Path) -> &'static str {
let value = path.to_string_lossy();
if value.starts_with("s3://") {
"s3"
} else if value.starts_with("gs://") {
"gcs"
} else {
"azure"
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -112,4 +163,23 @@ mod tests {
assert!(hash.starts_with("sha256:"));
std::fs::remove_file(path).ok();
}

#[test]
fn returns_file_not_found_error_for_missing_file() {
let missing = PathBuf::from("/tmp/dtoo-missing-fingerprint-input.bin");
let err = fingerprint_file(&missing).expect_err("missing file should fail");
assert!(matches!(err, DtooError::FileNotFound { .. }));
}

#[test]
fn display_name_uses_basename_for_local_path() {
let name = display_name(Path::new("/tmp/data/sales.parquet"));
assert_eq!(name, "sales.parquet");
}

#[test]
fn display_name_uses_last_segment_for_cloud_path() {
let name = display_name(Path::new("s3://bucket/path/to/sales.parquet"));
assert_eq!(name, "sales.parquet");
}
}
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use std::process::ExitCode;

use cli::{Cli, Commands, QueryArgs};
use error::DtooError;
use fingerprint::fingerprint_file;
use fingerprint::{display_name as fingerprint_display_name, fingerprint_file};
use inspect::run as run_inspect;
use profile_command::run as run_profile;
use query_pipeline::QueryPipeline;
Expand Down Expand Up @@ -72,7 +72,7 @@ fn dispatch(cli: Cli) -> Result<(), DtooError> {
Commands::Inspect(args) => run_inspect(&args),
Commands::Fingerprint(args) => {
let hash = fingerprint_file(&args.path)?;
println!("{hash} {}", args.path.display());
println!("{hash} {}", fingerprint_display_name(&args.path));
Ok(())
}
}
Expand Down
Loading