Split server into multiple files

This commit is contained in:
Arlo Filley 2023-01-11 15:25:52 +00:00
parent ed6830b39a
commit 6dc7d6a033
49 changed files with 764 additions and 125 deletions

View File

@ -5,7 +5,7 @@ workers = 4
max_blocking = 512 max_blocking = 512
keep_alive = 5 keep_alive = 5
ident = "Rocket" ident = "Rocket"
log_level = "normal" log_level = "critical"
temp_dir = "/tmp" temp_dir = "/tmp"
cli_colors = true cli_colors = true
## NOTE: Don't (!) use this key! Generate your own! ## NOTE: Don't (!) use this key! Generate your own!

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
server/SUPERPC.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
server/arch-server.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -15,17 +15,28 @@ use rocket::{
FileServer, FileServer,
relative relative
}, },
serde::{
Deserialize,
json::Json
}
}; };
use rand::Rng;
use std::{fs, vec}; mod typing;
mod servers;
use crate::typing::sql::*;
use crate::typing::user::{
get_tests,
login,
sign_up
};
use crate::typing::test::{
create_test,
new_test
};
use crate::typing::leaderboard::leaderboard;
use crate::servers::server::{
server,
server_info
};
// Imports for sql, see sql.rs for more information // Imports for sql, see sql.rs for more information
pub mod sql;
use crate::sql::*;
/// Test api route that returns hello world. /// Test api route that returns hello world.
/// Acessible from http://url/test /// Acessible from http://url/test
@ -34,118 +45,8 @@ fn test() -> String {
String::from("Hello World!") String::from("Hello World!")
} }
/// Api route that creates a database if one
/// does not already exist.
/// Acessible from http://url/api/create_database
#[get("/create_database")]
fn create_database() -> String {
sql::create_database()
.expect("couldn't create database");
String::from("Successfully created a database")
}
/// the datascructure that the webserver will recieve
/// when a post is made to the http://url/api/post_test route
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct PostTest<'r> {
test_type: &'r str,
test_length: u32,
test_time: u32,
test_seed: i64,
quote_id: i32,
wpm: u8,
accuracy: u8,
user_id: u32
}
/// Api Route that accepts test data and posts it to the database
/// Acessible from http://url/api/post_test
#[post("/post_test", data = "<test>")]
fn post_test(
test: Json<PostTest<'_>>
) {
sql::post_test(
test.test_type,
test.test_length,
test.test_time,
test.test_seed,
test.quote_id,
test.wpm,
test.accuracy,
test.user_id
).expect("error in posting test to tests table");
}
/// Struct representing the user
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct User<'r> {
username: &'r str,
password: &'r str
}
/// Route takes data about the user as a struct
/// and then creates the user in the database
/// Acessible from http://url/api/create_user
#[post("/create_user", data = "<user>")]
fn create_user(
user: Json<User<'_>>
) {
sql::create_user(
user.username,
user.password
).expect("Error: Couldn't create new user");
}
/// takes the users login information and returns the users user id
/// which can be used to identify their tests etc.
/// Accessible from http://url/api/login
#[get("/login/<username>/<password>")]
fn login(username: &str, password: &str) -> String {
let user_id = sql::find_user(username, password).expect("error finding user_id");
user_id.to_string()
}
/// Gets the users tests from the database and returns it as a
/// json array.
/// Accessible from http://url/api/get_user_tests
#[get("/get_user_tests/<user_id>")]
fn get_user_tests(user_id: u32) -> Json<Vec<Test>> {
let tests = sql::get_user_tests(user_id).expect("error finding user_id");
Json(tests)
}
/// Returns the highest test data from each user as
/// a json array
/// Acessible from http://url/api/leaderboard
#[get("/leaderboard")]
fn leaderboard() -> Json<Vec<LeaderBoardTest>> {
let leaderboard = sql::get_leaderboard(0).expect("error finding user_id");
Json(leaderboard)
}
/// Returns an array of words as Json
/// Accessible from http://url/api/get_test
#[get("/get_test")]
fn get_test() -> Json<Vec<String>> {
let mut word_vec: Vec<&str> = vec![];
let words = fs::read_to_string("wordlist.txt").unwrap();
for word in words.split('\n') {
word_vec.push(word.clone());
}
let mut return_list: Vec<String> = vec![];
let mut rng = rand::thread_rng();
for _i in 0..100 {
let hi: u32 = rng.gen_range(0..999);
return_list.push(word_vec[hi as usize].to_string())
}
Json(return_list.clone())
}
/// The main function which builds and launches the /// The main function which builds and launches the
/// webserver with all appropriate routes and fileservers /// webserver with all appropriate routes and fileservers
@ -157,10 +58,15 @@ fn rocket() -> Rocket<Build> {
// hosts the api routes necessary for the website // hosts the api routes necessary for the website
// to interact with the database // to interact with the database
.mount("/api", routes![ .mount("/api", routes![
create_database, create_user, create_database, sign_up,
post_test, login, get_user_tests, create_test, login, get_tests,
leaderboard, get_test leaderboard, new_test,
])
.mount("/api", routes![
server, server_info
]) ])
// hosts the fileserver // hosts the fileserver
.mount("/typing", FileServer::from(relative!("website"))) .mount("/typing", FileServer::from(relative!("websites/Typing")))
.mount("/servers", FileServer::from(relative!("websites/Servers")))
} }

1
src/servers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod server;

155
src/servers/server.rs Normal file
View File

@ -0,0 +1,155 @@
use rocket::serde::{
json::{
Json,
to_string, from_str
},
Deserialize,
Serialize
};
use std::{
io::{
Write,
Read
},
fs::{
File,
read_dir,
},
};
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
struct Process {
name: String,
memory: String,
run_time: String,
id: String,
user_id: String,
virtual_memory: String
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
struct Disk {
name: String,
disk_type: String,
total_space: String,
available_space: String,
usage: String,
file_system: String
}
#[derive(Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct System {
key: String,
host_name: String,
uptime: String,
os: String,
total_ram: String,
used_ram: String,
available_ram: String,
ram_usage: String,
total_swap: String,
used_swap: String,
available_swap: String,
swap_usage: String,
disks: Vec<Disk>,
processes: Vec<Process>,
}
#[post("/servers", data = "<data>")]
pub fn server(data: Json<System>) {
if data.key == "0etnmXPSr95@FNy6A3U9Bw*ZupNIR85zI!hRFGIdj6SW$Tu0q%" {
println!("Data From : {}", data.host_name);
}
let mut file = match File::create(format!("./server/{}.json", &data.host_name)) {
Err(why) => {
println!("Error: {why}");
return;
}
Ok(file) => file,
};
let data: System = System {
key: format!("null"),
host_name: format!("{}", data.host_name),
uptime: format!("{}", data.uptime),
os: format!("{}", data.os),
total_ram: format!("{}", data.total_ram),
used_ram: format!("{}", data.used_ram),
available_ram: format!("{}", data.available_ram),
ram_usage: format!("{}", data.ram_usage),
total_swap: format!("{}", data.total_swap),
used_swap: format!("{}", data.used_swap),
available_swap: format!("{}", data.available_swap),
swap_usage: format!("{}", data.swap_usage),
disks: data.disks.clone(),
processes: data.processes.clone(),
};
let string = match to_string(&data) {
Err(why) => {
println!("Error: {why}");
return;
}
Ok(string) => string,
};
let write = file.write_all(
string
.as_bytes()
);
match write {
Err(why) => println!("Error {why}"),
Ok(_) => (),
}
}
#[get("/server_info")]
pub fn server_info() -> Json<Vec<System>> {
let mut systems: Vec<System> = vec![];
let folder = match read_dir("./server/") {
Err(why) => {
println!("Error: {why}");
read_dir("./server/").unwrap()
},
Ok(dir) => dir,
};
let mut file: File;
let mut string;
for path in folder {
string = String::new();
let path = match path {
Err(ref why) => {
println!("Error: {why}");
path.unwrap()
},
Ok(path) => path
}.path();
file = match File::open(format!("{}", path.display())) {
Err(why) => {
println!("Error: {why}");
File::open(format!("{}", path.display())).unwrap()
}
Ok(file) => file,
};
match file.read_to_string(&mut string) {
Err(why) => println!("Error: {why}"),
Ok(_) => (),
};
match from_str(&string) {
Err(why) => println!("Error: {why}"),
Ok(string) => systems.push(string),
};
}
Json(systems)
}

14
src/typing/leaderboard.rs Normal file
View File

@ -0,0 +1,14 @@
use rocket::serde::json::Json;
use crate::typing::sql::{
get_leaderboard,
LeaderBoardTest
};
/// Returns the highest test data from each user as
/// a json array
/// Acessible from http://url/api/leaderboard
#[get("/leaderboard")]
pub fn leaderboard() -> Json<Vec<LeaderBoardTest>> {
let leaderboard: Vec<LeaderBoardTest> = get_leaderboard(0).expect("error finding user_id");
Json(leaderboard)
}

4
src/typing/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod sql;
pub mod user;
pub mod leaderboard;
pub mod test;

View File

@ -23,7 +23,7 @@ fn get_connection() -> rusqlite::Connection {
/// Creates the necessary tables inside the database with /// Creates the necessary tables inside the database with
/// correct normalised links between data for later /// correct normalised links between data for later
/// querying /// querying
pub fn create_database() -> Result<()> { fn new_database() -> Result<()> {
let connection = get_connection(); let connection = get_connection();
connection.execute( connection.execute(
@ -54,6 +54,18 @@ pub fn create_database() -> Result<()> {
Ok(()) Ok(())
} }
/// Api route that creates a database if one
/// does not already exist.
/// Acessible from http://url/api/create_database
#[get("/create_database")]
pub fn create_database() -> String {
let database = new_database();
match database {
Err(why) => format!("Error: {why}"),
Ok(_) => format!("Sucessfully created the database")
}
}
/// takes necessary data about a test and creates /// takes necessary data about a test and creates
/// a database record with the data /// a database record with the data
pub fn post_test( pub fn post_test(

65
src/typing/test.rs Normal file
View File

@ -0,0 +1,65 @@
use crate::typing::sql::post_test;
use rocket::serde::{
Deserialize,
json::Json
};
use rand::{
Rng,
rngs::ThreadRng
};
use std::{
fs,
vec,
};
/// the datascructure that the webserver will recieve
/// when a post is made to the http://url/api/post_test route
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct PostTest<'r> {
test_type: &'r str,
test_length: u32,
test_time: u32,
test_seed: i64,
quote_id: i32,
wpm: u8,
accuracy: u8,
user_id: u32
}
/// Api Route that accepts test data and posts it to the database
/// Acessible from http://url/api/post_test
#[post("/post_test", data = "<test>")]
pub fn create_test(test: Json<PostTest<'_>>) {
post_test(
test.test_type,
test.test_length,
test.test_time,
test.test_seed,
test.quote_id,
test.wpm,
test.accuracy,
test.user_id
).expect("error in posting test to tests table");
}
/// Returns an array of words as Json
/// Accessible from http://url/api/get_test
#[get("/new_test")]
pub fn new_test() -> Json<Vec<String>> {
let mut word_vec: Vec<&str> = vec![];
let words: String = fs::read_to_string("wordlist.txt").unwrap();
for word in words.split('\n') {
word_vec.push(word.clone());
}
let mut return_list: Vec<String> = vec![];
let mut rng: ThreadRng = rand::thread_rng();
for _i in 0..100 {
let hi: u32 = rng.gen_range(0..999);
return_list.push(word_vec[hi as usize].to_string())
}
Json(return_list.clone())
}

50
src/typing/user.rs Normal file
View File

@ -0,0 +1,50 @@
use rocket::serde::{
Deserialize,
json::Json
};
use crate::typing::sql::{
get_user_tests,
find_user,
create_user,
Test,
};
/// Struct representing the user
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct User<'r> {
username: &'r str,
password: &'r str
}
/// Route takes data about the user as a struct
/// and then creates the user in the database
/// Acessible from http://url/api/create_user
#[post("/create_user", data = "<user>")]
pub fn sign_up(user: Json<User<'_>>) {
create_user(
user.username,
user.password
).expect("Error: Couldn't create new user");
}
/// Gets the users tests from the database and returns it as a
/// json array.
/// Accessible from http://url/api/get_user_tests
#[get("/get_tests/<user_id>")]
pub fn get_tests(user_id: u32) -> Json<Vec<Test>> {
let tests: Vec<Test> = get_user_tests(user_id).expect("error finding user_id");
Json(tests)
}
/// takes the users login information and returns the users user id
/// which can be used to identify their tests etc.
/// Accessible from http://url/api/login
#[get("/login/<username>/<password>")]
pub fn login(username: &str, password: &str) -> String {
let user_id = find_user(username, password).expect("error finding user_id");
user_id.to_string()
}

BIN
website/.DS_Store vendored

Binary file not shown.

100
websites/Servers/index.css Normal file
View File

@ -0,0 +1,100 @@
:root {
--navbar: #52b2cf;
--navbar-button: #283F3B;
--background: #7EC4CF;
--table: #FFD07B;
--table-top:#FDB833;
--title: #296EB4;
--text: #000000;
--border: #000000;
}
body {
background: var(--background);
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: medium;
margin: 0px;
}
div {
display: block;
}
::-webkit-scrollbar {
display: none;
}
h1 {
color:black;
text-align: center;
width: 100%;
margin: 0px;
}
#all, #systems {
display: inline;
}
#navbar {
width: 100%;
background: var(--navbar);
border: 1px black solid;
padding: 5px;
}
#navbar button {
background-color: var(--navbar-button);
color: wheat;
border: 3px black solid;
padding: 15px;
font-size: larger;
}
#navbar button:focus {
background-color: #23296c;
}
#navbar button:hover {
background-color: #fc9e4f;
color: var(--text);
border: 3px var(--border) solid;
}
#SystemDiv {
background: var(--title);
width: 100%;
font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 50px;
padding-bottom: 20px;
border-bottom: black 1px solid;
}
#system {
margin: 0px;
}
table {
border: 2px black solid;
margin: 0px;
width: 100%;
padding: 0px;
}
table tr:nth-child(n) {
background: var(--table);
}
table tr:nth-child(1) {
background: var(--table-top);
}
tr {
border: 1px black solid;
}
td {
border: 1px black solid;
text-align: center;
padding: 10px;
font-size: larger;
}

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="title">Leaderboard</title>
<link rel="stylesheet" href="index.css">
<script src="index.js" defer="true"></script>
</head>
<body>
<div id="SystemDiv"><h1 id="system">Typing Leaderboard</h1></div>
<div id="navbar">
<div id="allButton">
<button id="all">all</button>
</div>
<div id="systems">
<button>Arch</button>
</div>
</div>
<div id="navbar">
<button id="systeminfo">System</button>
<button id="Disks Header">Disks</button>
<button id="Processes Header">Processes</button>
</div>
<div id="systemInfo" align="center"></div>
<div id="disks" align="center"></div>
<div id="processes" align="center"></div>
</body>
</html>

296
websites/Servers/index.js Normal file
View File

@ -0,0 +1,296 @@
get();
let disks = false;
let processes = false;
let sysinfo = true;
let json;
let server = 0;
setInterval(get, 5000);
function get() {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://arlofilley.com/api/server_info');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.send();
xhr.onload = () => {
json = xhr.response
createElements(json[server]);
createButtons(json);
};
};
function createElements(pJson) {
let title = document.getElementById(`system`);
title.textContent = pJson.host_name;
document.title = pJson.host_name;
createProcesses(pJson);
createDisks(pJson);
createSystemInfo(pJson);
};
function createSystemInfo(pJson) {
let div = document.getElementById(`systemInfo`);
while (div.firstChild) {
div.removeChild(div.firstChild)
}
if (!sysinfo) return;
let table = document.createElement(`table`);
let tableBody = document.createElement('tbody');
table.appendChild(tableBody);
let tr = document.createElement('tr');
tableBody.appendChild(tr);
let td = document.createElement('td');
td.appendChild(document.createTextNode(`Name`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`OS`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Uptime`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Total Ram`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Used Ram`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Available Ram`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Total Swap`));
tr.appendChild(td);
tableBody.appendChild(tr);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Used Swap`));
tr.appendChild(td);
tableBody.appendChild(tr);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Available Swap`));
tr.appendChild(td);
tableBody.appendChild(tr);
tr = document.createElement('tr');
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.host_name));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.os));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.uptime));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.total_ram));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.used_ram));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.available_ram));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.total_swap));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.used_swap));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(pJson.available_swap));
tr.appendChild(td);
tableBody.appendChild(tr);
tr = document.createElement('tr');
div.appendChild(table);
};
function createProcesses(pJson) {
let div = document.getElementById(`processes`);
while (div.firstChild) {
div.removeChild(div.firstChild)
}
if (!processes) return;
let table = document.createElement(`table`);
let tableBody = document.createElement('tbody');
table.appendChild(tableBody);
let tr = document.createElement('tr');
tableBody.appendChild(tr);
let td = document.createElement('td');
td.appendChild(document.createTextNode(`Name`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Memory`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Run Time`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Process ID`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`User ID`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Virtual Memory`));
tr.appendChild(td);
tableBody.appendChild(tr);
tr = document.createElement('tr');
for (let i = 0; i < pJson.processes.length; i++) {
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.processes[i].name}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.processes[i].memory}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.processes[i].run_time}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.processes[i].id}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.processes[i].user_id}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.processes[i].virtual_memory}`));
tr.appendChild(td);
tableBody.appendChild(tr);
tr = document.createElement('tr');
}
div.appendChild(table);
};
function createDisks(pJson) {
let div = document.getElementById(`disks`);
while (div.firstChild) {
div.removeChild(div.firstChild)
}
if (!disks) return;
let table = document.createElement(`table`);
let tableBody = document.createElement('tbody');
table.appendChild(tableBody);
let tr = document.createElement('tr');
tableBody.appendChild(tr);
let td = document.createElement('td');
td.appendChild(document.createTextNode(`Name`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Type`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Total Space`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Available Space`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`Usage`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`File System`));
tr.appendChild(td);
tableBody.appendChild(tr);
tr = document.createElement('tr');
for (let i = 0; i < pJson.disks.length; i++) {
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.disks[i].name}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.disks[i].disk_type}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.disks[i].total_space}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.disks[i].available_space}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.disks[i].usage}`));
tr.appendChild(td);
td = document.createElement('td');
td.appendChild(document.createTextNode(`${pJson.disks[i].file_system}`));
tr.appendChild(td);
tableBody.appendChild(tr);
tr = document.createElement('tr');
}
div.appendChild(table);
};
b = document.getElementById("Processes Header");
b.addEventListener("click", () => {
processes = true;
disks = false;
sysinfo = false;
createElements(json[server]);
});
c = document.getElementById("systeminfo");
c.addEventListener("click", () => {
processes = false;
disks = false;
sysinfo = true;
createElements(json[server]);
});
let button = document.getElementById
e = document.getElementById("Disks Header");
e.addEventListener("click", () => {
disks = true;
processes = false;
sysinfo = false;
createElements(json[server]);
});
function createButtons(pJson) {
let div = document.getElementById("systems");
while (div.firstChild) {
div.removeChild(div.firstChild)
}
for (let i = 0; i < pJson.length; i++) {
let system = pJson[i];
let button = document.createElement("button");
button.textContent = system.host_name.toUpperCase();
button.id = i;
button.addEventListener("click", () => {
disks = false;
processes = false;
sysinfo = true;
server = button.id
createElements(json[button.id]);
});
div.appendChild(button);
}
}

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 552 B

View File

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 968 B

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB