pub mod auth;
use std::io::Write;
#[derive(Debug)]
pub struct GDriveHashResult {
pub file_id: String,
pub bytes_read: u64,
pub sha256: Option<String>,
pub blake3: Option<String>,
}
pub fn parse_file_id(input: &str) -> Option<String> {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Some(id) = input.strip_prefix("gdrive://") {
let id = id.trim_end_matches('/');
return if id.is_empty() { None } else { Some(id.to_string()) };
}
if let Some(rest) = input
.strip_prefix("https://drive.google.com/file/d/")
.or_else(|| input.strip_prefix("http://drive.google.com/file/d/"))
{
let id = rest.split('/').next().unwrap_or("").trim();
return if id.is_empty() { None } else { Some(id.to_string()) };
}
if input.starts_with("https://drive.google.com/open")
|| input.starts_with("http://drive.google.com/open")
{
if let Some(id) = input.split("id=").nth(1) {
let id = id.split('&').next().unwrap_or("").trim();
return if id.is_empty() { None } else { Some(id.to_string()) };
}
return None;
}
if input.contains("://") {
return None;
}
if input.contains('/') {
return None;
}
Some(input.to_string())
}
#[cfg(feature = "remote")]
pub fn hash_gdrive_file(
file_id: &str,
algos: &[crate::algorithm::Algorithm],
sink: &mut dyn Write,
) -> Result<GDriveHashResult, Box<dyn std::error::Error>> {
let auth = auth::resolve_auth_mode();
hash_gdrive_file_with_auth_inner(file_id, &auth, algos, sink, None)
}
#[cfg(feature = "remote")]
pub fn hash_gdrive_file_with_auth(
file_id: &str,
auth_mode: &auth::GDriveAuthMode,
algos: &[crate::algorithm::Algorithm],
sink: &mut dyn Write,
) -> Result<GDriveHashResult, Box<dyn std::error::Error>> {
hash_gdrive_file_with_auth_inner(file_id, auth_mode, algos, sink, None)
}
#[cfg(feature = "remote")]
#[doc(hidden)]
pub fn hash_gdrive_file_with_auth_at(
file_id: &str,
auth_mode: &auth::GDriveAuthMode,
algos: &[crate::algorithm::Algorithm],
sink: &mut dyn Write,
api_base: Option<&str>,
) -> Result<GDriveHashResult, Box<dyn std::error::Error>> {
hash_gdrive_file_with_auth_inner(file_id, auth_mode, algos, sink, api_base)
}
#[cfg(feature = "remote")]
fn hash_gdrive_file_with_auth_inner(
file_id: &str,
auth_mode: &auth::GDriveAuthMode,
algos: &[crate::algorithm::Algorithm],
sink: &mut dyn Write,
api_base: Option<&str>,
) -> Result<GDriveHashResult, Box<dyn std::error::Error>> {
use crate::algorithm::Algorithm;
use auth::GDriveAuthMode;
let response = match auth_mode {
GDriveAuthMode::Public => {
let base = api_base.unwrap_or("https://drive.usercontent.google.com");
let url = format!("{base}/download?id={file_id}&export=download&confirm=t");
ureq::get(&url).call()?
}
GDriveAuthMode::UserOAuth { access_token } => {
let base = api_base.unwrap_or("https://www.googleapis.com");
let url = format!("{base}/drive/v3/files/{file_id}?alt=media");
ureq::get(&url)
.set("Authorization", &format!("Bearer {access_token}"))
.call()?
}
GDriveAuthMode::ServiceAccount { .. } => {
return Err(
"service account auth is not yet implemented — \
run `blazehash gdrive auth login` to authenticate as a user, \
or set GOOGLE_APPLICATION_CREDENTIALS and use `gcloud auth print-access-token`"
.into(),
);
}
};
if response.status() != 200 {
return Err(format!(
"Google Drive returned HTTP {} for file ID {file_id}",
response.status()
)
.into());
}
let mut body = response.into_reader();
let mut buf = [0u8; 65536];
let mut bytes_read: u64 = 0;
use sha2::Digest as _;
let mut sha256_hasher: Option<sha2::Sha256> = None;
let mut blake3_hasher: Option<blake3::Hasher> = None;
for algo in algos {
match algo {
Algorithm::Sha256 => sha256_hasher = Some(sha2::Sha256::new()),
Algorithm::Blake3 => blake3_hasher = Some(blake3::Hasher::new()),
_ => {}
}
}
loop {
let n = std::io::Read::read(&mut body, &mut buf)?;
if n == 0 {
break;
}
let chunk = &buf[..n];
bytes_read += n as u64;
if let Some(h) = &mut sha256_hasher {
sha2::Digest::update(h, chunk);
}
if let Some(h) = &mut blake3_hasher {
h.update(chunk);
}
}
if bytes_read == 0 {
return Err(format!(
"Google Drive returned empty body for file ID {file_id} — \
file may not be public or the token may be expired"
)
.into());
}
let sha256 = sha256_hasher.map(|h| format!("{:x}", sha2::Digest::finalize(h)));
let blake3 = blake3_hasher.map(|h| h.finalize().to_hex().to_string());
let hash_str = sha256.as_deref().or(blake3.as_deref()).unwrap_or("(no hash)");
writeln!(sink, "{hash_str} gdrive://{file_id}")?;
Ok(GDriveHashResult {
file_id: file_id.to_string(),
bytes_read,
sha256,
blake3,
})
}