mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
Merge pull request #2115 from tertsdiepraam/ls/reduce_write_calls
`ls`: reduce write syscalls & cleanup
This commit is contained in:
commit
e667cc2641
1 changed files with 95 additions and 91 deletions
|
@ -22,19 +22,22 @@ use lscolors::LsColors;
|
||||||
use number_prefix::NumberPrefix;
|
use number_prefix::NumberPrefix;
|
||||||
use once_cell::unsync::OnceCell;
|
use once_cell::unsync::OnceCell;
|
||||||
use quoting_style::{escape_name, QuotingStyle};
|
use quoting_style::{escape_name, QuotingStyle};
|
||||||
#[cfg(unix)]
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::{self, DirEntry, FileType, Metadata};
|
|
||||||
#[cfg(any(unix, target_os = "redox"))]
|
|
||||||
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::fs::MetadataExt;
|
use std::os::windows::fs::MetadataExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
cmp::Reverse,
|
||||||
|
fs::{self, DirEntry, FileType, Metadata},
|
||||||
|
io::{stdout, BufWriter, Stdout, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::exit,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::time::Duration;
|
use std::{
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
collections::HashMap,
|
||||||
use std::{cmp::Reverse, process::exit};
|
os::unix::fs::{FileTypeExt, MetadataExt},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
|
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
|
||||||
use time::{strftime, Timespec};
|
use time::{strftime, Timespec};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -86,10 +89,8 @@ pub mod options {
|
||||||
pub static C: &str = "quote-name";
|
pub static C: &str = "quote-name";
|
||||||
}
|
}
|
||||||
pub static QUOTING_STYLE: &str = "quoting-style";
|
pub static QUOTING_STYLE: &str = "quoting-style";
|
||||||
|
|
||||||
pub mod indicator_style {
|
pub mod indicator_style {
|
||||||
pub static NONE: &str = "none";
|
pub static SLASH: &str = "p";
|
||||||
pub static SLASH: &str = "slash";
|
|
||||||
pub static FILE_TYPE: &str = "file-type";
|
pub static FILE_TYPE: &str = "file-type";
|
||||||
pub static CLASSIFY: &str = "classify";
|
pub static CLASSIFY: &str = "classify";
|
||||||
}
|
}
|
||||||
|
@ -108,9 +109,6 @@ pub mod options {
|
||||||
pub static TIME: &str = "time";
|
pub static TIME: &str = "time";
|
||||||
pub static IGNORE_BACKUPS: &str = "ignore-backups";
|
pub static IGNORE_BACKUPS: &str = "ignore-backups";
|
||||||
pub static DIRECTORY: &str = "directory";
|
pub static DIRECTORY: &str = "directory";
|
||||||
pub static CLASSIFY: &str = "classify";
|
|
||||||
pub static FILE_TYPE: &str = "file-type";
|
|
||||||
pub static SLASH: &str = "p";
|
|
||||||
pub static INODE: &str = "inode";
|
pub static INODE: &str = "inode";
|
||||||
pub static REVERSE: &str = "reverse";
|
pub static REVERSE: &str = "reverse";
|
||||||
pub static RECURSIVE: &str = "recursive";
|
pub static RECURSIVE: &str = "recursive";
|
||||||
|
@ -425,19 +423,11 @@ impl Config {
|
||||||
"slash" => IndicatorStyle::Slash,
|
"slash" => IndicatorStyle::Slash,
|
||||||
&_ => IndicatorStyle::None,
|
&_ => IndicatorStyle::None,
|
||||||
}
|
}
|
||||||
} else if options.is_present(options::indicator_style::NONE) {
|
} else if options.is_present(options::indicator_style::CLASSIFY) {
|
||||||
IndicatorStyle::None
|
|
||||||
} else if options.is_present(options::indicator_style::CLASSIFY)
|
|
||||||
|| options.is_present(options::CLASSIFY)
|
|
||||||
{
|
|
||||||
IndicatorStyle::Classify
|
IndicatorStyle::Classify
|
||||||
} else if options.is_present(options::indicator_style::SLASH)
|
} else if options.is_present(options::indicator_style::SLASH) {
|
||||||
|| options.is_present(options::SLASH)
|
|
||||||
{
|
|
||||||
IndicatorStyle::Slash
|
IndicatorStyle::Slash
|
||||||
} else if options.is_present(options::indicator_style::FILE_TYPE)
|
} else if options.is_present(options::indicator_style::FILE_TYPE) {
|
||||||
|| options.is_present(options::FILE_TYPE)
|
|
||||||
{
|
|
||||||
IndicatorStyle::FileType
|
IndicatorStyle::FileType
|
||||||
} else {
|
} else {
|
||||||
IndicatorStyle::None
|
IndicatorStyle::None
|
||||||
|
@ -963,45 +953,45 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.possible_values(&["none", "slash", "file-type", "classify"])
|
.possible_values(&["none", "slash", "file-type", "classify"])
|
||||||
.overrides_with_all(&[
|
.overrides_with_all(&[
|
||||||
options::FILE_TYPE,
|
options::indicator_style::FILE_TYPE,
|
||||||
options::SLASH,
|
options::indicator_style::SLASH,
|
||||||
options::CLASSIFY,
|
options::indicator_style::CLASSIFY,
|
||||||
options::INDICATOR_STYLE,
|
options::INDICATOR_STYLE,
|
||||||
]))
|
]))
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::CLASSIFY)
|
Arg::with_name(options::indicator_style::CLASSIFY)
|
||||||
.short("F")
|
.short("F")
|
||||||
.long(options::CLASSIFY)
|
.long(options::indicator_style::CLASSIFY)
|
||||||
.help("Append a character to each file name indicating the file type. Also, for \
|
.help("Append a character to each file name indicating the file type. Also, for \
|
||||||
regular files that are executable, append '*'. The file type indicators are \
|
regular files that are executable, append '*'. The file type indicators are \
|
||||||
'/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \
|
'/' for directories, '@' for symbolic links, '|' for FIFOs, '=' for sockets, \
|
||||||
'>' for doors, and nothing for regular files.")
|
'>' for doors, and nothing for regular files.")
|
||||||
.overrides_with_all(&[
|
.overrides_with_all(&[
|
||||||
options::FILE_TYPE,
|
options::indicator_style::FILE_TYPE,
|
||||||
options::SLASH,
|
options::indicator_style::SLASH,
|
||||||
options::CLASSIFY,
|
options::indicator_style::CLASSIFY,
|
||||||
options::INDICATOR_STYLE,
|
options::INDICATOR_STYLE,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::FILE_TYPE)
|
Arg::with_name(options::indicator_style::FILE_TYPE)
|
||||||
.long(options::FILE_TYPE)
|
.long(options::indicator_style::FILE_TYPE)
|
||||||
.help("Same as --classify, but do not append '*'")
|
.help("Same as --classify, but do not append '*'")
|
||||||
.overrides_with_all(&[
|
.overrides_with_all(&[
|
||||||
options::FILE_TYPE,
|
options::indicator_style::FILE_TYPE,
|
||||||
options::SLASH,
|
options::indicator_style::SLASH,
|
||||||
options::CLASSIFY,
|
options::indicator_style::CLASSIFY,
|
||||||
options::INDICATOR_STYLE,
|
options::INDICATOR_STYLE,
|
||||||
]))
|
]))
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(options::SLASH)
|
Arg::with_name(options::indicator_style::SLASH)
|
||||||
.short(options::SLASH)
|
.short(options::indicator_style::SLASH)
|
||||||
.help("Append / indicator to directories."
|
.help("Append / indicator to directories."
|
||||||
)
|
)
|
||||||
.overrides_with_all(&[
|
.overrides_with_all(&[
|
||||||
options::FILE_TYPE,
|
options::indicator_style::FILE_TYPE,
|
||||||
options::SLASH,
|
options::indicator_style::SLASH,
|
||||||
options::CLASSIFY,
|
options::indicator_style::CLASSIFY,
|
||||||
options::INDICATOR_STYLE,
|
options::INDICATOR_STYLE,
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
@ -1092,6 +1082,8 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
|
||||||
let mut dirs = Vec::<PathData>::new();
|
let mut dirs = Vec::<PathData>::new();
|
||||||
let mut has_failed = false;
|
let mut has_failed = false;
|
||||||
|
|
||||||
|
let mut out = BufWriter::new(stdout());
|
||||||
|
|
||||||
for loc in locs {
|
for loc in locs {
|
||||||
let p = PathBuf::from(&loc);
|
let p = PathBuf::from(&loc);
|
||||||
if !p.exists() {
|
if !p.exists() {
|
||||||
|
@ -1118,14 +1110,14 @@ fn list(locs: Vec<String>, config: Config) -> i32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort_entries(&mut files, &config);
|
sort_entries(&mut files, &config);
|
||||||
display_items(&files, None, &config);
|
display_items(&files, None, &config, &mut out);
|
||||||
|
|
||||||
sort_entries(&mut dirs, &config);
|
sort_entries(&mut dirs, &config);
|
||||||
for dir in dirs {
|
for dir in dirs {
|
||||||
if number_of_locs > 1 {
|
if number_of_locs > 1 {
|
||||||
println!("\n{}:", dir.p_buf.display());
|
let _ = writeln!(out, "\n{}:", dir.p_buf.display());
|
||||||
}
|
}
|
||||||
enter_directory(&dir, &config);
|
enter_directory(&dir, &config, &mut out);
|
||||||
}
|
}
|
||||||
if has_failed {
|
if has_failed {
|
||||||
1
|
1
|
||||||
|
@ -1178,7 +1170,7 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool {
|
||||||
!config.ignore_patterns.is_match(&ffi_name)
|
!config.ignore_patterns.is_match(&ffi_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_directory(dir: &PathData, config: &Config) {
|
fn enter_directory(dir: &PathData, config: &Config, out: &mut BufWriter<Stdout>) {
|
||||||
let mut entries: Vec<_> = if config.files == Files::All {
|
let mut entries: Vec<_> = if config.files == Files::All {
|
||||||
vec![
|
vec![
|
||||||
PathData::new(dir.p_buf.join("."), None, config, false),
|
PathData::new(dir.p_buf.join("."), None, config, false),
|
||||||
|
@ -1198,7 +1190,7 @@ fn enter_directory(dir: &PathData, config: &Config) {
|
||||||
|
|
||||||
entries.append(&mut temp);
|
entries.append(&mut temp);
|
||||||
|
|
||||||
display_items(&entries, Some(&dir.p_buf), config);
|
display_items(&entries, Some(&dir.p_buf), config, out);
|
||||||
|
|
||||||
if config.recursive {
|
if config.recursive {
|
||||||
for e in entries
|
for e in entries
|
||||||
|
@ -1206,8 +1198,8 @@ fn enter_directory(dir: &PathData, config: &Config) {
|
||||||
.skip(if config.files == Files::All { 2 } else { 0 })
|
.skip(if config.files == Files::All { 2 } else { 0 })
|
||||||
.filter(|p| p.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
.filter(|p| p.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
||||||
{
|
{
|
||||||
println!("\n{}:", e.p_buf.display());
|
let _ = writeln!(out, "\n{}:", e.p_buf.display());
|
||||||
enter_directory(&e, config);
|
enter_directory(&e, config, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1235,7 +1227,12 @@ fn pad_left(string: String, count: usize) -> String {
|
||||||
format!("{:>width$}", string, width = count)
|
format!("{:>width$}", string, width = count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_items(items: &[PathData], strip: Option<&Path>, config: &Config) {
|
fn display_items(
|
||||||
|
items: &[PathData],
|
||||||
|
strip: Option<&Path>,
|
||||||
|
config: &Config,
|
||||||
|
out: &mut BufWriter<Stdout>,
|
||||||
|
) {
|
||||||
if config.format == Format::Long {
|
if config.format == Format::Long {
|
||||||
let (mut max_links, mut max_size) = (1, 1);
|
let (mut max_links, mut max_size) = (1, 1);
|
||||||
for item in items {
|
for item in items {
|
||||||
|
@ -1244,7 +1241,7 @@ fn display_items(items: &[PathData], strip: Option<&Path>, config: &Config) {
|
||||||
max_size = size.max(max_size);
|
max_size = size.max(max_size);
|
||||||
}
|
}
|
||||||
for item in items {
|
for item in items {
|
||||||
display_item_long(item, strip, max_links, max_size, config);
|
display_item_long(item, strip, max_links, max_size, config, out);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let names = items
|
let names = items
|
||||||
|
@ -1252,42 +1249,51 @@ fn display_items(items: &[PathData], strip: Option<&Path>, config: &Config) {
|
||||||
.filter_map(|i| display_file_name(&i, strip, config));
|
.filter_map(|i| display_file_name(&i, strip, config));
|
||||||
|
|
||||||
match (&config.format, config.width) {
|
match (&config.format, config.width) {
|
||||||
(Format::Columns, Some(width)) => display_grid(names, width, Direction::TopToBottom),
|
(Format::Columns, Some(width)) => {
|
||||||
(Format::Across, Some(width)) => display_grid(names, width, Direction::LeftToRight),
|
display_grid(names, width, Direction::TopToBottom, out)
|
||||||
|
}
|
||||||
|
(Format::Across, Some(width)) => {
|
||||||
|
display_grid(names, width, Direction::LeftToRight, out)
|
||||||
|
}
|
||||||
(Format::Commas, width_opt) => {
|
(Format::Commas, width_opt) => {
|
||||||
let term_width = width_opt.unwrap_or(1);
|
let term_width = width_opt.unwrap_or(1);
|
||||||
let mut current_col = 0;
|
let mut current_col = 0;
|
||||||
let mut names = names;
|
let mut names = names;
|
||||||
if let Some(name) = names.next() {
|
if let Some(name) = names.next() {
|
||||||
print!("{}", name.contents);
|
let _ = write!(out, "{}", name.contents);
|
||||||
current_col = name.width as u16 + 2;
|
current_col = name.width as u16 + 2;
|
||||||
}
|
}
|
||||||
for name in names {
|
for name in names {
|
||||||
let name_width = name.width as u16;
|
let name_width = name.width as u16;
|
||||||
if current_col + name_width + 1 > term_width {
|
if current_col + name_width + 1 > term_width {
|
||||||
current_col = name_width + 2;
|
current_col = name_width + 2;
|
||||||
print!(",\n{}", name.contents);
|
let _ = write!(out, ",\n{}", name.contents);
|
||||||
} else {
|
} else {
|
||||||
current_col += name_width + 2;
|
current_col += name_width + 2;
|
||||||
print!(", {}", name.contents);
|
let _ = write!(out, ", {}", name.contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Current col is never zero again if names have been printed.
|
// Current col is never zero again if names have been printed.
|
||||||
// So we print a newline.
|
// So we print a newline.
|
||||||
if current_col > 0 {
|
if current_col > 0 {
|
||||||
println!();
|
let _ = writeln!(out,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
for name in names {
|
for name in names {
|
||||||
println!("{}", name.contents);
|
let _ = writeln!(out, "{}", name.contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_grid(names: impl Iterator<Item = Cell>, width: u16, direction: Direction) {
|
fn display_grid(
|
||||||
|
names: impl Iterator<Item = Cell>,
|
||||||
|
width: u16,
|
||||||
|
direction: Direction,
|
||||||
|
out: &mut BufWriter<Stdout>,
|
||||||
|
) {
|
||||||
let mut grid = Grid::new(GridOptions {
|
let mut grid = Grid::new(GridOptions {
|
||||||
filling: Filling::Spaces(2),
|
filling: Filling::Spaces(2),
|
||||||
direction,
|
direction,
|
||||||
|
@ -1298,9 +1304,13 @@ fn display_grid(names: impl Iterator<Item = Cell>, width: u16, direction: Direct
|
||||||
}
|
}
|
||||||
|
|
||||||
match grid.fit_into_width(width as usize) {
|
match grid.fit_into_width(width as usize) {
|
||||||
Some(output) => print!("{}", output),
|
Some(output) => {
|
||||||
|
let _ = write!(out, "{}", output);
|
||||||
|
}
|
||||||
// Width is too small for the grid, so we fit it in one column
|
// Width is too small for the grid, so we fit it in one column
|
||||||
None => print!("{}", grid.fit_into_columns(1)),
|
None => {
|
||||||
|
let _ = write!(out, "{}", grid.fit_into_columns(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1312,6 +1322,7 @@ fn display_item_long(
|
||||||
max_links: usize,
|
max_links: usize,
|
||||||
max_size: usize,
|
max_size: usize,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
out: &mut BufWriter<Stdout>,
|
||||||
) {
|
) {
|
||||||
let md = match item.md() {
|
let md = match item.md() {
|
||||||
None => {
|
None => {
|
||||||
|
@ -1325,11 +1336,12 @@ fn display_item_long(
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
if config.inode {
|
if config.inode {
|
||||||
print!("{} ", get_inode(&md));
|
let _ = write!(out, "{} ", get_inode(&md));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print!(
|
let _ = write!(
|
||||||
|
out,
|
||||||
"{}{} {}",
|
"{}{} {}",
|
||||||
display_file_type(md.file_type()),
|
display_file_type(md.file_type()),
|
||||||
display_permissions(&md),
|
display_permissions(&md),
|
||||||
|
@ -1337,20 +1349,21 @@ fn display_item_long(
|
||||||
);
|
);
|
||||||
|
|
||||||
if config.long.owner {
|
if config.long.owner {
|
||||||
print!(" {}", display_uname(&md, config));
|
let _ = write!(out, " {}", display_uname(&md, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.long.group {
|
if config.long.group {
|
||||||
print!(" {}", display_group(&md, config));
|
let _ = write!(out, " {}", display_group(&md, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Author is only different from owner on GNU/Hurd, so we reuse
|
// Author is only different from owner on GNU/Hurd, so we reuse
|
||||||
// the owner, since GNU/Hurd is not currently supported by Rust.
|
// the owner, since GNU/Hurd is not currently supported by Rust.
|
||||||
if config.long.author {
|
if config.long.author {
|
||||||
print!(" {}", display_uname(&md, config));
|
let _ = write!(out, " {}", display_uname(&md, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
" {} {} {}",
|
" {} {} {}",
|
||||||
pad_left(display_file_size(&md, config), max_size),
|
pad_left(display_file_size(&md, config), max_size),
|
||||||
display_date(&md, config),
|
display_date(&md, config),
|
||||||
|
@ -1380,14 +1393,10 @@ fn cached_uid2usr(uid: u32) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut uid_cache = UID_CACHE.lock().unwrap();
|
let mut uid_cache = UID_CACHE.lock().unwrap();
|
||||||
match uid_cache.get(&uid) {
|
uid_cache
|
||||||
Some(usr) => usr.clone(),
|
.entry(uid)
|
||||||
None => {
|
.or_insert_with(|| entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()))
|
||||||
let usr = entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string());
|
.clone()
|
||||||
uid_cache.insert(uid, usr.clone());
|
|
||||||
usr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -1406,14 +1415,10 @@ fn cached_gid2grp(gid: u32) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut gid_cache = GID_CACHE.lock().unwrap();
|
let mut gid_cache = GID_CACHE.lock().unwrap();
|
||||||
match gid_cache.get(&gid) {
|
gid_cache
|
||||||
Some(grp) => grp.clone(),
|
.entry(gid)
|
||||||
None => {
|
.or_insert_with(|| entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()))
|
||||||
let grp = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string());
|
.clone()
|
||||||
gid_cache.insert(gid, grp.clone());
|
|
||||||
grp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -1431,7 +1436,6 @@ fn display_uname(_metadata: &Metadata, _config: &Config) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn display_group(_metadata: &Metadata, _config: &Config) -> String {
|
fn display_group(_metadata: &Metadata, _config: &Config) -> String {
|
||||||
"somegroup".to_string()
|
"somegroup".to_string()
|
||||||
}
|
}
|
||||||
|
@ -1506,13 +1510,13 @@ fn display_file_size(metadata: &Metadata, config: &Config) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_file_type(file_type: FileType) -> String {
|
fn display_file_type(file_type: FileType) -> char {
|
||||||
if file_type.is_dir() {
|
if file_type.is_dir() {
|
||||||
"d".to_string()
|
'd'
|
||||||
} else if file_type.is_symlink() {
|
} else if file_type.is_symlink() {
|
||||||
"l".to_string()
|
'l'
|
||||||
} else {
|
} else {
|
||||||
"-".to_string()
|
'-'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue