mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
wip
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -2,6 +2,16 @@
|
|||||||
target/
|
target/
|
||||||
_docyml/
|
_docyml/
|
||||||
|
|
||||||
|
target
|
||||||
|
index.node
|
||||||
|
**/node_modules
|
||||||
|
**/.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
lib
|
||||||
|
cargo.log
|
||||||
|
cross.log
|
||||||
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
## Eclipse
|
## Eclipse
|
||||||
#################
|
#################
|
||||||
|
|||||||
57
Cargo.lock
generated
57
Cargo.lock
generated
@@ -1207,6 +1207,32 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "neon"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d75440242411c87dc39847b0e33e961ec1f10326a9d8ecf9c1ea64a3b3c13dc"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libloading",
|
||||||
|
"neon-macros",
|
||||||
|
"once_cell",
|
||||||
|
"semver",
|
||||||
|
"send_wrapper",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "neon-macros"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
"syn-mid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -1718,6 +1744,12 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "send_wrapper"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.204"
|
version = "1.0.204"
|
||||||
@@ -1791,6 +1823,12 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@@ -1857,6 +1895,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn-mid"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.12.0"
|
version = "3.12.0"
|
||||||
@@ -2139,6 +2188,14 @@ dependencies = [
|
|||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "velopack_nodeffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"neon",
|
||||||
|
"velopack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "versions"
|
name = "versions"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"src/bins",
|
"src/bins",
|
||||||
"src/lib-rust",
|
"src/lib-rust",
|
||||||
|
"src/lib-node/crates/velopack_nodeffi",
|
||||||
# "src/lib-cpp/generator"
|
# "src/lib-cpp/generator"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
8
src/lib-node/.gitignore
vendored
8
src/lib-node/.gitignore
vendored
@@ -1,8 +0,0 @@
|
|||||||
target
|
|
||||||
index.node
|
|
||||||
**/node_modules
|
|
||||||
**/.DS_Store
|
|
||||||
npm-debug.log*
|
|
||||||
lib
|
|
||||||
cargo.log
|
|
||||||
cross.log
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
members = ["crates/veloz"]
|
|
||||||
resolver = "2"
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "veloz"
|
name = "velopack_nodeffi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
license = "ISC"
|
license = "MIT"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
exclude = ["index.node"]
|
exclude = ["index.node"]
|
||||||
|
|
||||||
@@ -12,3 +12,4 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
neon = "1"
|
neon = "1"
|
||||||
|
velopack = { path = "../../../lib-rust" }
|
||||||
49
src/lib-node/crates/velopack_nodeffi/src/lib.rs
Normal file
49
src/lib-node/crates/velopack_nodeffi/src/lib.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use neon::prelude::*;
|
||||||
|
use velopack::*;
|
||||||
|
use velopack::sources::*;
|
||||||
|
|
||||||
|
|
||||||
|
struct UpdateManagerWrapper<'a> {
|
||||||
|
manager: UpdateManager<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Finalize for UpdateManagerWrapper<'a> {}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_js_options(mut cx: FunctionContext, obj: &Handle<JsObject>) -> JsResult<UpdateOptions> {
|
||||||
|
let allow_downgrade = obj.get(&mut cx, "allowDowngrade")?;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_new_from_http_source(mut cx: FunctionContext) -> JsResult<JsBox<UpdateManagerWrapper>> {
|
||||||
|
let url = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||||
|
|
||||||
|
let options: Option<UpdateOptions> = None;
|
||||||
|
|
||||||
|
|
||||||
|
let obj = cx.argument::<JsObject>(1)?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let source = HttpSource::new(&url);
|
||||||
|
let um = UpdateManager::new(source, options).map_err(|e| cx.throw_error(e.to_string()))?;
|
||||||
|
|
||||||
|
let wrapper = UpdateManagerWrapper { manager: um };
|
||||||
|
|
||||||
|
|
||||||
|
Ok(cx.boxed(wrapper))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||||
|
Ok(cx.string("hello node"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[neon::main]
|
||||||
|
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||||
|
cx.export_function("hello", hello)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
use neon::prelude::*;
|
|
||||||
|
|
||||||
fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
|
|
||||||
Ok(cx.string("hello node"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[neon::main]
|
|
||||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
|
||||||
cx.export_function("hello", hello)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{fs, process::Command as Process, process::exit};
|
use std::{fs, process::{exit, Command as Process}, rc::Rc, sync::mpsc::Sender};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
@@ -91,26 +91,24 @@ pub struct UpdateOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
|
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
|
||||||
pub struct UpdateManager<T>
|
pub struct UpdateManager<'a>
|
||||||
where
|
|
||||||
T: UpdateSource,
|
|
||||||
{
|
{
|
||||||
allow_version_downgrade: bool,
|
allow_version_downgrade: bool,
|
||||||
explicit_channel: Option<String>,
|
explicit_channel: Option<String>,
|
||||||
source: T,
|
source: Rc<Box<dyn UpdateSource + 'a>>,
|
||||||
paths: VelopackLocator,
|
paths: VelopackLocator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: UpdateSource> Clone for UpdateManager<T> {
|
// impl Clone for UpdateManager {
|
||||||
fn clone(&self) -> Self {
|
// fn clone(&self) -> Self {
|
||||||
UpdateManager {
|
// UpdateManager {
|
||||||
allow_version_downgrade: self.allow_version_downgrade,
|
// allow_version_downgrade: self.allow_version_downgrade,
|
||||||
explicit_channel: self.explicit_channel.clone(),
|
// explicit_channel: self.explicit_channel.clone(),
|
||||||
source: self.source.clone(),
|
// source: self.source.clone(),
|
||||||
paths: self.paths.clone(),
|
// paths: self.paths.clone(),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// Arguments to pass to the Update.exe process when restarting the application after applying updates.
|
/// Arguments to pass to the Update.exe process when restarting the application after applying updates.
|
||||||
pub enum RestartArgs<'a> {
|
pub enum RestartArgs<'a> {
|
||||||
@@ -145,7 +143,7 @@ pub enum UpdateCheck {
|
|||||||
UpdateAvailable(UpdateInfo),
|
UpdateAvailable(UpdateInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: UpdateSource> UpdateManager<T> {
|
impl<'a> UpdateManager<'a> {
|
||||||
/// Create a new UpdateManager instance using the specified UpdateSource.
|
/// Create a new UpdateManager instance using the specified UpdateSource.
|
||||||
/// This will return an error if the application is not yet installed.
|
/// This will return an error if the application is not yet installed.
|
||||||
/// ## Example:
|
/// ## Example:
|
||||||
@@ -155,12 +153,12 @@ impl<T: UpdateSource> UpdateManager<T> {
|
|||||||
/// let source = sources::HttpSource::new("https://the.place/you-host/updates");
|
/// let source = sources::HttpSource::new("https://the.place/you-host/updates");
|
||||||
/// let um = UpdateManager::new(source, None);
|
/// let um = UpdateManager::new(source, None);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new(source: T, options: Option<UpdateOptions>) -> Result<UpdateManager<T>, Error> {
|
pub fn new<T: UpdateSource + 'a>(source: T, options: Option<UpdateOptions>) -> Result<UpdateManager::<'a>, Error> {
|
||||||
Ok(UpdateManager {
|
Ok(UpdateManager {
|
||||||
paths: locator::auto_locate()?,
|
paths: locator::auto_locate()?,
|
||||||
allow_version_downgrade: options.as_ref().map(|f| f.AllowVersionDowngrade).unwrap_or(false),
|
allow_version_downgrade: options.as_ref().map(|f| f.AllowVersionDowngrade).unwrap_or(false),
|
||||||
explicit_channel: options.as_ref().map(|f| f.ExplicitChannel.clone()).unwrap_or(None),
|
explicit_channel: options.as_ref().map(|f| f.ExplicitChannel.clone()).unwrap_or(None),
|
||||||
source,
|
source: Rc::new(Box::new(source)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +230,6 @@ impl<T: UpdateSource> UpdateManager<T> {
|
|||||||
|
|
||||||
debug!("Latest remote release: {} ({}).", remote_asset.FileName, remote_version.to_string());
|
debug!("Latest remote release: {} ({}).", remote_asset.FileName, remote_version.to_string());
|
||||||
|
|
||||||
|
|
||||||
if remote_version > app.version {
|
if remote_version > app.version {
|
||||||
info!("Found newer remote release available ({} -> {}).", app.version, remote_version);
|
info!("Found newer remote release available ({} -> {}).", app.version, remote_version);
|
||||||
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: false }))
|
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: false }))
|
||||||
@@ -260,13 +257,13 @@ impl<T: UpdateSource> UpdateManager<T> {
|
|||||||
async_std::task::spawn_blocking(move || self_clone.check_for_updates())
|
async_std::task::spawn_blocking(move || self_clone.check_for_updates())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads the specified updates to the local app packages directory. If the update contains delta packages and the delta feature is enabled
|
/// Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional Sender.
|
||||||
/// this method will attempt to unpack and prepare them. If there is no delta update available, or there is an error preparing delta
|
/// This function will acquire a global update lock so may fail if there is already another update operation in progress.
|
||||||
/// packages, this method will fall back to downloading the full version of the update. This function will acquire a global update lock
|
/// - If the update contains delta packages and the delta feature is enabled
|
||||||
/// so may fail if there is already another update operation in progress.
|
/// this method will attempt to unpack and prepare them.
|
||||||
pub fn download_updates<A>(&self, update: &UpdateInfo, progress: A) -> Result<(), Error>
|
/// - If there is no delta update available, or there is an error preparing delta
|
||||||
where
|
/// packages, this method will fall back to downloading the full version of the update.
|
||||||
A: FnMut(i16),
|
pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error>
|
||||||
{
|
{
|
||||||
let name = &update.TargetFullRelease.FileName;
|
let name = &update.TargetFullRelease.FileName;
|
||||||
let packages_dir = &self.paths.packages_dir;
|
let packages_dir = &self.paths.packages_dir;
|
||||||
@@ -321,25 +318,17 @@ impl<T: UpdateSource> UpdateManager<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
/// Downloads the specified updates to the local app packages directory. If the update contains delta packages and the delta feature is enabled
|
/// Downloads the specified updates to the local app packages directory. Progress is reported back to the caller via an optional Sender.
|
||||||
/// this method will attempt to unpack and prepare them. If there is no delta update available, or there is an error preparing delta
|
/// This function will acquire a global update lock so may fail if there is already another update operation in progress.
|
||||||
/// packages, this method will fall back to downloading the full version of the update. This function will acquire a global update lock
|
/// - If the update contains delta packages and the delta feature is enabled
|
||||||
/// so may fail if there is already another update operation in progress.
|
/// this method will attempt to unpack and prepare them.
|
||||||
|
/// - If there is no delta update available, or there is an error preparing delta
|
||||||
|
/// packages, this method will fall back to downloading the full version of the update.
|
||||||
pub fn download_updates_async(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> JoinHandle<Result<(), Error>>
|
pub fn download_updates_async(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> JoinHandle<Result<(), Error>>
|
||||||
where
|
|
||||||
T: 'static,
|
|
||||||
{
|
{
|
||||||
let self_clone = self.clone();
|
let self_clone = self.clone();
|
||||||
let update_clone = update.clone();
|
let update_clone = update.clone();
|
||||||
if let Some(p) = progress {
|
async_std::task::spawn_blocking(move || self_clone.download_updates(&update_clone, progress))
|
||||||
async_std::task::spawn_blocking(move || {
|
|
||||||
self_clone.download_updates(&update_clone, move |x| {
|
|
||||||
let _ = p.try_send(x);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
async_std::task::spawn_blocking(move || self_clone.download_updates(&update_clone, |_| {}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This will exit your app immediately, apply updates, and then optionally relaunch the app using the specified
|
/// This will exit your app immediately, apply updates, and then optionally relaunch the app using the specified
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::mpsc::Sender,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Abstraction for finding and downloading updates from a package source / repository.
|
/// Abstraction for finding and downloading updates from a package source / repository.
|
||||||
/// An implementation may copy a file from a local repository, download from a web address,
|
/// An implementation may copy a file from a local repository, download from a web address,
|
||||||
/// or even use third party services and parse proprietary data to produce a package feed.
|
/// or even use third party services and parse proprietary data to produce a package feed.
|
||||||
pub trait UpdateSource: Clone + Send + Sync {
|
pub trait UpdateSource: Send + Sync {
|
||||||
/// Retrieve the list of available remote releases from the package source. These releases
|
/// Retrieve the list of available remote releases from the package source. These releases
|
||||||
/// can subsequently be downloaded with download_release_entry.
|
/// can subsequently be downloaded with download_release_entry.
|
||||||
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error>;
|
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error>;
|
||||||
/// Download the specified VelopackAsset to the provided local file path.
|
/// Download the specified VelopackAsset to the provided local file path.
|
||||||
fn download_release_entry<A>(&self, asset: &VelopackAsset, local_file: &str, progress: A) -> Result<(), Error>
|
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error>;
|
||||||
where A: FnMut(i16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -44,15 +46,17 @@ impl UpdateSource for HttpSource {
|
|||||||
Ok(feed)
|
Ok(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_release_entry<A>(&self, asset: &VelopackAsset, local_file: &str, progress: A) -> Result<(), Error>
|
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
|
||||||
where A: FnMut(i16),
|
|
||||||
{
|
|
||||||
let path = self.url.trim_end_matches('/').to_owned() + "/";
|
let path = self.url.trim_end_matches('/').to_owned() + "/";
|
||||||
let url = url::Url::parse(&path)?;
|
let url = url::Url::parse(&path)?;
|
||||||
let asset_url = url.join(&asset.FileName)?;
|
let asset_url = url.join(&asset.FileName)?;
|
||||||
|
|
||||||
info!("About to download from URL '{}' to file '{}'", asset_url, local_file);
|
info!("About to download from URL '{}' to file '{}'", asset_url, local_file);
|
||||||
download::download_url_to_file(asset_url.as_str(), local_file, progress)?;
|
download::download_url_to_file(asset_url.as_str(), local_file, move |p| {
|
||||||
|
if let Some(progress_sender) = &progress_sender {
|
||||||
|
let _ = progress_sender.send(p);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,14 +87,16 @@ impl UpdateSource for FileSource {
|
|||||||
Ok(feed)
|
Ok(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_release_entry<A>(&self, asset: &VelopackAsset, local_file: &str, mut progress: A) -> Result<(), Error>
|
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
|
||||||
where A: FnMut(i16),
|
|
||||||
{
|
|
||||||
let asset_path = self.path.join(&asset.FileName);
|
let asset_path = self.path.join(&asset.FileName);
|
||||||
info!("About to copy from file '{}' to file '{}'", asset_path.display(), local_file);
|
info!("About to copy from file '{}' to file '{}'", asset_path.display(), local_file);
|
||||||
progress(50);
|
if let Some(progress_sender) = &progress_sender {
|
||||||
|
let _ = progress_sender.send(50);
|
||||||
|
}
|
||||||
std::fs::copy(asset_path, local_file)?;
|
std::fs::copy(asset_path, local_file)?;
|
||||||
progress(100);
|
if let Some(progress_sender) = &progress_sender {
|
||||||
|
let _ = progress_sender.send(100);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user