From a41c850fe5ccbe40cb33177740808575a93e1ae6 Mon Sep 17 00:00:00 2001 From: JessicaTegner Date: Mon, 2 Jun 2025 14:10:00 +0200 Subject: [PATCH] Pass update info back and forward --- src/lib-python/src/asset.rs | 207 +++++++++++++++++++++++++++++++ src/lib-python/src/exceptions.rs | 3 +- src/lib-python/src/lib.rs | 9 ++ src/lib-python/src/manager.rs | 107 +++++++++------- src/lib-python/test/app.py | 9 +- 5 files changed, 282 insertions(+), 53 deletions(-) create mode 100644 src/lib-python/src/asset.rs diff --git a/src/lib-python/src/asset.rs b/src/lib-python/src/asset.rs new file mode 100644 index 00000000..66c29184 --- /dev/null +++ b/src/lib-python/src/asset.rs @@ -0,0 +1,207 @@ +use pyo3::prelude::*; + +// Import your original structs from your rust library +// Adjust the import path according to your crate structure +use velopack::{VelopackAsset, UpdateInfo}; + +#[pyclass(name = "VelopackAsset")] +#[derive(Debug, Clone)] +pub struct PyVelopackAsset(pub VelopackAsset); + +#[pymethods] +impl PyVelopackAsset { + #[new] + #[pyo3(signature = (package_id=String::new(), version=String::new(), type_=String::new(), file_name=String::new(), sha1=String::new(), sha256=String::new(), size=0, notes_markdown=String::new(), notes_html=String::new()))] + pub fn new( + package_id: String, + version: String, + type_: String, + file_name: String, + sha1: String, + sha256: String, + size: u64, + notes_markdown: String, + notes_html: String, + ) -> Self { + PyVelopackAsset(VelopackAsset { + PackageId: package_id, + Version: version, + Type: type_, + FileName: file_name, + SHA1: sha1, + SHA256: sha256, + Size: size, + NotesMarkdown: notes_markdown, + NotesHtml: notes_html, + }) + } + + // Direct field access - much cleaner! + #[getter] + fn package_id(&self) -> &str { + &self.0.PackageId + } + + #[getter] + fn version(&self) -> &str { + &self.0.Version + } + + #[getter] + fn type_(&self) -> &str { + &self.0.Type + } + + #[getter] + fn file_name(&self) -> &str { + &self.0.FileName + } + + #[getter] + fn sha1(&self) -> &str { + &self.0.SHA1 + } + + #[getter] + fn sha256(&self) -> &str { + &self.0.SHA256 + } + + #[getter] + fn size(&self) -> u64 { + self.0.Size + } + + #[getter] + fn notes_markdown(&self) -> &str { + &self.0.NotesMarkdown + } + + #[getter] + fn notes_html(&self) -> &str { + &self.0.NotesHtml + } + + fn __repr__(&self) -> String { + format!( + "VelopackAsset(package_id='{}', version='{}', type='{}', file_name='{}', size={})", + self.0.PackageId, self.0.Version, self.0.Type, self.0.FileName, self.0.Size + ) + } +} + +// Conversion traits for seamless interop +impl From for PyVelopackAsset { + fn from(asset: VelopackAsset) -> Self { + PyVelopackAsset(asset) + } +} + +impl From for VelopackAsset { + fn from(py_asset: PyVelopackAsset) -> Self { + py_asset.0 + } +} + +impl AsRef for PyVelopackAsset { + fn as_ref(&self) -> &VelopackAsset { + &self.0 + } +} + +#[pyclass(name = "UpdateInfo")] +#[derive(Debug, Clone)] +pub struct PyUpdateInfo(pub UpdateInfo); + +#[pymethods] +impl PyUpdateInfo { + #[new] + #[pyo3(signature = (target_full_release, base_release=None, deltas_to_target=Vec::new(), is_downgrade=false))] + pub fn new( + target_full_release: PyVelopackAsset, + base_release: Option, + deltas_to_target: Vec, + is_downgrade: bool, + ) -> Self { + PyUpdateInfo(UpdateInfo { + TargetFullRelease: target_full_release.into(), + BaseRelease: base_release.map(Into::into), + DeltasToTarget: deltas_to_target.into_iter().map(Into::into).collect(), + IsDowngrade: is_downgrade, + }) + } + + #[staticmethod] + pub fn new_full(target: PyVelopackAsset, is_downgrade: bool) -> PyUpdateInfo { + PyUpdateInfo(UpdateInfo { + TargetFullRelease: target.into(), + BaseRelease: None, + DeltasToTarget: Vec::new(), + IsDowngrade: is_downgrade, + }) + } + + #[staticmethod] + pub fn new_delta( + target: PyVelopackAsset, + base: PyVelopackAsset, + deltas: Vec, + ) -> PyUpdateInfo { + let rust_deltas = deltas.into_iter().map(Into::into).collect(); + PyUpdateInfo(UpdateInfo { + TargetFullRelease: target.into(), + BaseRelease: Some(base.into()), + DeltasToTarget: rust_deltas, + IsDowngrade: false, + }) + } + + #[getter] + fn target_full_release(&self) -> PyVelopackAsset { + PyVelopackAsset(self.0.TargetFullRelease.clone()) + } + + #[getter] + fn base_release(&self) -> Option { + self.0.BaseRelease.clone().map(PyVelopackAsset) + } + + #[getter] + fn deltas_to_target(&self) -> Vec { + self.0.DeltasToTarget.iter().cloned().map(PyVelopackAsset).collect() + } + + #[getter] + fn is_downgrade(&self) -> bool { + self.0.IsDowngrade + } + + fn __repr__(&self) -> String { + format!( + "UpdateInfo(target_version='{}', has_base_release={}, deltas_count={}, is_downgrade={})", + self.0.TargetFullRelease.Version, + self.0.BaseRelease.is_some(), + self.0.DeltasToTarget.len(), + self.0.IsDowngrade + ) + } +} + +impl From for PyUpdateInfo { + fn from(info: UpdateInfo) -> Self { + PyUpdateInfo(info) + } +} + +impl From for UpdateInfo { + fn from(py_info: PyUpdateInfo) -> Self { + py_info.0 + } +} + +impl AsRef for PyUpdateInfo { + fn as_ref(&self) -> &UpdateInfo { + &self.0 + } +} + diff --git a/src/lib-python/src/exceptions.rs b/src/lib-python/src/exceptions.rs index e3357c56..6457fcbe 100644 --- a/src/lib-python/src/exceptions.rs +++ b/src/lib-python/src/exceptions.rs @@ -17,4 +17,5 @@ impl VelopackError { fn __str__(&self) -> String { self.message.clone() } -} \ No newline at end of file +} + diff --git a/src/lib-python/src/lib.rs b/src/lib-python/src/lib.rs index 534dbeef..8fcf5b56 100644 --- a/src/lib-python/src/lib.rs +++ b/src/lib-python/src/lib.rs @@ -1,6 +1,11 @@ +use asset::PyUpdateInfo; use pyo3::prelude::*; use pyo3::types::PyModule; +mod asset; +pub use asset::PyVelopackAsset; + + mod exceptions; pub use exceptions::VelopackError; @@ -14,6 +19,10 @@ pub use manager::UpdateManagerWrapper; #[pymodule] fn velopack(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; diff --git a/src/lib-python/src/manager.rs b/src/lib-python/src/manager.rs index ccedd882..589e98d8 100644 --- a/src/lib-python/src/manager.rs +++ b/src/lib-python/src/manager.rs @@ -7,11 +7,11 @@ use velopack::{UpdateCheck, UpdateInfo, UpdateManager as VelopackUpdateManagerRu use velopack::sources::AutoSource; use crate::exceptions::VelopackError; +use crate::asset::PyUpdateInfo; #[pyclass(name = "UpdateManager")] pub struct UpdateManagerWrapper { inner: VelopackUpdateManagerRust, - updates: UpdateInfo, } #[pymethods] @@ -25,69 +25,80 @@ pub fn new(source: String) -> PyResult { .map_err(|e| PyErr::new::(format!("Failed to create UpdateManager: {}", e)))?; Ok(UpdateManagerWrapper { inner, - updates: UpdateInfo::default(), } ) } -// check_for_updates return a bool indicating if updates are available - /// This method checks for updates and returns true if updates are available, false otherwise. - pub fn check_for_updates(&mut self) -> PyResult { - match self.inner.check_for_updates() { - Ok(UpdateCheck::UpdateAvailable(updates)) => { - self.updates = updates; - Ok(true) - } - Ok(_) => { - self.updates = UpdateInfo::default(); - Ok(false) - } - Err(e) => Err(PyErr::new::(format!("Failed to check for updates: {}", e))) +// check_for_updates return update info indicating if updates are available + /// This method checks for updates and returns update info if updates are available, None otherwise. +pub fn check_for_updates(&mut self) -> PyResult> { + let update_check = self.inner.check_for_updates() + .map_err(|e| PyErr::new::(format!("Failed to check for updates: {}", e)))?; + + match update_check { + UpdateCheck::UpdateAvailable(updates) => { + let py_updates = PyUpdateInfo::from(updates); + Ok(Some(py_updates)) + }, + UpdateCheck::NoUpdateAvailable => { + Ok(None) + }, + UpdateCheck::RemoteIsEmpty => { + Ok(None) } } +} - #[pyo3(signature = (progress_callback = None))] - pub fn download_updates(&mut self, progress_callback: Option) -> PyResult<()> { - if let Some(callback) = progress_callback { - // Create a channel for progress updates - let (sender, receiver) = mpsc::channel::(); - - // Spawn a thread to handle progress updates - let progress_thread = thread::spawn(move || { - Python::with_gil(|py| { - while let Ok(progress) = receiver.recv() { - if let Err(e) = callback.call1(py, (progress,)) { - // Log error but continue - don't break the download - eprintln!("Progress callback error: {}", e); - break; - } + +#[pyo3(signature = (update_info, progress_callback = None))] +pub fn download_updates(&mut self, update_info: &PyUpdateInfo, progress_callback: Option) -> PyResult<()> { + // Convert PyUpdateInfo back to rust UpdateInfo + let rust_update_info: UpdateInfo = update_info.clone().into(); + + if let Some(callback) = progress_callback { + // Create a channel for progress updates + let (sender, receiver) = mpsc::channel::(); + + // Spawn a thread to handle progress updates + let progress_thread = thread::spawn(move || { + Python::with_gil(|py| { + while let Ok(progress) = receiver.recv() { + if let Err(e) = callback.call1(py, (progress,)) { + // Log error but continue - don't break the download + eprintln!("Progress callback error: {}", e); + break; } - }); + } }); - - // Call download with the sender - let result = self.inner.download_updates(&self.updates, Some(sender)) - .map_err(|e| PyErr::new::(format!("Failed to download updates: {}", e))); - - // Wait for the progress thread to finish - let _ = progress_thread.join(); - - result.map(|_| ()) - } else { - // No progress callback provided - self.inner.download_updates(&self.updates, None) - .map_err(|e| PyErr::new::(format!("Failed to download updates: {}", e))) - .map(|_| ()) - } + }); + + // Call download with the sender + let result = self.inner.download_updates(&rust_update_info, Some(sender)) + .map_err(|e| PyErr::new::(format!("Failed to download updates: {}", e))); + + // Wait for the progress thread to finish + let _ = progress_thread.join(); + + result.map(|_| ()) + } else { + // No progress callback provided + self.inner.download_updates(&rust_update_info, None) + .map_err(|e| PyErr::new::(format!("Failed to download updates: {}", e))) + .map(|_| ()) } +} -pub fn apply_updates_and_restart(&mut self) -> PyResult<()> { - self.inner.apply_updates_and_restart(&self.updates) +pub fn apply_updates_and_restart(&mut self, update_info: &PyUpdateInfo) -> PyResult<()> { + // Convert PyUpdateInfo back to rust UpdateInfo + let rust_update_info: UpdateInfo = update_info.clone().into(); + + self.inner.apply_updates_and_restart(&rust_update_info) .map_err(|e| PyErr::new::(format!("Failed to apply updates and restart: {}", e))) .map(|_| ()) } + } diff --git a/src/lib-python/test/app.py b/src/lib-python/test/app.py index ca639143..85a9d34f 100644 --- a/src/lib-python/test/app.py +++ b/src/lib-python/test/app.py @@ -1,4 +1,3 @@ - import velopack import app_version @@ -8,8 +7,10 @@ version = app_version.version if __name__ == "__main__": velopack.App().run() um = velopack.UpdateManager("http://localhost:8080") - if um.check_for_updates(): - um.download_updates() - um.apply_updates_and_restart() + update_info = um.check_for_updates() + print(f"Update info: {update_info}") + if update_info: + um.download_updates(update_info) + um.apply_updates_and_restart(update_info) with open("version_result.txt", "w") as f: f.write(f"{version}") \ No newline at end of file