Initial commit.

Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
This commit is contained in:
Edu4rdSHL 2021-06-09 19:38:12 -05:00
parent f9c5631d6d
commit 23f888270e
No known key found for this signature in database
GPG key ID: 3A574A4009F553E5
14 changed files with 1511 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
*.txt

19
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceRoot}/target/debug/${workspaceRootFolderName}",
"args": "-f slack3",
"cwd": "${workspaceRoot}",
"sourceLanguages": [
"rust"
]
}
]
}

15
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cargo build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
}
]
}

1206
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "webtail-rs"
version = "0.1.0"
authors = ["Edu4rdSHL <edu4rdshl@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.4.10", features = ["sse"] }
rocket_contrib = "0.4.7"
clap = { version = "2.33.3", features = ["yaml"] }
rayon = "1.5.1"

2
examples/webtail.conf Normal file
View file

@ -0,0 +1,2 @@
file1.txt:8080
file2.txt:9090

18
src/args.rs Normal file
View file

@ -0,0 +1,18 @@
use {
crate::structs::Args,
clap::{load_yaml, value_t, App},
};
#[allow(clippy::cognitive_complexity)]
pub fn get_args() -> Args {
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml)
.version(clap::crate_version!())
.get_matches();
Args {
file_name: value_t!(matches.value_of("file"), String).unwrap_or_default(),
delay: value_t!(matches.value_of("delay"), u64).unwrap_or(1),
port: value_t!(matches.value_of("port"), u16).unwrap_or(8080),
config_file: value_t!(matches.value_of("config"), String).unwrap_or_default(),
}
}

34
src/cli.yml Normal file
View file

@ -0,0 +1,34 @@
name: webtail-rs
author: Eduard Tolosa <edu4rdshl@protonmail.com>
settings:
- ArgRequiredElseHelp
- StrictUtf8
about: tail -f for your browser.
args:
- file:
short: f
long: file
help: File to follow.
takes_value: true
multiple: false
- delay:
short: d
long: delay
help: Time between reads.
takes_value: true
multiple: false
- port:
short: p
long: port
help: Port to use.
takes_value: true
multiple: false
- config:
short: c
long: config
help: Config file to use. You can use the path:port syntax for viewing multiple files.
takes_value: true
multiple: false

6
src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
#![feature(proc_macro_hygiene, decl_macro)]
pub mod args;
pub mod structs;
pub mod utils;
pub mod worker;

58
src/main.rs Normal file
View file

@ -0,0 +1,58 @@
use std::{collections::HashMap, u16};
use webtail_rs::{
structs::{File, State},
utils,
};
use {
rayon::prelude::*,
rocket::{
config::{Config, Environment, LoggingLevel},
routes,
},
rocket_contrib::serve::StaticFiles,
webtail_rs::{args, worker::*},
};
fn main() {
let args = args::get_args();
if args.file_name.is_empty() && args.config_file.is_empty() {
eprintln!("No files to process.");
std::process::exit(1)
}
let mut input_files: HashMap<String, u16> = HashMap::new();
if !args.config_file.is_empty() {
let buffer = utils::return_bufreader(&args.config_file);
input_files = utils::read_config_file(buffer);
}
if !args.file_name.is_empty() {
input_files.insert(args.file_name.clone(), args.port);
}
input_files
.par_iter()
.map(|(file, port)| {
println!("=> Watching file {} in port {}\n", file, port);
let error = rocket::custom(
Config::build(Environment::Production)
.log_level(LoggingLevel::Off)
.port(port.to_owned())
.unwrap(),
)
.manage(File {
data: String::new(),
name: file.clone(),
state: State::Write,
delay: args.delay,
})
.mount("/", routes![updates])
.mount("/", StaticFiles::from("static"))
.launch();
eprintln!("Launch failed! Error: {}", error);
})
.collect::<Vec<()>>();
}

21
src/structs.rs Normal file
View file

@ -0,0 +1,21 @@
#[derive(Clone)]
pub struct Args {
pub file_name: String,
pub delay: u64,
pub port: u16,
pub config_file: String,
}
pub struct File {
pub data: String,
pub state: State,
pub name: String,
pub delay: u64,
}
#[derive(Clone, Copy)]
pub enum State {
Flush,
Sleep,
Write,
}

32
src/utils.rs Normal file
View file

@ -0,0 +1,32 @@
use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader},
};
pub fn return_bufreader(file: &str) -> BufReader<File> {
match std::fs::File::open(file) {
Ok(file) => BufReader::new(file),
Err(e) => {
eprintln!("Error reading the file: {}. Error: {}", file, e);
std::process::exit(1)
}
}
}
pub fn read_config_file(buffer: BufReader<File>) -> HashMap<String, u16> {
let mut data: HashMap<String, u16> = HashMap::new();
for line in buffer.lines() {
let line_str = line.expect("Error");
let data_vec = line_str.split(':').collect::<Vec<&str>>();
if data_vec.len() == 2 {
data.insert(
data_vec.get(0).unwrap().to_string(),
data_vec.get(1).unwrap().parse().unwrap_or_else(|_| {
panic!("Error reading the port for the line {:?}", data_vec)
}),
);
}
}
data
}

61
src/worker.rs Normal file
View file

@ -0,0 +1,61 @@
use rocket::response::{Content, Stream};
use {
crate::{
structs::{File, State},
utils,
},
rocket::{get, http::ContentType},
std::{
io::{self, prelude::*, BufReader, Read, Write},
time::Duration,
},
};
const BUF_SIZE: usize = 52428800;
impl Read for File {
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
match self.state {
State::Flush => {
self.state = State::Sleep;
return Err(io::ErrorKind::WouldBlock.into());
}
State::Sleep => std::thread::sleep(Duration::from_secs(self.delay)),
State::Write => {}
}
self.state = State::Flush;
self.data.clear();
self.data += &format!("data: Watching file {}\ndata: \n", self.name);
for line in utils::return_bufreader(&self.name).lines() {
self.data += &format!("data: {}\n", line.expect("Error"))
}
self.data += "\n\n";
buf.write_all(self.data.as_bytes())?;
Ok(self.data.len())
}
}
type CounterStream = Stream<BufReader<File>>;
#[get("/updates")]
pub fn updates(file_data: rocket::State<File>) -> Content<CounterStream> {
let reader = BufReader::with_capacity(
BUF_SIZE,
File {
data: file_data.data.clone(),
state: file_data.state,
name: file_data.name.clone(),
delay: file_data.delay,
},
);
let ct = ContentType::with_params("text", "event-stream", ("charset", "utf-8"));
Content(ct, Stream::from(reader))
}

24
static/index.html Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Webtail RS</title>
</head>
<body>
<div id="spong">Loading...</div>
</body>
<script charset="utf-8">
status_node = document.getElementById('spong');
status_node.innerText = 'Waiting...';
es = new EventSource("updates");
es.onmessage = function (event) {
console.log(event);
status_node.innerText = event.data;
};
</script>
</html>