Works as an MVP
This commit is contained in:
parent
a0014cf247
commit
3ba9097489
3 changed files with 329 additions and 20 deletions
196
Cargo.lock
generated
196
Cargo.lock
generated
|
|
@ -8,12 +8,50 @@ version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"unicode-width",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"shell-words",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
@ -30,6 +68,23 @@ version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
|
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.164"
|
version = "0.2.164"
|
||||||
|
|
@ -46,6 +101,8 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||||
name = "mvw"
|
name = "mvw"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dialoguer",
|
||||||
|
"rand",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -55,6 +112,63 @@ version = "1.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.41"
|
version = "0.38.41"
|
||||||
|
|
@ -68,6 +182,23 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.87"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.14.0"
|
version = "3.14.0"
|
||||||
|
|
@ -81,6 +212,44 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
@ -162,3 +331,30 @@ name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.7.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,5 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tempfile = "3.14.0"
|
tempfile = "3.14.0"
|
||||||
|
dialoguer = "0.11.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
|
||||||
151
src/main.rs
151
src/main.rs
|
|
@ -1,13 +1,24 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::path::Path;
|
use std::io::{ErrorKind, Write, BufRead, BufReader};
|
||||||
use std::io::ErrorKind;
|
use std::process;
|
||||||
use std::io::Write;
|
use std::process::Command;
|
||||||
|
use std::collections::HashMap;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
use dialoguer::Confirm;
|
||||||
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
|
|
||||||
const LOCK_FILE: &str = ".mvwrap";
|
const LOCK_FILE: &str = ".mvwrap";
|
||||||
|
|
||||||
|
struct Cleanup;
|
||||||
|
|
||||||
|
impl Drop for Cleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unlock_dir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_current_dir() -> Vec<String> {
|
fn read_current_dir() -> Vec<String> {
|
||||||
let paths : Vec<PathBuf> = match fs::read_dir(".") {
|
let paths : Vec<PathBuf> = match fs::read_dir(".") {
|
||||||
Err(e) if e.kind() == ErrorKind::NotFound => Vec::new(),
|
Err(e) if e.kind() == ErrorKind::NotFound => Vec::new(),
|
||||||
|
|
@ -47,7 +58,7 @@ fn unlock_dir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_temp_file(paths: Vec<String>) -> String {
|
fn create_temp_file(paths: &Vec<String>) -> String {
|
||||||
let mut tmpfile = match NamedTempFile::new(){
|
let mut tmpfile = match NamedTempFile::new(){
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(e) => panic!("Could not create tempfile: {}", e),
|
Err(e) => panic!("Could not create tempfile: {}", e),
|
||||||
|
|
@ -67,35 +78,135 @@ fn create_temp_file(paths: Vec<String>) -> String {
|
||||||
filepath
|
filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_temp_file(tmpfile: String) {
|
fn remove_temp_file(tmpfile: &String) {
|
||||||
if Path::new(&tmpfile).is_file() {
|
if Path::new(tmpfile).is_file() {
|
||||||
let _result = match fs::remove_file(&tmpfile){
|
let _result = match fs::remove_file(tmpfile){
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(_) => panic!("Could not remove tempfile: {}", &tmpfile),
|
Err(_) => panic!("Could not remove tempfile: {}", tmpfile),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn edit_temp_file(tmpfile: &String) {
|
||||||
lock_dir();
|
let _output = Command::new("vi")
|
||||||
|
.arg(tmpfile)
|
||||||
|
.status()
|
||||||
|
.expect("failed to execute editor process");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_temp_file(tmpfile: &String) -> Vec<String> {
|
||||||
|
let file = File::open(tmpfile).expect("no such file");
|
||||||
|
let buf = BufReader::new(file);
|
||||||
|
buf.lines()
|
||||||
|
.map(|l| l.expect("Could not parse line"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_length(dst_paths: &Vec<String>) -> usize {
|
||||||
|
let map = dst_paths.into_iter().map(|x| (x, x)).collect::<HashMap<_, _>>();
|
||||||
|
map.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_filename(dst_paths: &Vec<String>) -> String {
|
||||||
|
let mut string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
||||||
|
|
||||||
|
// Generate unique name that does not exist in dir and is not in dst_paths
|
||||||
|
while dst_paths.iter().any(|j| j==&string) || Path::new(&string).is_file() {
|
||||||
|
string = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
|
||||||
|
}
|
||||||
|
string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_safely(src_paths: &Vec<String>, dst_paths: &Vec<String>) {
|
||||||
|
let src_len = src_paths.len();
|
||||||
|
let mut intermediate_files: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
|
for i in 0..src_len {
|
||||||
|
if src_paths[i] != dst_paths[i] {
|
||||||
|
// is the destination already in the source list?
|
||||||
|
// if so, an intermediate file is needed
|
||||||
|
if src_paths.iter().any(|j| j==&dst_paths[i]) {
|
||||||
|
let unique = unique_filename(&dst_paths);
|
||||||
|
intermediate_files.insert(unique.clone(), dst_paths[i].clone());
|
||||||
|
fs::rename(&src_paths[i], &unique).expect("failed to rename file");
|
||||||
|
println!("Moving {} -> {}", src_paths[i], unique);
|
||||||
|
} else {
|
||||||
|
fs::rename(&src_paths[i], &dst_paths[i]).expect("failed to rename file");
|
||||||
|
println!("Moving {} -> {}", src_paths[i], dst_paths[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (src, dst) in intermediate_files.iter() {
|
||||||
|
fs::rename(&src, &dst).expect("failed to rename file");
|
||||||
|
println!("Moving {} -> {}", src, dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_checks(src_paths: &Vec<String>, dst_paths: &Vec<String>) -> bool {
|
||||||
|
// Make sure there are an equal number of sources and destinations
|
||||||
|
if src_paths.len() != dst_paths.len() {
|
||||||
|
println!("ERROR: Source and target list don't have the same number of lines.");
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure destination names are not empty
|
||||||
|
if dst_paths.iter().any(|i| i=="") {
|
||||||
|
println!("ERROR: You can't move to empty names.");
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure all destination files are unique
|
||||||
|
let dst_paths_length_unique = unique_length(&dst_paths);
|
||||||
|
if dst_paths.len() != dst_paths_length_unique {
|
||||||
|
println!("Clashing destination names!");
|
||||||
|
// TODO: show which ones!
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Place lockfile to not have multiple mvw processes running in the same dir at the same time
|
||||||
|
lock_dir();
|
||||||
|
let _cleanup = Cleanup; // Cleanup LOCK_FILE on panic
|
||||||
|
|
||||||
|
// read directory contents into vector of Strings
|
||||||
let paths = read_current_dir();
|
let paths = read_current_dir();
|
||||||
|
|
||||||
// create named tempfile and fill with paths
|
// create named tempfile and fill with paths
|
||||||
let temp_file = create_temp_file(paths);
|
let temp_file = create_temp_file(&paths);
|
||||||
|
|
||||||
// edit tempfile
|
//display_temp_file(&temp_file);
|
||||||
|
|
||||||
// read and process tempfile
|
edit_temp_file(&temp_file);
|
||||||
println!("In file: {}", temp_file);
|
|
||||||
|
|
||||||
let contents = fs::read_to_string(temp_file.clone())
|
let mut new_paths = read_temp_file(&temp_file);
|
||||||
.expect("Should have been able to read the file");
|
|
||||||
|
|
||||||
println!("With text:\n{contents}");
|
// Compare length, if not equal offer to try again (or panic for now)
|
||||||
println!("----- END ----");
|
while run_checks(&paths, &new_paths) {
|
||||||
|
|
||||||
remove_temp_file(temp_file);
|
let confirmation = Confirm::new()
|
||||||
|
.with_prompt("Continue editing?")
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if ! confirmation {
|
||||||
|
unlock_dir();
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_temp_file(&temp_file);
|
||||||
|
new_paths = read_temp_file(&temp_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (also don't overwrite existing files that are not in the input list!)
|
||||||
|
|
||||||
|
|
||||||
|
move_safely(&paths, &new_paths);
|
||||||
|
|
||||||
|
//display_temp_file(&temp_file);
|
||||||
|
|
||||||
|
remove_temp_file(&temp_file);
|
||||||
unlock_dir();
|
unlock_dir();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue