use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use dicom_dump::DumpOptions;
use dicom_web::DicomWebClient;
use tracing::{error, Level};
const ERROR_READ: i32 = -2;
const ERROR_TRANSCODE: i32 = -3;
const ERROR_WRITE: i32 = -4;
const ERROR_DUMP: i32 = -5;
const ERROR_OTHER: i32 = -128;
#[derive(ValueEnum, Clone, Default, Debug)]
enum DicomLevel {
#[default]
Study,
Series,
Instance,
}
#[derive(Debug, Subcommand)]
enum Mode {
Qido {
#[clap(long, short = 'f')]
fuzzy_matching: bool,
level: DicomLevel,
},
Wado {},
Stow {
#[clap(long = "study")]
study_uid: Option<String>,
#[clap(long = "instances")]
instances: Vec<PathBuf>,
},
Mwl {},
Asdo {
#[clap(long = "transaction")]
transaction_uid: Option<String>,
#[clap(long = "destination")]
destination: String,
#[clap(long = "dst-username")]
dst_username: Option<String>,
#[clap(long = "dst-password")]
dst_password: Option<String>,
#[clap(long = "dst-token")]
dst_token: Option<String>,
#[clap(long = "status", default_value_t = false)]
status: bool,
level: DicomLevel,
},
}
#[derive(Debug, Parser)]
#[command(version)]
struct App {
#[clap(short = 'o', long = "output")]
output: Option<PathBuf>,
#[clap(subcommand)]
mode: Mode,
#[clap(short = 'u', long = "url")]
url: String,
#[clap(long = "username")]
username: Option<String>,
#[clap(long = "password")]
password: Option<String>,
#[clap(long = "token")]
token: Option<String>,
#[clap(short = 'v', long = "verbose")]
verbose: bool,
}
#[tokio::main]
async fn main() {
let app = App::parse();
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_max_level(if app.verbose {
Level::DEBUG
} else {
Level::INFO
})
.finish(),
)
.unwrap_or_else(|e| {
error!("{}", snafu::Report::from_error(e));
});
let mut client = DicomWebClient::with_single_url(&app.url);
if let (Some(username), Some(password)) = (app.username, app.password) {
client.set_basic_auth(&username, &password);
}
if let Some(token) = app.token {
client.set_bearer_token(&token);
}
match app.mode {
Mode::Qido {
fuzzy_matching,
level,
} => {
println!("QIDO-RS mode");
let mut builder = match level {
DicomLevel::Study => client.query_studies(),
DicomLevel::Series => client.query_series(),
DicomLevel::Instance => client.query_instances(),
};
let results = builder
.with_fuzzymatching(fuzzy_matching)
.run()
.await
.unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_OTHER);
});
let mut i = 0;
println!("Received {} DICOM objects", results.len());
for dcm in results {
println!("DICOM object #{}", i);
DumpOptions::new().dump_object(&dcm).unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_DUMP);
});
i += 1;
}
}
Mode::Wado {} => {
println!("WADO-RS mode");
}
Mode::Stow {
study_uid,
instances,
} => {
println!("STOW-RS mode");
let builder = match study_uid {
Some(study_uid) => client.store_instances_in_study(&study_uid),
None => client.store_instances(),
};
let instances: Vec<_> = instances
.into_iter()
.map(|path| {
dicom_object::open_file(path).unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_READ);
})
})
.collect();
let instance_stream = futures_util::stream::iter(instances);
let stored_instances = builder
.with_instances(instance_stream)
.run()
.await
.unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_OTHER);
});
println!("Stored DICOM objects");
DumpOptions::new()
.dump_object(&stored_instances)
.unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_DUMP);
});
}
Mode::Mwl {} => {
println!("MWL-RS mode");
}
Mode::Asdo {
transaction_uid,
destination,
dst_username,
dst_password,
dst_token,
status,
level,
} => {
println!("ASDO-RS mode");
let status = if status {
let transaction_uid = transaction_uid.unwrap_or_else(|| {
eprintln!("Error: Transaction UID must be provided when --status is set");
std::process::exit(ERROR_OTHER);
});
let builder = match level {
DicomLevel::Study => client.send_studies_status(&transaction_uid),
_ => unimplemented!("Only study level is currently supported for ASDO-RS"),
};
builder.run().await.unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_OTHER);
})
} else {
let transaction_uid =
transaction_uid.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let mut builder = match level {
DicomLevel::Study => client.send_studies(&transaction_uid),
_ => unimplemented!("Only study level is currently supported for ASDO-RS"),
};
if let Some(username) = dst_username {
if let Some(password) = dst_password {
builder = builder.with_basic_auth(username, password);
} else {
eprintln!("Error: Destination password must be provided when destination username is set");
std::process::exit(ERROR_OTHER);
}
} else if let Some(token) = dst_token {
builder = builder.with_bearer_token(token);
}
builder
.with_destination(destination)
.run()
.await
.unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_OTHER);
})
};
DumpOptions::new().dump_object(&status).unwrap_or_else(|e| {
eprintln!("Error: {:?}", e);
std::process::exit(ERROR_DUMP);
});
}
}
}