Pass update info back and forward

This commit is contained in:
JessicaTegner
2025-06-02 14:10:00 +02:00
committed by Caelan
parent a10b538177
commit a41c850fe5
5 changed files with 282 additions and 53 deletions

207
src/lib-python/src/asset.rs Normal file
View File

@@ -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<VelopackAsset> for PyVelopackAsset {
fn from(asset: VelopackAsset) -> Self {
PyVelopackAsset(asset)
}
}
impl From<PyVelopackAsset> for VelopackAsset {
fn from(py_asset: PyVelopackAsset) -> Self {
py_asset.0
}
}
impl AsRef<VelopackAsset> 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<PyVelopackAsset>,
deltas_to_target: Vec<PyVelopackAsset>,
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<PyVelopackAsset>,
) -> 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<PyVelopackAsset> {
self.0.BaseRelease.clone().map(PyVelopackAsset)
}
#[getter]
fn deltas_to_target(&self) -> Vec<PyVelopackAsset> {
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<UpdateInfo> for PyUpdateInfo {
fn from(info: UpdateInfo) -> Self {
PyUpdateInfo(info)
}
}
impl From<PyUpdateInfo> for UpdateInfo {
fn from(py_info: PyUpdateInfo) -> Self {
py_info.0
}
}
impl AsRef<UpdateInfo> for PyUpdateInfo {
fn as_ref(&self) -> &UpdateInfo {
&self.0
}
}

View File

@@ -17,4 +17,5 @@ impl VelopackError {
fn __str__(&self) -> String {
self.message.clone()
}
}
}

View File

@@ -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::<VelopackError>()?;
m.add_class::<PyVelopackAsset>()?;
m.add_class::<PyUpdateInfo>()?;
m.add_class::<VelopackAppWrapper>()?;

View File

@@ -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<Self> {
.map_err(|e| PyErr::new::<VelopackError, _>(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<bool> {
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::<VelopackError, _>(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<Option<PyUpdateInfo>> {
let update_check = self.inner.check_for_updates()
.map_err(|e| PyErr::new::<VelopackError, _>(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<PyObject>) -> PyResult<()> {
if let Some(callback) = progress_callback {
// Create a channel for progress updates
let (sender, receiver) = mpsc::channel::<i16>();
// 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<PyObject>) -> 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::<i16>();
// 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::<VelopackError, _>(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::<VelopackError, _>(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::<VelopackError, _>(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::<VelopackError, _>(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::<VelopackError, _>(format!("Failed to apply updates and restart: {}", e)))
.map(|_| ())
}
}

View File

@@ -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}")