mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-06 13:53:54 +02:00
refactor: move deleted tauri APIs, prepare for next release (#355)
This commit is contained in:
committed by
GitHub
parent
937e6a5be6
commit
702b7b36bd
Generated
-4184
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>>();
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
-3852
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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user