Getting ready for 1.2.0 release: many smaller requests implemented
- Implements #7 - Implements #42 and therefore #64 - Improved #53 - Implements #59 - Implements #61 - Implements #63 - Implements #80 - Implements #84 - Added Info page - Removed Help page - Bumped version number to 1.2.0 - Fixed a bug where wide mode was still 720px wide - Created FUNDING.yml - Reorganised arguments in README.MD and documented new options - Updated SECURITY.MD - Added display of last read time and read count - Increased default width to 800px to make UI less cluttered - Reorganised index page - New, better attach file button I want to spend some time testing these changes and let everyone have a look at them before tagging and releasing new artifacts.
This commit is contained in:
parent
769901c895
commit
44b55ae08e
22 changed files with 977 additions and 327 deletions
21
src/args.rs
21
src/args.rs
|
@ -63,11 +63,32 @@ pub struct Args {
|
|||
#[clap(short, long, env = "MICROBIN_THREADS", default_value_t = 1)]
|
||||
pub threads: u8,
|
||||
|
||||
#[clap(short, long, env = "MICROBIN_GC_DAYS", default_value_t = 90)]
|
||||
pub gc_days: u16,
|
||||
|
||||
#[clap(long, env = "MICROBIN_ENABLE_BURN_AFTER")]
|
||||
pub enable_burn_after: bool,
|
||||
|
||||
#[clap(short, long, env = "MICROBIN_DEFAULT_BURN_AFTER", default_value_t = 0)]
|
||||
pub default_burn_after: u16,
|
||||
|
||||
#[clap(long, env = "MICROBIN_WIDE")]
|
||||
pub wide: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_QR")]
|
||||
pub qr: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_NO_ETERNAL_PASTA")]
|
||||
pub no_eternal_pasta: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_DEFAULT_EXPIRY", default_value = "24hour")]
|
||||
pub default_expiry: String,
|
||||
|
||||
#[clap(short, long, env = "MICROBIN_NO_FILE_UPLOAD")]
|
||||
pub no_file_upload: bool,
|
||||
|
||||
#[clap(long, env = "MICROBIN_CUSTOM_CSS")]
|
||||
pub custom_css: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -54,6 +54,9 @@ pub async fn create(
|
|||
private: false,
|
||||
editable: false,
|
||||
created: timenow,
|
||||
read_count: 0,
|
||||
burn_after_reads: 0,
|
||||
last_read: timenow,
|
||||
pasta_type: String::from(""),
|
||||
expiration: 0,
|
||||
};
|
||||
|
@ -78,9 +81,34 @@ pub async fn create(
|
|||
"1hour" => timenow + 60 * 60,
|
||||
"24hour" => timenow + 60 * 60 * 24,
|
||||
"1week" => timenow + 60 * 60 * 24 * 7,
|
||||
"never" => 0,
|
||||
"never" => {
|
||||
if ARGS.no_eternal_pasta {
|
||||
timenow + 60 * 60 * 24 * 7
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("{}", "Unexpected expiration time!");
|
||||
timenow + 60 * 60 * 24 * 7
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
"burn_after" => {
|
||||
while let Some(chunk) = field.try_next().await? {
|
||||
new_pasta.burn_after_reads = match std::str::from_utf8(&chunk).unwrap() {
|
||||
// give an extra read because the user will be redirected to the pasta page automatically
|
||||
"1" => 2,
|
||||
"10" => 10,
|
||||
"100" => 100,
|
||||
"1000" => 1000,
|
||||
"10000" => 10000,
|
||||
"0" => 0,
|
||||
_ => {
|
||||
log::error!("{}", "Unexpected burn after value!");
|
||||
0
|
||||
}
|
||||
};
|
||||
|
@ -126,8 +154,11 @@ pub async fn create(
|
|||
}
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(format!("./pasta_data/public/{}", &new_pasta.id_as_animals()))
|
||||
.unwrap();
|
||||
std::fs::create_dir_all(format!(
|
||||
"./pasta_data/public/{}",
|
||||
&new_pasta.id_as_animals()
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let filepath = format!(
|
||||
"./pasta_data/public/{}/{}",
|
||||
|
@ -158,6 +189,9 @@ pub async fn create(
|
|||
save_to_file(&pastas);
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header(("Location", format!("{}/pasta/{}", ARGS.public_path, to_animal_names(id))))
|
||||
.append_header((
|
||||
"Location",
|
||||
format!("{}/pasta/{}", ARGS.public_path, to_animal_names(id)),
|
||||
))
|
||||
.finish())
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
use crate::args::{Args, ARGS};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use askama::Template;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "help.html")]
|
||||
struct Help<'a> {
|
||||
args: &'a Args,
|
||||
_marker: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
#[get("/help")]
|
||||
pub async fn help() -> HttpResponse {
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
Help {
|
||||
args: &ARGS,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
42
src/endpoints/info.rs
Normal file
42
src/endpoints/info.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use crate::args::{Args, ARGS};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::AppState;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "info.html")]
|
||||
struct Info<'a> {
|
||||
args: &'a Args,
|
||||
pastas: &'a Vec<Pasta>,
|
||||
status: &'a String,
|
||||
version_string: &'a String,
|
||||
message: &'a String,
|
||||
}
|
||||
|
||||
#[get("/info")]
|
||||
pub async fn info(data: web::Data<AppState>) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
// todo status report more sophisticated
|
||||
let mut status = "OK";
|
||||
let mut message = "";
|
||||
|
||||
if ARGS.public_path.to_string() == "" {
|
||||
status = "WARNING";
|
||||
message = "Warning: No public URL set with --public-path parameter. QR code and URL Copying functions have been disabled"
|
||||
}
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
Info {
|
||||
args: &ARGS,
|
||||
pastas: &pastas,
|
||||
status: &String::from(status),
|
||||
version_string: &String::from("1.2.0-20221029"),
|
||||
message: &String::from(message),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
use crate::args::{Args, ARGS};
|
||||
use crate::dbio::save_to_file;
|
||||
use crate::endpoints::errors::ErrorTemplate;
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::animalnumbers::to_u64;
|
||||
use crate::util::misc::remove_expired;
|
||||
use crate::AppState;
|
||||
use actix_web::rt::time;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pasta.html", escape = "none")]
|
||||
|
@ -17,27 +19,60 @@ struct PastaTemplate<'a> {
|
|||
|
||||
#[get("/pasta/{id}")]
|
||||
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
// get the u64 id from the animal names in the path
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
println!("{}", id);
|
||||
|
||||
// remove expired pastas (including this one if needed)
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
// find the index of the pasta in the collection based on u64 id
|
||||
let mut index: usize = 0;
|
||||
let mut found: bool = false;
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
return HttpResponse::Ok().content_type("text/html").body(
|
||||
PastaTemplate {
|
||||
pasta: &pasta,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
// increment read count
|
||||
pastas[index].read_count = pastas[index].read_count + 1;
|
||||
|
||||
// save the updated read count
|
||||
save_to_file(&pastas);
|
||||
|
||||
// serve pasta in template
|
||||
let response = HttpResponse::Ok().content_type("text/html").body(
|
||||
PastaTemplate {
|
||||
pasta: &pastas[index],
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// get current unix time in seconds
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
// update last read time
|
||||
pastas[index].last_read = timenow;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// otherwise
|
||||
// send pasta not found error
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
|
@ -45,26 +80,62 @@ pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
|
|||
|
||||
#[get("/url/{id}")]
|
||||
pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
// get the u64 id from the animal names in the path
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
// remove expired pastas (including this one if needed)
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
// find the index of the pasta in the collection based on u64 id
|
||||
let mut index: usize = 0;
|
||||
let mut found: bool = false;
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
if pasta.pasta_type == "url" {
|
||||
return HttpResponse::Found()
|
||||
.append_header(("Location", String::from(&pasta.content)))
|
||||
.finish();
|
||||
} else {
|
||||
return HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
|
||||
}
|
||||
index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
// increment read count
|
||||
pastas[index].read_count = pastas[index].read_count + 1;
|
||||
|
||||
// save the updated read count
|
||||
save_to_file(&pastas);
|
||||
|
||||
// send redirect if it's a url pasta
|
||||
if pastas[index].pasta_type == "url" {
|
||||
let response = HttpResponse::Found()
|
||||
.append_header(("Location", String::from(&pastas[index].content)))
|
||||
.finish();
|
||||
|
||||
// get current unix time in seconds
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
// update last read time
|
||||
pastas[index].last_read = timenow;
|
||||
|
||||
return response;
|
||||
// send error if we're trying to open a non-url pasta as a redirect
|
||||
} else {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise
|
||||
// send pasta not found error
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
|
||||
|
@ -72,17 +143,50 @@ pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> Ht
|
|||
|
||||
#[get("/raw/{id}")]
|
||||
pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
|
||||
// get access to the pasta collection
|
||||
let mut pastas = data.pastas.lock().unwrap();
|
||||
|
||||
// get the u64 id from the animal names in the path
|
||||
let id = to_u64(&*id.into_inner()).unwrap_or(0);
|
||||
|
||||
// remove expired pastas (including this one if needed)
|
||||
remove_expired(&mut pastas);
|
||||
|
||||
for pasta in pastas.iter() {
|
||||
// find the index of the pasta in the collection based on u64 id
|
||||
let mut index: usize = 0;
|
||||
let mut found: bool = false;
|
||||
for (i, pasta) in pastas.iter().enumerate() {
|
||||
if pasta.id == id {
|
||||
return pasta.content.to_owned();
|
||||
index = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
// increment read count
|
||||
pastas[index].read_count = pastas[index].read_count + 1;
|
||||
|
||||
// get current unix time in seconds
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
// update last read time
|
||||
pastas[index].last_read = timenow;
|
||||
|
||||
// save the updated read count
|
||||
save_to_file(&pastas);
|
||||
|
||||
// send raw content of pasta
|
||||
return pastas[index].content.to_owned();
|
||||
}
|
||||
|
||||
// otherwise
|
||||
// send pasta not found error as raw text
|
||||
String::from("Pasta not found! :-(")
|
||||
}
|
||||
|
|
34
src/endpoints/qr.rs
Normal file
34
src/endpoints/qr.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use crate::args::{Args, ARGS};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::misc::{self, remove_expired};
|
||||
use crate::AppState;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use askama::Template;
|
||||
use qrcode_generator::QrCodeEcc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "qr.html", escape = "none")]
|
||||
struct QRTemplate<'a> {
|
||||
qr: &'a String,
|
||||
args: &'a Args,
|
||||
}
|
||||
|
||||
#[get("/qr/{id}")]
|
||||
pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
|
||||
// find the index of the pasta in the collection based on u64 id
|
||||
|
||||
let svg: String = misc::string_to_qr_svg(
|
||||
format!("{}/pasta/{}", &ARGS.public_path, &*id.into_inner()).as_str(),
|
||||
);
|
||||
|
||||
// serve qr code in template
|
||||
HttpResponse::Ok().content_type("text/html").body(
|
||||
QRTemplate {
|
||||
qr: &svg,
|
||||
args: &ARGS,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
18
src/main.rs
18
src/main.rs
|
@ -2,7 +2,7 @@ extern crate core;
|
|||
|
||||
use crate::args::ARGS;
|
||||
use crate::endpoints::{
|
||||
create, edit, errors, help, pasta as pasta_endpoint, pastalist, remove, static_resources,
|
||||
create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, remove, static_resources,
|
||||
};
|
||||
use crate::pasta::Pasta;
|
||||
use crate::util::dbio;
|
||||
|
@ -31,9 +31,10 @@ pub mod endpoints {
|
|||
pub mod create;
|
||||
pub mod edit;
|
||||
pub mod errors;
|
||||
pub mod help;
|
||||
pub mod info;
|
||||
pub mod pasta;
|
||||
pub mod pastalist;
|
||||
pub mod qr;
|
||||
pub mod remove;
|
||||
pub mod static_resources;
|
||||
}
|
||||
|
@ -66,8 +67,14 @@ async fn main() -> std::io::Result<()> {
|
|||
match fs::create_dir_all("./pasta_data/public") {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
log::error!("Couldn't create data directory ./pasta_data/public/: {:?}", error);
|
||||
panic!("Couldn't create data directory ./pasta_data/public/: {:?}", error);
|
||||
log::error!(
|
||||
"Couldn't create data directory ./pasta_data/public/: {:?}",
|
||||
error
|
||||
);
|
||||
panic!(
|
||||
"Couldn't create data directory ./pasta_data/public/: {:?}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -80,13 +87,14 @@ async fn main() -> std::io::Result<()> {
|
|||
.app_data(data.clone())
|
||||
.wrap(middleware::NormalizePath::trim())
|
||||
.service(create::index)
|
||||
.service(help::help)
|
||||
.service(info::info)
|
||||
.service(pasta_endpoint::getpasta)
|
||||
.service(pasta_endpoint::getrawpasta)
|
||||
.service(pasta_endpoint::redirecturl)
|
||||
.service(edit::get_edit)
|
||||
.service(edit::post_edit)
|
||||
.service(static_resources::static_resources)
|
||||
.service(qr::getqr)
|
||||
.service(actix_files::Files::new("/file", "./pasta_data/public/"))
|
||||
.service(web::resource("/upload").route(web::post().to(create::create)))
|
||||
.default_service(web::route().to(errors::not_found))
|
||||
|
|
56
src/pasta.rs
56
src/pasta.rs
|
@ -3,6 +3,7 @@ use chrono::{Datelike, Local, TimeZone, Timelike};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::util::animalnumbers::to_animal_names;
|
||||
use crate::util::syntaxhighlighter::html_highlight;
|
||||
|
@ -39,6 +40,9 @@ pub struct Pasta {
|
|||
pub editable: bool,
|
||||
pub created: i64,
|
||||
pub expiration: i64,
|
||||
pub last_read: i64,
|
||||
pub read_count: u64,
|
||||
pub burn_after_reads: u64,
|
||||
pub pasta_type: String,
|
||||
}
|
||||
|
||||
|
@ -73,6 +77,58 @@ impl Pasta {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn last_read_time_ago_as_string(&self) -> String {
|
||||
// get current unix time in seconds
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
// get seconds since last read and convert it to days
|
||||
let days = ((timenow - self.last_read) / 86400) as u16;
|
||||
if days > 1 {
|
||||
return format!("{} days ago", days);
|
||||
};
|
||||
|
||||
// it's less than 1 day, let's do hours then
|
||||
let hours = ((timenow - self.last_read) / 3600) as u16;
|
||||
if hours > 1 {
|
||||
return format!("{} hours ago", hours);
|
||||
};
|
||||
|
||||
// it's less than 1 hour, let's do minutes then
|
||||
let minutes = ((timenow - self.last_read) / 60) as u16;
|
||||
if minutes > 1 {
|
||||
return format!("{} minutes ago", minutes);
|
||||
};
|
||||
|
||||
// it's less than 1 minute, let's do seconds then
|
||||
let seconds = (timenow - self.last_read) as u16;
|
||||
if seconds > 1 {
|
||||
return format!("{} seconds ago", seconds);
|
||||
};
|
||||
|
||||
// it's less than 1 second?????
|
||||
return String::from("just now");
|
||||
}
|
||||
|
||||
pub fn last_read_days_ago(&self) -> u16 {
|
||||
// get current unix time in seconds
|
||||
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => {
|
||||
log::error!("SystemTime before UNIX EPOCH!");
|
||||
0
|
||||
}
|
||||
} as i64;
|
||||
|
||||
// get seconds since last read and convert it to days
|
||||
return ((timenow - self.last_read) / 86400) as u16;
|
||||
}
|
||||
|
||||
pub fn content_syntax_highlighted(&self) -> String {
|
||||
html_highlight(&self.content, &self.extension)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::args::ARGS;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use qrcode_generator::QrCodeEcc;
|
||||
use std::fs;
|
||||
|
||||
use crate::{dbio, Pasta};
|
||||
|
@ -16,8 +18,16 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
|
|||
} as i64;
|
||||
|
||||
pastas.retain(|p| {
|
||||
// expiration is `never` or not reached
|
||||
if p.expiration == 0 || p.expiration > timenow {
|
||||
// keep if:
|
||||
// expiration is `never` or not reached
|
||||
// AND
|
||||
// read count is less than burn limit, or no limit set
|
||||
// AND
|
||||
// has been read in the last N days where N is the arg --gc-days OR N is 0 (no GC)
|
||||
if (p.expiration == 0 || p.expiration > timenow)
|
||||
&& (p.read_count < p.burn_after_reads || p.burn_after_reads == 0)
|
||||
&& (p.last_read_days_ago() < ARGS.gc_days || ARGS.gc_days == 0)
|
||||
{
|
||||
// keep
|
||||
true
|
||||
} else {
|
||||
|
@ -45,6 +55,10 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
|
|||
dbio::save_to_file(pastas);
|
||||
}
|
||||
|
||||
pub fn string_to_qr_svg(str: &str) -> String {
|
||||
qrcode_generator::to_svg_to_string(str, QrCodeEcc::Low, 512, None::<&str>).unwrap()
|
||||
}
|
||||
|
||||
pub fn is_valid_url(url: &str) -> bool {
|
||||
let finder = LinkFinder::new();
|
||||
let spans: Vec<_> = finder.spans(url).collect();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue