velopack/
sources.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::mpsc::Sender,
4};
5
6use crate::*;
7use crate::bundle::Manifest;
8
9/// Abstraction for finding and downloading updates from a package source / repository.
10/// An implementation may copy a file from a local repository, download from a web address,
11/// or even use third party services and parse proprietary data to produce a package feed.
12pub trait UpdateSource: Send + Sync {
13    /// Retrieve the list of available remote releases from the package source. These releases
14    /// can subsequently be downloaded with download_release_entry.
15    fn get_release_feed(&self, channel: &str, app: &bundle::Manifest, staged_user_id: &str) -> Result<VelopackAssetFeed, Error>;
16    /// Download the specified VelopackAsset to the provided local file path.
17    fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error>;
18    /// Clone the source to create a new lifetime.
19    fn clone_boxed(&self) -> Box<dyn UpdateSource>;
20}
21
22impl Clone for Box<dyn UpdateSource> {
23    fn clone(&self) -> Self {
24        self.clone_boxed()
25    }
26}
27
28/// A source that does not provide any update capability. 
29#[derive(Clone)]
30pub struct NoneSource {}
31
32impl UpdateSource for NoneSource {
33    fn get_release_feed(&self, _channel: &str, _app: &Manifest, _staged_user_id: &str) -> Result<VelopackAssetFeed, Error> {
34        Err(Error::Generic("None source does not checking release feed".to_owned()))
35    }
36    fn download_release_entry(&self, _asset: &VelopackAsset, _local_file: &str, _progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
37        Err(Error::Generic("None source does not support downloads".to_owned()))
38    }
39    fn clone_boxed(&self) -> Box<dyn UpdateSource> {
40        Box::new(self.clone())
41    }
42}
43
44#[derive(Clone)]
45/// Automatically delegates to the appropriate source based on the provided input string. If the input is a local path,
46/// it will use a FileSource. If the input is a URL, it will use an HttpSource.
47pub struct AutoSource {
48    source: Box<dyn UpdateSource>,
49}
50
51impl AutoSource {
52    /// Create a new AutoSource with the specified input string.
53    pub fn new(input: &str) -> AutoSource {
54        let source: Box<dyn UpdateSource> = if Self::is_http_url(input) {
55            Box::new(HttpSource::new(input))
56        } else {
57            Box::new(FileSource::new(input))
58        };
59        AutoSource { source }
60    }
61
62    fn is_http_url(url: &str) -> bool {
63        match url::Url::parse(url) {
64            Ok(url) => url.scheme().eq_ignore_ascii_case("http") || url.scheme().eq_ignore_ascii_case("https"),
65            _ => false,
66        }
67    }
68}
69
70impl UpdateSource for AutoSource {
71    fn get_release_feed(&self, channel: &str, app: &bundle::Manifest, staged_user_id: &str) -> Result<VelopackAssetFeed, Error> {
72        self.source.get_release_feed(channel, app, staged_user_id)
73    }
74
75    fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
76        self.source.download_release_entry(asset, local_file, progress_sender)
77    }
78
79    fn clone_boxed(&self) -> Box<dyn UpdateSource> {
80        self.source.clone_boxed()
81    }
82}
83
84#[derive(Clone)]
85/// Retrieves updates from a static file host or other web server.
86/// Will perform a request for '{baseUri}/RELEASES' to locate the available packages,
87/// and provides query parameters to specify the name of the requested package.
88pub struct HttpSource {
89    url: String,
90}
91
92impl HttpSource {
93    /// Create a new HttpSource with the specified base URL.
94    pub fn new<S: AsRef<str>>(url: S) -> HttpSource {
95        HttpSource { url: url.as_ref().to_owned() }
96    }
97}
98
99impl UpdateSource for HttpSource {
100    fn get_release_feed(&self, channel: &str, app: &bundle::Manifest, staged_user_id: &str) -> Result<VelopackAssetFeed, Error> {
101        let releases_name = format!("releases.{}.json", channel);
102
103        let path = self.url.trim_end_matches('/').to_owned() + "/";
104        let url = url::Url::parse(&path)?;
105        let mut releases_url = url.join(&releases_name)?;
106        releases_url.set_query(Some(format!("localVersion={}&id={}&stagingId={}", app.version, app.id, staged_user_id).as_str()));
107
108        info!("Downloading releases for channel {} from: {}", channel, releases_url.to_string());
109        let json = download::download_url_as_string(releases_url.as_str())?;
110        let feed: VelopackAssetFeed = serde_json::from_str(&json)?;
111        Ok(feed)
112    }
113
114    fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
115        let path = self.url.trim_end_matches('/').to_owned() + "/";
116        let url = url::Url::parse(&path)?;
117        let asset_url = url.join(&asset.FileName)?;
118
119        info!("About to download from URL '{}' to file '{}'", asset_url, local_file);
120        download::download_url_to_file(asset_url.as_str(), local_file, move |p| {
121            if let Some(progress_sender) = &progress_sender {
122                let _ = progress_sender.send(p);
123            }
124        })?;
125        Ok(())
126    }
127
128    fn clone_boxed(&self) -> Box<dyn UpdateSource> {
129        Box::new(self.clone())
130    }
131}
132
133#[derive(Clone)]
134/// Retrieves available updates from a local or network-attached disk. The directory
135/// must contain one or more valid packages, as well as a 'releases.{channel}.json' index file.
136pub struct FileSource {
137    path: PathBuf,
138}
139
140impl FileSource {
141    /// Create a new FileSource with the specified base directory.
142    pub fn new<P: AsRef<Path>>(path: P) -> FileSource {
143        let path = path.as_ref();
144        FileSource { path: PathBuf::from(path) }
145    }
146}
147
148impl UpdateSource for FileSource {
149    fn get_release_feed(&self, channel: &str, _: &bundle::Manifest, _staged_user_id: &str) -> Result<VelopackAssetFeed, Error> {
150        let releases_name = format!("releases.{}.json", channel);
151        let releases_path = self.path.join(&releases_name);
152
153        info!("Reading releases from file: {}", releases_path.display());
154        let json = std::fs::read_to_string(releases_path)?;
155        let feed: VelopackAssetFeed = serde_json::from_str(&json)?;
156        Ok(feed)
157    }
158
159    fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
160        let asset_path = self.path.join(&asset.FileName);
161        info!("About to copy from file '{}' to file '{}'", asset_path.display(), local_file);
162        if let Some(progress_sender) = &progress_sender {
163            let _ = progress_sender.send(50);
164        }
165        std::fs::copy(asset_path, local_file)?;
166        if let Some(progress_sender) = &progress_sender {
167            let _ = progress_sender.send(100);
168        }
169        Ok(())
170    }
171
172    fn clone_boxed(&self) -> Box<dyn UpdateSource> {
173        Box::new(self.clone())
174    }
175}