Calculate download checksums
This commit is contained in:
Generated
+1
@@ -2202,6 +2202,7 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
"gpui-component",
|
"gpui-component",
|
||||||
"gpui-component-assets",
|
"gpui-component-assets",
|
||||||
|
"sha2",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ gpui = "0.2.2"
|
|||||||
gpui-component = "0.5.1"
|
gpui-component = "0.5.1"
|
||||||
gpui-component-assets = "0.5.1"
|
gpui-component-assets = "0.5.1"
|
||||||
servo = "0.1.0"
|
servo = "0.1.0"
|
||||||
|
sha2 = "0.10.9"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
uuid = { version = "1.12.1", features = ["v7"] }
|
uuid = { version = "1.12.1", features = ["v7"] }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ ely_domain = { path = "../ely_domain" }
|
|||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui-component.workspace = true
|
gpui-component.workspace = true
|
||||||
gpui-component-assets.workspace = true
|
gpui-component-assets.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::{self, Read},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ely_domain::{DomainError, DownloadChecksum};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const CHECKSUM_BUFFER_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
|
pub struct DownloadChecksumCalculator;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DownloadChecksumError {
|
||||||
|
#[error("download file is unavailable: {path}")]
|
||||||
|
FileUnavailable {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("download path is not a file: {path}")]
|
||||||
|
NotAFile { path: PathBuf },
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Domain(#[from] DomainError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DownloadChecksumCalculator {
|
||||||
|
pub fn sha256(path: &Path) -> Result<DownloadChecksum, DownloadChecksumError> {
|
||||||
|
require_file(path)?;
|
||||||
|
let mut file = File::open(path).map_err(|source| {
|
||||||
|
DownloadChecksumError::FileUnavailable { path: path.to_path_buf(), source }
|
||||||
|
})?;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
let mut buffer = [0; CHECKSUM_BUFFER_BYTES];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = file.read(&mut buffer).map_err(|source| {
|
||||||
|
DownloadChecksumError::FileUnavailable { path: path.to_path_buf(), source }
|
||||||
|
})?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasher.update(&buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadChecksum::sha256_hex(encode_lower_hex(&hasher.finalize())).map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_file(path: &Path) -> Result<(), DownloadChecksumError> {
|
||||||
|
match fs::metadata(path) {
|
||||||
|
Ok(metadata) if metadata.is_file() => Ok(()),
|
||||||
|
Ok(_) => Err(DownloadChecksumError::NotAFile { path: path.to_path_buf() }),
|
||||||
|
Err(source) => {
|
||||||
|
Err(DownloadChecksumError::FileUnavailable { path: path.to_path_buf(), source })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_lower_hex(bytes: &[u8]) -> String {
|
||||||
|
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
|
||||||
|
let mut output = String::with_capacity(bytes.len() * 2);
|
||||||
|
for byte in bytes {
|
||||||
|
let byte = *byte;
|
||||||
|
output.push(HEX[(byte >> 4) as usize] as char);
|
||||||
|
output.push(HEX[(byte & 0x0f) as usize] as char);
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fs,
|
||||||
|
path::PathBuf,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{DownloadChecksumCalculator, DownloadChecksumError};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn computes_sha256_for_download_file() -> Result<(), Box<dyn Error>> {
|
||||||
|
let path = temp_file_path("checksum")?;
|
||||||
|
fs::write(&path, [])?;
|
||||||
|
|
||||||
|
let checksum = DownloadChecksumCalculator::sha256(&path)?;
|
||||||
|
let value = checksum.value().to_string();
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
|
||||||
|
assert_eq!(value, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_missing_download_file() -> Result<(), Box<dyn Error>> {
|
||||||
|
let path = temp_file_path("missing")?;
|
||||||
|
|
||||||
|
let error = match DownloadChecksumCalculator::sha256(&path) {
|
||||||
|
Ok(_) => return Err("missing download file should be rejected".into()),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(error, DownloadChecksumError::FileUnavailable { .. }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_file_path(name: &str) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
|
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
||||||
|
Ok(std::env::temp_dir().join(format!("ely-download-{name}-{nanos}.bin")))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod download_checksums;
|
||||||
pub mod download_files;
|
pub mod download_files;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ impl ElyShell {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.w(px(64.0))
|
.w(px(96.0))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
@@ -84,6 +84,7 @@ impl ElyShell {
|
|||||||
|this| {
|
|this| {
|
||||||
let open_id = entry.id().clone();
|
let open_id = entry.id().clone();
|
||||||
let reveal_id = entry.id().clone();
|
let reveal_id = entry.id().clone();
|
||||||
|
let checksum_id = entry.id().clone();
|
||||||
|
|
||||||
this.child(
|
this.child(
|
||||||
download_action_button("open", index, IconName::ExternalLink, "Open File")
|
download_action_button("open", index, IconName::ExternalLink, "Open File")
|
||||||
@@ -104,6 +105,20 @@ impl ElyShell {
|
|||||||
}))
|
}))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
|
.when(entry.checksum().is_none(), |this| {
|
||||||
|
this.child(
|
||||||
|
download_action_button(
|
||||||
|
"checksum",
|
||||||
|
index,
|
||||||
|
IconName::CircleCheck,
|
||||||
|
"Calculate SHA-256",
|
||||||
|
)
|
||||||
|
.on_click(cx.listener(move |shell, _, _, cx| {
|
||||||
|
shell.calculate_download_checksum(&checksum_id, cx);
|
||||||
|
}))
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use ely_domain::{DownloadEntry, DownloadSecurity};
|
use ely_domain::{DownloadChecksum, DownloadEntry, DownloadSecurity};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
|
||||||
@@ -106,8 +106,8 @@ impl ElyShell {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when_some(self.download_file_error.clone(), |this, message| {
|
.when_some(self.download_action_error.clone(), |this, message| {
|
||||||
this.child(render_download_file_error(message))
|
this.child(render_download_action_error(message))
|
||||||
})
|
})
|
||||||
.when(
|
.when(
|
||||||
self.download_clear_confirmation && !snapshot.download_entries.is_empty(),
|
self.download_clear_confirmation && !snapshot.download_entries.is_empty(),
|
||||||
@@ -259,6 +259,9 @@ impl ElyShell {
|
|||||||
)
|
)
|
||||||
.when(entry.security().requires_prompt(), |this| {
|
.when(entry.security().requires_prompt(), |this| {
|
||||||
this.child(render_security_prompt(entry.security()))
|
this.child(render_security_prompt(entry.security()))
|
||||||
|
})
|
||||||
|
.when_some(entry.checksum(), |this, checksum| {
|
||||||
|
this.child(render_checksum_label(checksum))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -296,7 +299,7 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_download_file_error(message: String) -> AnyElement {
|
fn render_download_action_error(message: String) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.border_1()
|
.border_1()
|
||||||
@@ -323,3 +326,18 @@ fn render_security_prompt(security: &DownloadSecurity) -> AnyElement {
|
|||||||
.child(download_security_label(security))
|
.child(download_security_label(security))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_checksum_label(checksum: &DownloadChecksum) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.text_color(rgb(colors::SUCCESS))
|
||||||
|
.child(IconName::CircleCheck)
|
||||||
|
.child(format!("SHA-256 {}", short_checksum(checksum.value())))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_checksum(value: &str) -> &str {
|
||||||
|
value.get(..12).unwrap_or(value)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ use gpui_component::input::{InputEvent, InputState, SelectAll};
|
|||||||
use crate::{
|
use crate::{
|
||||||
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
||||||
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab,
|
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab,
|
||||||
TogglePinnedTab, services::download_files::DownloadFileAction,
|
TogglePinnedTab,
|
||||||
|
services::{
|
||||||
|
download_checksums::DownloadChecksumCalculator, download_files::DownloadFileAction,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ShellState {
|
enum ShellState {
|
||||||
@@ -22,7 +25,7 @@ pub struct ElyShell {
|
|||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
command_input: Entity<InputState>,
|
command_input: Entity<InputState>,
|
||||||
last_intent: Option<CommandIntent>,
|
last_intent: Option<CommandIntent>,
|
||||||
download_file_error: Option<String>,
|
download_action_error: Option<String>,
|
||||||
download_clear_confirmation: bool,
|
download_clear_confirmation: bool,
|
||||||
_command_subscription: Subscription,
|
_command_subscription: Subscription,
|
||||||
}
|
}
|
||||||
@@ -79,7 +82,7 @@ impl ElyShell {
|
|||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
command_input,
|
command_input,
|
||||||
last_intent: None,
|
last_intent: None,
|
||||||
download_file_error: None,
|
download_action_error: None,
|
||||||
download_clear_confirmation: false,
|
download_clear_confirmation: false,
|
||||||
_command_subscription: command_subscription,
|
_command_subscription: command_subscription,
|
||||||
}
|
}
|
||||||
@@ -266,7 +269,26 @@ impl ElyShell {
|
|||||||
core.clear_downloads_for_active_profile();
|
core.clear_downloads_for_active_profile();
|
||||||
}
|
}
|
||||||
self.download_clear_confirmation = false;
|
self.download_clear_confirmation = false;
|
||||||
self.download_file_error = None;
|
self.download_action_error = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_download_checksum(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
||||||
|
let result = match &mut self.state {
|
||||||
|
ShellState::Ready(core) => core
|
||||||
|
.download_target_file_path(download_id)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
.and_then(|path| {
|
||||||
|
DownloadChecksumCalculator::sha256(&path).map_err(|error| error.to_string())
|
||||||
|
})
|
||||||
|
.and_then(|checksum| {
|
||||||
|
core.record_download_checksum(download_id, checksum)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}),
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.download_action_error = result.err();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +314,7 @@ impl ElyShell {
|
|||||||
ShellState::StartupError(message) => Err(message.clone()),
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.download_file_error = result.err();
|
self.download_action_error = result.err();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user