use std::fmt;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use async_trait::async_trait;
use serde_json::json;
use crate::downloads::store::DownloadSource;
use crate::downloads::DownloadError;
use crate::fingerprint::apply::CdpTransport;
pub const STAGING_DIRECTORY: &str = ".browser-commander-staging";
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DownloadFailure {
Cancelled,
Failed,
}
impl DownloadFailure {
pub fn as_str(&self) -> &'static str {
match self {
Self::Cancelled => "cancelled",
Self::Failed => "failed",
}
}
}
impl fmt::Display for DownloadFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub fn classify_failure(reason: &str) -> DownloadFailure {
if reason.to_ascii_lowercase().contains("cancel") {
DownloadFailure::Cancelled
} else {
DownloadFailure::Failed
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DownloadStart {
pub engine_handle: String,
pub url: Option<String>,
pub suggested_filename: Option<String>,
pub mime_type: Option<String>,
}
#[async_trait]
pub trait DownloadSink: Send + Sync {
fn started(&self, start: DownloadStart) -> String;
async fn finished(&self, id: String, source: DownloadSource);
fn failed(&self, id: String, kind: DownloadFailure, reason: String);
}
pub struct SourceHandle {
pub staging_directory: PathBuf,
pub detach: Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>,
}
impl fmt::Debug for SourceHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceHandle")
.field("staging_directory", &self.staging_directory)
.finish_non_exhaustive()
}
}
pub fn prepare_staging_directory(root: &Path) -> Result<PathBuf, DownloadError> {
let staging_directory = root.join(STAGING_DIRECTORY);
std::fs::create_dir_all(&staging_directory).map_err(|source| {
DownloadError::DirectoryNotCreated {
root: staging_directory.clone(),
source,
}
})?;
let _ = crate::downloads::destination::restrict(
&staging_directory,
crate::downloads::destination::ARTIFACT_DIRECTORY_MODE,
);
Ok(staging_directory)
}
pub async fn set_download_behavior(
transport: &dyn CdpTransport,
staging_directory: &Path,
) -> anyhow::Result<()> {
transport
.send(
"Browser.setDownloadBehavior",
json!({
"behavior": "allow",
"downloadPath": staging_directory.to_string_lossy(),
"eventsEnabled": true,
}),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::downloads::test_support::{RecordingTransport, TempDir};
#[test]
fn reads_a_cancellation_out_of_the_engines_own_words() {
assert_eq!(
classify_failure("Download canceled by the user"),
DownloadFailure::Cancelled
);
assert_eq!(
classify_failure("download cancelled"),
DownloadFailure::Cancelled
);
assert_eq!(classify_failure("net::ERR_FAILED"), DownloadFailure::Failed);
assert_eq!(classify_failure(""), DownloadFailure::Failed);
}
#[test]
fn names_the_two_outcomes_the_way_the_other_languages_do() {
assert_eq!(DownloadFailure::Cancelled.to_string(), "cancelled");
assert_eq!(DownloadFailure::Failed.to_string(), "failed");
}
#[test]
fn creates_the_staging_directory_inside_the_managed_root() {
let temp = TempDir::new("bc-staging");
let root = temp.path().join("downloads");
let staging = prepare_staging_directory(&root).unwrap();
assert_eq!(staging, root.join(STAGING_DIRECTORY));
assert!(staging.is_dir());
}
#[tokio::test]
async fn tells_the_browser_where_to_put_downloads() {
let temp = TempDir::new("bc-behavior");
let transport = RecordingTransport::default();
set_download_behavior(&transport, temp.path())
.await
.unwrap();
let sent = transport.sent();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].0, "Browser.setDownloadBehavior");
assert_eq!(sent[0].1["behavior"], "allow");
assert_eq!(
sent[0].1["downloadPath"],
temp.path().to_string_lossy().as_ref()
);
}
#[tokio::test]
async fn reports_a_browser_that_refuses_the_browser_domain() {
let temp = TempDir::new("bc-behavior-refused");
let transport = RecordingTransport::refusing("Browser domain is not available");
let error = set_download_behavior(&transport, temp.path())
.await
.unwrap_err();
assert!(
error
.to_string()
.contains("Browser domain is not available"),
"unexpected message: {error}"
);
}
}