Initial POC
This commit is contained in:
commit
7c765fd48e
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
187
Cargo.lock
generated
Normal file
187
Cargo.lock
generated
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumset"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19be8061a06ab6f3a6cf21106c873578bf01bd42ad15e0311a9c76161cb1c753"
|
||||||
|
dependencies = [
|
||||||
|
"enumset_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumset_derive"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03e7b551eba279bf0fa88b83a46330168c1560a52a94f5126f892f0b364ab3e0"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.49"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.152"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.152"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92b5b431e8907b50339b51223b97d102db8d987ced36f6e4d03621db9316c834"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-updater"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"enumset",
|
||||||
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2"
|
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "system-updater"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/main.rs"
|
||||||
|
name = "sup"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
enumset = { version = "1.0" , feature = ["serde"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_yaml = "0.9"
|
35
examples/openSUSE.yml
Normal file
35
examples/openSUSE.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
systems:
|
||||||
|
- fetch:
|
||||||
|
command:
|
||||||
|
exe: sudo
|
||||||
|
params:
|
||||||
|
- zypper
|
||||||
|
- refresh
|
||||||
|
current_dir: null
|
||||||
|
env: {}
|
||||||
|
compile: null
|
||||||
|
install:
|
||||||
|
command:
|
||||||
|
exe: sudo
|
||||||
|
params:
|
||||||
|
- zypper
|
||||||
|
- dup
|
||||||
|
current_dir: null
|
||||||
|
env: {}
|
||||||
|
- install:
|
||||||
|
command:
|
||||||
|
exe: rustup
|
||||||
|
params:
|
||||||
|
- update
|
||||||
|
current_dir: null
|
||||||
|
env: {}
|
||||||
|
- install:
|
||||||
|
command:
|
||||||
|
exe: cargo
|
||||||
|
params:
|
||||||
|
- install-update
|
||||||
|
- -a
|
||||||
|
current_dir: null
|
||||||
|
env: {}
|
||||||
|
steps: Install
|
||||||
|
nice: null
|
208
src/command.rs
Normal file
208
src/command.rs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
use crate::*;
|
||||||
|
use enumset::EnumSetType;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, EnumSetType)]
|
||||||
|
//#[enumset(serialize_as_list)] // TODO: use it or not?
|
||||||
|
enum UpdateSteps {
|
||||||
|
Fetch,
|
||||||
|
Compile,
|
||||||
|
Install,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Updater {
|
||||||
|
systems: Vec<System>,
|
||||||
|
steps: UpdateSteps,
|
||||||
|
nice: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct System {
|
||||||
|
fetch: Option<Fetch>,
|
||||||
|
compile: Option<Compile>,
|
||||||
|
install: Install,
|
||||||
|
// deps or rDeps : Tree
|
||||||
|
// exclusive_with : List
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Fetch {
|
||||||
|
command: Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Compile {
|
||||||
|
command: Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Install {
|
||||||
|
command: Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Cmd {
|
||||||
|
exe: String,
|
||||||
|
params: Vec<String>,
|
||||||
|
current_dir: Option<PathBuf>,
|
||||||
|
env: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Updater {
|
||||||
|
fn new() -> Updater {
|
||||||
|
let mut up = Updater {
|
||||||
|
systems: vec![],
|
||||||
|
steps: UpdateSteps::Fetch,
|
||||||
|
nice: None,
|
||||||
|
};
|
||||||
|
up.systems.push(System {
|
||||||
|
fetch: None,
|
||||||
|
compile: None,
|
||||||
|
install: Install {
|
||||||
|
command: Cmd::new(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
up
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To create a sample config from code
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn write_config(&self, opt: &Opt) {
|
||||||
|
use ::std::io::Write;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
|
||||||
|
let mut f = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&opt.config_file)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
f.write_all(serde_yaml::to_string(&self).unwrap().as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_config(opt: &Opt) -> Result<Updater> {
|
||||||
|
// let u = Updater::new();
|
||||||
|
// u.write_config(opt);
|
||||||
|
|
||||||
|
let file = std::fs::read_to_string(&opt.config_file).unwrap();
|
||||||
|
Ok(serde_yaml::from_str(&file).unwrap())
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_all(&self) -> Result<()> {
|
||||||
|
let mut errors = vec![];
|
||||||
|
for sys in &self.systems {
|
||||||
|
if let Err(err) = self.update(sys) {
|
||||||
|
eprintln!("Error catched {}", err);
|
||||||
|
errors.push(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
if errors.len() == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::new(
|
||||||
|
errors,
|
||||||
|
ErrorKind::Fetch, // TODO: Why should I choose here?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&self, sys: &System) -> Result<()> {
|
||||||
|
if self.steps == UpdateSteps::Fetch {
|
||||||
|
sys.fetch()?;
|
||||||
|
}
|
||||||
|
if self.steps == UpdateSteps::Compile {
|
||||||
|
sys.compile()?;
|
||||||
|
}
|
||||||
|
if self.steps == UpdateSteps::Install {
|
||||||
|
sys.install()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl System {
|
||||||
|
pub fn fetch(&self) -> Result<()> {
|
||||||
|
if let Some(fetch) = &self.fetch {
|
||||||
|
fetch.command.execute()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile(&self) -> Result<()> {
|
||||||
|
if let Some(compile) = &self.compile {
|
||||||
|
compile.command.execute()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install(&self) -> Result<()> {
|
||||||
|
self.install.command.execute()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd {
|
||||||
|
fn new() -> Cmd {
|
||||||
|
Cmd {
|
||||||
|
exe: "".into(),
|
||||||
|
params: vec![],
|
||||||
|
current_dir: None,
|
||||||
|
env: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(&self) -> Result<()> {
|
||||||
|
let mut cmd = Command::new(&self.exe);
|
||||||
|
cmd.args(&self.params)
|
||||||
|
.env_clear()
|
||||||
|
.envs(&self.env)
|
||||||
|
// .stdout(cfg)
|
||||||
|
// .stderr(cfg)
|
||||||
|
;
|
||||||
|
|
||||||
|
if let Some(cdir) = &self.current_dir {
|
||||||
|
cmd.current_dir(std::fs::canonicalize(cdir).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Executing {}", self);
|
||||||
|
// TODO: Ask if ok or
|
||||||
|
if cmd.status().unwrap().success() {
|
||||||
|
println!("Youpi !");
|
||||||
|
// Other checks?
|
||||||
|
}
|
||||||
|
println!("Exécutée");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Cmd {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let command = if self.params.is_empty() {
|
||||||
|
self.exe.clone()
|
||||||
|
} else {
|
||||||
|
format!("{} {}", &self.exe, &self.params.join(" "))
|
||||||
|
};
|
||||||
|
write!(f, "`{}`", command)?;
|
||||||
|
if let Some(cdir) = &self.current_dir {
|
||||||
|
write!(f, " in {:?}", cdir)?;
|
||||||
|
}
|
||||||
|
if !self.env.is_empty() {
|
||||||
|
writeln!(f, " with the following environment variable:")?;
|
||||||
|
writeln!(f, "{:?}", self.env)
|
||||||
|
} else {
|
||||||
|
writeln!(f, " without any environment variable.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/errors.rs
Normal file
58
src/errors.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// An error that can occur in this crate.
|
||||||
|
///
|
||||||
|
/// Generally, this error corresponds to problems with underlying process.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error {
|
||||||
|
source: Vec<Error>,
|
||||||
|
kind: ErrorKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
Config,
|
||||||
|
Fetch, // TODO: merge into "Update" or "Command" type of error? => Have this as an other level of error?
|
||||||
|
Compile, // TODO: merge into "Update" or "Command" type of error? => Have this as an other level of error?
|
||||||
|
Install, // TODO: merge into "Update" or "Command" type of error? => Have this as an other level of error?
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn new(source: Vec<Error>, kind: ErrorKind) -> Error {
|
||||||
|
Error { source, kind }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the kind of this error.
|
||||||
|
pub fn kind(&self) -> &ErrorKind {
|
||||||
|
&self.kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
|
if self.source.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&self.source[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
match self.kind {
|
||||||
|
ErrorKind::Config => write!(
|
||||||
|
f,
|
||||||
|
"Could not read configuration file: {}",
|
||||||
|
self.source().unwrap()
|
||||||
|
),
|
||||||
|
ErrorKind::Fetch => write!(f, "Could not install: {}", self.source().unwrap()),
|
||||||
|
ErrorKind::Compile => write!(f, "Could not install: {}", self.source().unwrap()),
|
||||||
|
ErrorKind::Install => write!(f, "Could not install: {}", self.source().unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
src/lib.rs
Normal file
20
src/lib.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
mod command;
|
||||||
|
mod errors;
|
||||||
|
|
||||||
|
use command::*;
|
||||||
|
use errors::*;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::result;
|
||||||
|
|
||||||
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
|
pub struct Opt {
|
||||||
|
pub config_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(opt: &Opt) {
|
||||||
|
let updater = Updater::parse_config(opt).unwrap();
|
||||||
|
|
||||||
|
dbg!(updater).update_all().unwrap();
|
||||||
|
}
|
13
src/main.rs
Normal file
13
src/main.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use system_updater::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let opt = Opt {
|
||||||
|
// + One config file?
|
||||||
|
// + A config subFolder and execute in alphabetical order?
|
||||||
|
// - A master config file that list the sub/real files? no if it mean parsing 2 differents formats
|
||||||
|
//
|
||||||
|
// Hardcoded for now
|
||||||
|
config_file: "examples/openSUSE.yml".into(), // Default to something like -> "~/.config/system-updater/list.yml".into(),
|
||||||
|
};
|
||||||
|
run(&opt);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user