refactor: move deleted tauri APIs, prepare for next release (#355)

This commit is contained in:
Lucas Fernandes Nogueira
2023-05-13 08:32:30 -07:00
committed by GitHub
parent 937e6a5be6
commit 702b7b36bd
45 changed files with 1412 additions and 31963 deletions
-4184
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -6,7 +6,7 @@ authors.workspace = true
license.workspace = true
[dependencies]
tauri = { workspace = true, features = ["updater", "fs-extract-api"] }
tauri.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
@@ -23,6 +23,10 @@ percent-encoding = "2"
semver = { version = "1", features = [ "serde" ] }
futures-util = "0.3"
tempfile = "3"
flate2 = "1"
tar = "0.4"
ignore = "0.4"
zip = { version = "0.6", default-features = false }
[dev-dependencies]
mockito = "0.31"
+42
View File
@@ -0,0 +1,42 @@
use serde::{Deserialize, Deserializer};
use url::Url;
/// Updater configuration.
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
#[serde(default)]
pub endpoints: Vec<UpdaterEndpoint>,
/// Additional arguments given to the NSIS or WiX installer.
#[serde(default, alias = "installer-args")]
pub installer_args: Vec<String>,
}
/// A URL to an updater server.
///
/// The URL must use the `https` scheme on production.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct UpdaterEndpoint(pub Url);
impl std::fmt::Display for UpdaterEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'de> Deserialize<'de> for UpdaterEndpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let url = Url::deserialize(deserializer)?;
#[cfg(all(not(debug_assertions), not(feature = "schema")))]
{
if url.scheme() != "https" {
return Err(serde::de::Error::custom(
"The configured updater endpoint must use the `https` protocol.",
));
}
}
Ok(Self(url))
}
}
+6
View File
@@ -78,6 +78,12 @@ pub enum Error {
#[cfg(target_os = "linux")]
#[error("temp directory is not on the same mount point as the AppImage")]
TempDirNotOnSameMountPoint,
/// The path StripPrefixError error.
#[error("Path Error: {0}")]
PathPrefix(#[from] std::path::StripPrefixError),
/// Ignore error.
#[error("failed to walkdir: {0}")]
Ignore(#[from] ignore::Error),
}
impl Serialize for Error {
+23 -4
View File
@@ -6,15 +6,18 @@ use tauri::{
use tokio::sync::Mutex;
mod commands;
mod config;
mod error;
mod updater;
pub use config::Config;
pub use error::Error;
pub use updater::*;
pub type Result<T> = std::result::Result<T, Error>;
struct UpdaterState {
target: Option<String>,
config: Config,
}
struct PendingUpdate<R: Runtime>(Mutex<Option<UpdateResponse<R>>>);
@@ -22,6 +25,7 @@ struct PendingUpdate<R: Runtime>(Mutex<Option<UpdateResponse<R>>>);
#[derive(Default)]
pub struct Builder {
target: Option<String>,
installer_args: Option<Vec<String>>,
}
/// Extension trait to use the updater on [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`].
@@ -60,11 +64,26 @@ impl Builder {
self
}
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
pub fn installer_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.installer_args
.replace(args.into_iter().map(Into::into).collect());
self
}
pub fn build<R: Runtime>(self) -> TauriPlugin<R, Config> {
let target = self.target;
PluginBuilder::<R>::new("updater")
.setup(move |app, _api| {
app.manage(UpdaterState { target });
let installer_args = self.installer_args;
PluginBuilder::<R, Config>::new("updater")
.setup(move |app, api| {
let mut config = api.config().clone();
if let Some(installer_args) = installer_args {
config.installer_args = installer_args;
}
app.manage(UpdaterState { target, config });
app.manage(PendingUpdate::<R>(Default::default()));
Ok(())
})
+15 -10
View File
@@ -2,6 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(desktop)]
use super::{
extract::{ArchiveFormat, Extract},
move_file::Move,
};
use crate::{Error, Result};
use base64::Engine;
use futures_util::StreamExt;
@@ -13,8 +18,6 @@ use minisign_verify::{PublicKey, Signature};
use reqwest::ClientBuilder;
use semver::Version;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
#[cfg(desktop)]
use tauri::api::file::{ArchiveFormat, Extract, Move};
use tauri::utils::{platform::current_exe, Env};
use tauri::{AppHandle, Manager, Runtime};
use time::OffsetDateTime;
@@ -36,7 +39,7 @@ use std::{
use std::ffi::OsStr;
#[cfg(all(desktop, not(target_os = "windows")))]
use tauri::api::file::Compression;
use super::extract::Compression;
#[cfg(target_os = "windows")]
use std::{
@@ -607,6 +610,7 @@ impl<R: Runtime> Update<R> {
&self.extract_path,
self.with_elevated_task,
&self.app.config(),
&self.app.state::<UpdaterState>().config,
)?;
#[cfg(not(target_os = "windows"))]
copy_files_and_run(archive_buffer, &self.extract_path)?;
@@ -668,7 +672,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = entry.extract(extract_path) {
Move::from_source(tmp_app_image).to_dest(extract_path)?;
return Err(tauri::api::Error::Extract(err.to_string()));
return Err(err);
}
// early finish we have everything we need here
return Ok(true);
@@ -706,6 +710,7 @@ fn copy_files_and_run<R: Read + Seek>(
_extract_path: &Path,
with_elevated_task: bool,
config: &tauri::Config,
updater_config: &crate::Config,
) -> Result<()> {
// FIXME: We need to create a memory buffer with the MSI and then run it.
// (instead of extracting the MSI to a temp path)
@@ -733,11 +738,11 @@ fn copy_files_and_run<R: Read + Seek>(
// Run the EXE
let mut installer = Command::new(found_path);
if tauri::utils::config::WindowsUpdateInstallMode::Quiet
== config.tauri.updater.windows.install_mode
== config.tauri.bundle.updater.install_mode
{
installer.arg("/S");
}
installer.args(&config.tauri.updater.windows.installer_args);
installer.args(&updater_config.installer_args);
installer.spawn().expect("installer failed to start");
@@ -793,17 +798,17 @@ fn copy_files_and_run<R: Read + Seek>(
msi_path_arg.push(&found_path);
msi_path_arg.push("\"\"\"");
let mut msiexec_args = config
let mut msiexec_args = updater_config
.tauri
.bundle
.updater
.windows
.install_mode
.clone()
.msiexec_args()
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>();
msiexec_args.extend(config.tauri.updater.windows.installer_args.clone());
msiexec_args.extend(updater_config.installer_args.clone());
// run the installer and relaunch the application
let system_root = std::env::var("SYSTEMROOT");
@@ -890,7 +895,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
}
}
Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
return Err(tauri::api::Error::Extract(err.to_string()));
return Err(err);
}
extracted_files.push(extraction_path);
+336
View File
@@ -0,0 +1,336 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
borrow::Cow,
fs,
io::{self, Cursor, Read, Seek},
path::{self, Path, PathBuf},
};
use crate::{Error, Result};
/// The archive reader.
#[derive(Debug)]
pub enum ArchiveReader<R: Read + Seek> {
/// A plain reader.
Plain(R),
/// A GZ- compressed reader (decoder).
GzCompressed(Box<flate2::read::GzDecoder<R>>),
}
impl<R: Read + Seek> Read for ArchiveReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Plain(r) => r.read(buf),
Self::GzCompressed(decoder) => decoder.read(buf),
}
}
}
impl<R: Read + Seek> ArchiveReader<R> {
#[allow(dead_code)]
fn get_mut(&mut self) -> &mut R {
match self {
Self::Plain(r) => r,
Self::GzCompressed(decoder) => decoder.get_mut(),
}
}
}
/// The supported archive formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArchiveFormat {
/// Tar archive.
Tar(Option<Compression>),
/// Zip archive.
#[allow(dead_code)]
Zip,
}
/// The supported compression types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Compression {
/// Gz compression (e.g. `.tar.gz` archives)
Gz,
}
/// The zip entry.
pub struct ZipEntry {
path: PathBuf,
is_dir: bool,
file_contents: Vec<u8>,
}
/// A read-only view into an entry of an archive.
#[non_exhaustive]
pub enum Entry<'a, R: Read> {
/// An entry of a tar archive.
#[non_exhaustive]
Tar(Box<tar::Entry<'a, R>>),
/// An entry of a zip archive.
#[non_exhaustive]
#[allow(dead_code)]
Zip(ZipEntry),
}
impl<'a, R: Read> Entry<'a, R> {
/// The entry path.
pub fn path(&self) -> Result<Cow<'_, Path>> {
match self {
Self::Tar(e) => e.path().map_err(Into::into),
Self::Zip(e) => Ok(Cow::Borrowed(&e.path)),
}
}
/// Extract this entry into `into_path`.
/// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location.
/// Note: You need to include the complete path, with file name and extension.
pub fn extract(self, into_path: &path::Path) -> Result<()> {
match self {
Self::Tar(mut entry) => {
// determine if it's a file or a directory
if entry.header().entry_type() == tar::EntryType::Directory {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut entry, &mut out_file)?;
// make sure we set permissions
if let Ok(mode) = entry.header().mode() {
set_perms(into_path, Some(&mut out_file), mode, true)?;
}
}
}
Self::Zip(entry) => {
if entry.is_dir {
// this is a directory, lets create it
match fs::create_dir_all(into_path) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(e.into());
}
}
}
} else {
let mut out_file = fs::File::create(into_path)?;
io::copy(&mut Cursor::new(entry.file_contents), &mut out_file)?;
}
}
}
Ok(())
}
}
/// The extract manager to retrieve files from archives.
pub struct Extract<'a, R: Read + Seek> {
reader: ArchiveReader<R>,
archive_format: ArchiveFormat,
tar_archive: Option<tar::Archive<&'a mut ArchiveReader<R>>>,
}
impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Extract")
.field("reader", &self.reader)
.field("archive_format", &self.archive_format)
.finish()
}
}
impl<'a, R: Read + Seek> Extract<'a, R> {
/// Create archive from reader.
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<'a, R> {
if reader.rewind().is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}
let compression = if let ArchiveFormat::Tar(compression) = archive_format {
compression
} else {
None
};
Extract {
reader: match compression {
Some(Compression::Gz) => {
ArchiveReader::GzCompressed(Box::new(flate2::read::GzDecoder::new(reader)))
}
_ => ArchiveReader::Plain(reader),
},
archive_format,
tar_archive: None,
}
}
/// Reads the archive content.
pub fn with_files<
E: Into<Error>,
F: FnMut(Entry<'_, &mut ArchiveReader<R>>) -> std::result::Result<bool, E>,
>(
&'a mut self,
mut f: F,
) -> Result<()> {
match self.archive_format {
ArchiveFormat::Tar(_) => {
let archive = tar::Archive::new(&mut self.reader);
self.tar_archive.replace(archive);
for entry in self.tar_archive.as_mut().unwrap().entries()? {
let entry = entry?;
if entry.path().is_ok() {
let stop = f(Entry::Tar(Box::new(entry))).map_err(Into::into)?;
if stop {
break;
}
}
}
}
ArchiveFormat::Zip => {
#[cfg(feature = "fs-extract-api")]
{
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
let file_names = archive
.file_names()
.map(|f| f.to_string())
.collect::<Vec<String>>();
for path in file_names {
let mut zip_file = archive.by_name(&path)?;
let is_dir = zip_file.is_dir();
let mut file_contents = Vec::new();
zip_file.read_to_end(&mut file_contents)?;
let stop = f(Entry::Zip(ZipEntry {
path: path.into(),
is_dir,
file_contents,
}))
.map_err(Into::into)?;
if stop {
break;
}
}
}
}
}
Ok(())
}
/// Extract an entire source archive into a specified path. If the source is a single compressed
/// file and not an archive, it will be extracted into a file with the same name inside of
/// `into_dir`.
#[allow(dead_code)]
pub fn extract_into(&mut self, into_dir: &path::Path) -> Result<()> {
match self.archive_format {
ArchiveFormat::Tar(_) => {
let mut archive = tar::Archive::new(&mut self.reader);
archive.unpack(into_dir)?;
}
ArchiveFormat::Zip => {
#[cfg(feature = "fs-extract-api")]
{
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
// Decode the file name from raw bytes instead of using file.name() directly.
// file.name() uses String::from_utf8_lossy() which may return messy characters
// such as: 爱交易.app/, that does not work as expected.
// Here we require the file name must be a valid UTF-8.
let file_name = String::from_utf8(file.name_raw().to_vec())?;
let out_path = into_dir.join(file_name);
if file.is_dir() {
fs::create_dir_all(&out_path)?;
} else {
if let Some(out_path_parent) = out_path.parent() {
fs::create_dir_all(out_path_parent)?;
}
let mut out_file = fs::File::create(&out_path)?;
io::copy(&mut file, &mut out_file)?;
}
// Get and Set permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&out_path, fs::Permissions::from_mode(mode))?;
}
}
}
}
}
}
Ok(())
}
}
fn set_perms(
dst: &Path,
f: Option<&mut std::fs::File>,
mode: u32,
preserve: bool,
) -> io::Result<()> {
_set_perms(dst, f, mode, preserve).map_err(|_| {
io::Error::new(
io::ErrorKind::Other,
format!(
"failed to set permissions to {mode:o} \
for `{}`",
dst.display()
),
)
})
}
#[cfg(unix)]
fn _set_perms(
dst: &Path,
f: Option<&mut std::fs::File>,
mode: u32,
preserve: bool,
) -> io::Result<()> {
use std::os::unix::prelude::*;
let mode = if preserve { mode } else { mode & 0o777 };
let perm = fs::Permissions::from_mode(mode as _);
match f {
Some(f) => f.set_permissions(perm),
None => fs::set_permissions(dst, perm),
}
}
#[cfg(windows)]
fn _set_perms(
dst: &Path,
f: Option<&mut std::fs::File>,
mode: u32,
_preserve: bool,
) -> io::Result<()> {
if mode & 0o200 == 0o200 {
return Ok(());
}
match f {
Some(f) => {
let mut perm = f.metadata()?.permissions();
perm.set_readonly(true);
f.set_permissions(perm)
}
None => {
let mut perm = fs::metadata(dst)?.permissions();
perm.set_readonly(true);
fs::set_permissions(dst, perm)
}
}
}
+7 -6
View File
@@ -12,6 +12,8 @@
//! ```
mod core;
mod extract;
mod move_file;
use std::time::Duration;
@@ -23,7 +25,7 @@ pub use self::core::{DownloadEvent, RemoteRelease};
use tauri::{AppHandle, Manager, Runtime};
use crate::Result;
use crate::{Result, UpdaterState};
/// Gets the target string used on the updater.
pub fn target() -> Option<String> {
@@ -276,7 +278,7 @@ impl<R: Runtime> UpdateResponse<R> {
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
self.update
.download_and_install(
self.update.app.config().tauri.updater.pubkey.clone(),
self.update.app.config().tauri.bundle.updater.pubkey.clone(),
on_event,
)
.await
@@ -285,14 +287,13 @@ impl<R: Runtime> UpdateResponse<R> {
/// Initializes the [`UpdateBuilder`] using the app configuration.
pub fn builder<R: Runtime>(handle: AppHandle<R>) -> UpdateBuilder<R> {
let updater_config = &handle.config().tauri.updater;
let package_info = handle.package_info().clone();
// prepare our endpoints
let endpoints = updater_config
let endpoints = handle
.state::<UpdaterState>()
.config
.endpoints
.as_ref()
.expect("Something wrong with endpoints")
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>();
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use ignore::WalkBuilder;
use std::{fs, path};
use crate::Result;
/// Moves a file from the given path to the specified destination.
///
/// `source` and `dest` must be on the same filesystem.
/// If `replace_using_temp` is specified, the destination file will be
/// replaced using the given temporary path.
///
/// * Errors:
/// * Io - copying / renaming
#[derive(Debug)]
pub struct Move<'a> {
source: &'a path::Path,
temp: Option<&'a path::Path>,
}
impl<'a> Move<'a> {
/// Specify source file
pub fn from_source(source: &'a path::Path) -> Move<'a> {
Self { source, temp: None }
}
/// If specified and the destination file already exists, the "destination"
/// file will be moved to the given temporary location before the "source"
/// file is moved to the "destination" file.
///
/// In the event of an `io` error while renaming "source" to "destination",
/// the temporary file will be moved back to "destination".
///
/// The `temp` dir must be explicitly provided since `rename` operations require
/// files to live on the same filesystem.
#[allow(dead_code)]
pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self {
self.temp = Some(temp);
self
}
/// Move source file to specified destination (replace whole directory)
pub fn to_dest(&self, dest: &path::Path) -> Result<()> {
match self.temp {
None => {
fs::rename(self.source, dest)?;
}
Some(temp) => {
if dest.exists() {
fs::rename(dest, temp)?;
if let Err(e) = fs::rename(self.source, dest) {
fs::rename(temp, dest)?;
return Err(e.into());
}
} else {
fs::rename(self.source, dest)?;
}
}
};
Ok(())
}
/// Walk in the source and copy all files and create directories if needed by
/// replacing existing elements. (equivalent to a cp -R)
#[allow(dead_code)]
pub fn walk_to_dest(&self, dest: &path::Path) -> Result<()> {
match self.temp {
None => {
// got no temp -- no need to backup
walkdir_and_copy(self.source, dest)?;
}
Some(temp) => {
if dest.exists() {
// we got temp and our dest exist, lets make a backup
// of current files
walkdir_and_copy(dest, temp)?;
if let Err(e) = walkdir_and_copy(self.source, dest) {
// if we got something wrong we reset the dest with our backup
fs::rename(temp, dest)?;
return Err(e);
}
} else {
// got temp but dest didnt exist
walkdir_and_copy(self.source, dest)?;
}
}
};
Ok(())
}
}
// Walk into the source and create directories, and copy files
// Overwriting existing items but keeping untouched the files in the dest
// not provided in the source.
fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> Result<()> {
let walkdir = WalkBuilder::new(source).hidden(false).build();
for entry in walkdir {
// Check if it's a file
let element = entry?;
let metadata = element.metadata()?;
let destination = dest.join(element.path().strip_prefix(source)?);
// we make sure it's a directory and destination doesnt exist
if metadata.is_dir() && !&destination.exists() {
fs::create_dir_all(&destination)?;
}
// we make sure it's a file
if metadata.is_file() {
fs::copy(element.path(), destination)?;
}
}
Ok(())
}
File diff suppressed because it is too large Load Diff
@@ -7,24 +7,29 @@
use tauri_plugin_updater::UpdaterExt;
fn main() {
#[allow(unused_mut)]
let mut context = tauri::generate_context!();
let mut updater = tauri_plugin_updater::Builder::new();
if std::env::var("TARGET").unwrap_or_default() == "nsis" {
// /D sets the default installation directory ($INSTDIR),
// overriding InstallDir and InstallDirRegKey.
// It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces.
// Only absolute paths are supported.
// NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder
context.config_mut().tauri.updater.windows.installer_args = vec![format!(
// TODO mutate plugin config
updater = updater.installer_args(vec![format!(
"/D={}",
tauri::utils::platform::current_exe()
.unwrap()
.parent()
.unwrap()
.display()
)];
)]);
}
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(updater.build())
.setup(|app| {
let handle = app.handle();
tauri::async_runtime::spawn(async move {
@@ -21,18 +21,19 @@
"wix": {
"skipWebviewInstall": true
}
},
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"windows": {
"installMode": "quiet"
}
}
},
"allowlist": {
"all": false
},
}
},
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
"endpoints": ["http://localhost:3007"],
"windows": {
"installMode": "quiet"
}
"endpoints": ["http://localhost:3007"]
}
}
}