Skip to content
Closed
Changes from 1 commit
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
78 changes: 77 additions & 1 deletion dragonfly-client/src/bin/dfget/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ struct Args {
)]
overwrite: bool,

#[arg(
long = "skip-if-exists",
default_value_t = false,
env = "DFGET_SKIP_IF_EXISTS",
help = "Specify whether to skip the download and return success if the output file already exists. For recursive downloads, existing files will be skipped. Cannot be used with `--overwrite=true`"
)]
skip_if_exists: bool,

#[arg(
long = "force-hard-link",
default_value_t = false,
Expand Down Expand Up @@ -991,6 +999,15 @@ async fn download(
progress_bar: ProgressBar,
download_client: DfdaemonDownloadClient,
) -> Result<()> {
if should_skip_existing_output(&args) {
info!(
"output path {} already exists, skip download",
args.output.display()
);
progress_bar.finish_with_message(format!("{} (skipped)", args.output.display()));
return Ok(());
}

let url = Url::parse(args.url.as_str()).or_err(ErrorType::ParseError)?;
let object_storage = if object_storage::Scheme::is_supported(url.scheme()) {
Some(ObjectStorage {
Expand Down Expand Up @@ -1367,6 +1384,12 @@ fn convert_args(mut args: Args) -> Args {
/// The validation prevents common user errors and potential security issues before
/// starting the download process.
fn validate_args(args: &Args) -> Result<()> {
if args.overwrite && args.skip_if_exists {
return Err(Error::ValidationError(
"`--skip-if-exists` cannot be used with `--overwrite=true`".to_string(),
));
}

// If the URL is a directory, the output path should be a directory.
if args.url.path().ends_with('/') && !args.output.is_dir() {
return Err(Error::ValidationError(format!(
Expand Down Expand Up @@ -1396,7 +1419,7 @@ fn validate_args(args: &Args) -> Result<()> {
}
}

if !args.overwrite && absolute_path.exists() {
if !args.overwrite && !args.skip_if_exists && absolute_path.exists() {
return Err(Error::ValidationError(format!(
"output path {} is already exist",
args.output.to_string_lossy()
Expand Down Expand Up @@ -1435,6 +1458,10 @@ fn validate_args(args: &Args) -> Result<()> {
Ok(())
}

fn should_skip_existing_output(args: &Args) -> bool {
!args.url.path().ends_with('/') && args.skip_if_exists && args.output.exists()
}

/// Validates that a path string is a normal relative path without unsafe components.
///
/// This function ensures that a given path is both relative (doesn't start with '/')
Expand Down Expand Up @@ -1541,6 +1568,20 @@ mod tests {

let result = validate_args(&args);
assert!(result.is_ok());

// Skip download when the output file already exists.
let existing_file_path = tempdir.path().join("existing.txt");
std::fs::File::create(&existing_file_path).unwrap();
let args = Args::parse_from(vec![
"dfget",
"http://test.local/existing.txt",
"--output",
existing_file_path.as_os_str().to_str().unwrap(),
"--skip-if-exists",
]);

let result = validate_args(&args);
assert!(result.is_ok());
}

#[test]
Expand Down Expand Up @@ -1599,6 +1640,17 @@ mod tests {
Args::parse_from(vec!["dfget", "http://test.local/test.txt", "--output", "/"]),
"output path / is not exist".to_string(),
),
(
Args::parse_from(vec![
"dfget",
"http://test.local/test.txt",
"--output",
non_exist_file_path.as_os_str().to_str().unwrap(),
"--overwrite",
"--skip-if-exists",
]),
"`--skip-if-exists` cannot be used with `--overwrite=true`".to_string(),
),
];

for (args, error_message) in test_cases {
Expand All @@ -1611,6 +1663,30 @@ mod tests {
}
}

#[test]
fn should_skip_existing_output_file() {
let tempdir = tempfile::tempdir().unwrap();
let existing_file_path = tempdir.path().join("test.txt");
std::fs::File::create(&existing_file_path).unwrap();

let args = Args::parse_from(vec![
"dfget",
"http://test.local/test.txt",
"--output",
existing_file_path.as_os_str().to_str().unwrap(),
"--skip-if-exists",
]);
assert!(should_skip_existing_output(&args));

let args = Args::parse_from(vec![
"dfget",
"http://test.local/test.txt",
"--output",
existing_file_path.as_os_str().to_str().unwrap(),
]);
assert!(!should_skip_existing_output(&args));
}

#[test]
fn should_make_output_by_entry() {
let url = Url::parse("http://example.com/root/").unwrap();
Expand Down
Loading