Periodically publish results from mCaptcha/survey. #15
4 changed files with 292 additions and 1 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -502,6 +502,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f0778972c64420fdedc63f09919c8a88bda7b25135357fd25a5d9f3257e832"
|
checksum = "b7f0778972c64420fdedc63f09919c8a88bda7b25135357fd25a5d9f3257e832"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
"serde 1.0.152",
|
"serde 1.0.152",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -737,6 +739,32 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-async"
|
||||||
|
version = "1.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c652d4c48e4dc80b26fadd169c02fb6053d9f57507ddd3e6b8706e7d0242235e"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"cfg-if",
|
||||||
|
"csv-core",
|
||||||
|
"futures",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde 1.0.152",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctr"
|
name = "ctr"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
@ -2021,6 +2049,12 @@ dependencies = [
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.28"
|
version = "0.6.28"
|
||||||
|
@ -2484,6 +2518,7 @@ dependencies = [
|
||||||
"argon2-creds",
|
"argon2-creds",
|
||||||
"cache-buster",
|
"cache-buster",
|
||||||
"config",
|
"config",
|
||||||
|
"csv-async",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -2498,6 +2533,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tera",
|
"tera",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
|
|
@ -67,6 +67,8 @@ mime = "0.3.16"
|
||||||
#sailfish = "0.3.2"
|
#sailfish = "0.3.2"
|
||||||
tracing = { version = "0.1.37", features = ["log"] }
|
tracing = { version = "0.1.37", features = ["log"] }
|
||||||
tera = { version="1.17.1", features=["builtins"]}
|
tera = { version="1.17.1", features=["builtins"]}
|
||||||
|
tokio = { version = "1.25.0", features = ["fs"] }
|
||||||
|
csv-async = { version = "1.2.5", features = ["serde", "tokio"] }
|
||||||
|
|
||||||
#tokio = "1.11.0"
|
#tokio = "1.11.0"
|
||||||
|
|
||||||
|
|
249
src/archive.rs
Normal file
249
src/archive.rs
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::types::time::OffsetDateTime;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::api::v1::admin::campaigns::runners::get_results;
|
||||||
|
use crate::api::v1::admin::campaigns::SurveyResponse;
|
||||||
|
use crate::{errors::ServiceResult, AppData, Settings};
|
||||||
|
|
||||||
|
const CAMPAIGN_INFO_FILE: &str = "campaign.json";
|
||||||
|
|
||||||
|
const BENCHMARK_FILE: &str = "benchmark.csv";
|
||||||
|
|
||||||
|
pub struct Archiver {
|
||||||
|
base_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Archiver {
|
||||||
|
pub fn new(s: &Settings) -> Self {
|
||||||
|
Archiver {
|
||||||
|
base_path: s.archive.base_path.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn campaign_path(&self, id: &Uuid) -> PathBuf {
|
||||||
|
Path::new(&self.base_path).join(&id.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_dir_util(p: &PathBuf) -> ServiceResult<()> {
|
||||||
|
if p.exists() {
|
||||||
|
if !p.is_dir() {
|
||||||
|
fs::remove_file(&p).await.unwrap();
|
||||||
|
fs::create_dir_all(&p).await.unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::create_dir_all(&p).await.unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn archive_path_now(&self, id: &Uuid) -> PathBuf {
|
||||||
|
let unix_time = OffsetDateTime::now_utc().unix_timestamp();
|
||||||
|
self.campaign_path(id).join(unix_time.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn campaign_file_path(&self, id: &Uuid) -> PathBuf {
|
||||||
|
self.archive_path_now(id).join(CAMPAIGN_INFO_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn benchmark_file_path(&self, id: &Uuid) -> PathBuf {
|
||||||
|
self.archive_path_now(id).join(BENCHMARK_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_campaign_file(&self, c: &Campaign) -> ServiceResult<()> {
|
||||||
|
let archive_path = self.archive_path_now(&c.id);
|
||||||
|
Self::create_dir_util(&archive_path).await?;
|
||||||
|
let campaign_file_path = self.campaign_file_path(&c.id);
|
||||||
|
let contents = serde_json::to_string(c).unwrap();
|
||||||
|
// fs::write(campaign_file_path, contents).await.unwrap();
|
||||||
|
let mut file = fs::File::create(&campaign_file_path).await.unwrap();
|
||||||
|
file.write(contents.as_bytes()).await.unwrap();
|
||||||
|
file.flush().await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_benchmark_file(
|
||||||
|
&self,
|
||||||
|
c: &Campaign,
|
||||||
|
data: &AppData,
|
||||||
|
) -> ServiceResult<()> {
|
||||||
|
let archive_path = self.archive_path_now(&c.id);
|
||||||
|
Self::create_dir_util(&archive_path).await?;
|
||||||
|
|
||||||
|
let benchmark_file_path = self.benchmark_file_path(&c.id);
|
||||||
|
struct Username {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
let owner = sqlx::query_as!(
|
||||||
|
Username,
|
||||||
|
"SELECT
|
||||||
|
survey_admins.name
|
||||||
|
FROM
|
||||||
|
survey_admins
|
||||||
|
INNER JOIN survey_campaigns ON
|
||||||
|
survey_admins.ID = survey_campaigns.user_id
|
||||||
|
WHERE
|
||||||
|
survey_campaigns.ID = $1
|
||||||
|
",
|
||||||
|
&c.id
|
||||||
|
)
|
||||||
|
.fetch_one(&data.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut page = 0;
|
||||||
|
let limit = 50;
|
||||||
|
let file = fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.append(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&benchmark_file_path)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut wri = csv_async::AsyncWriter::from_writer(file);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut resp =
|
||||||
|
get_results(&owner.name, &c.id, data, page, limit, None).await?;
|
||||||
|
|
||||||
|
for r in resp.drain(0..) {
|
||||||
|
let csv_resp = to_hashmap(r, c);
|
||||||
|
let keys: Vec<&str> = csv_resp.keys().map(|k| k.as_str()).collect();
|
||||||
|
wri.write_record(&keys).await.unwrap();
|
||||||
|
|
||||||
|
let values: Vec<&str> = csv_resp.values().map(|v| v.as_str()).collect();
|
||||||
|
wri.write_record(&values).await.unwrap();
|
||||||
|
wri.flush().await.unwrap();
|
||||||
|
|
||||||
|
//wri.serialize(csv_resp).await.unwrap();
|
||||||
|
wri.flush().await.unwrap();
|
||||||
|
}
|
||||||
|
page += 1;
|
||||||
|
|
||||||
|
wri.flush().await.unwrap();
|
||||||
|
if resp.len() < limit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn archive(&self, data: &AppData) -> ServiceResult<()> {
|
||||||
|
let mut db_campaigns = sqlx::query_as!(
|
||||||
|
InnerCampaign,
|
||||||
|
"SELECT ID, name, difficulties, created_at FROM survey_campaigns"
|
||||||
|
)
|
||||||
|
.fetch_all(&data.db)
|
||||||
|
.await?;
|
||||||
|
for c in db_campaigns.drain(0..) {
|
||||||
|
let campaign: Campaign = c.into();
|
||||||
|
self.write_campaign_file(&campaign).await?;
|
||||||
|
self.write_benchmark_file(&campaign, data).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_hashmap(s: SurveyResponse, c: &Campaign) -> HashMap<String, String> {
|
||||||
|
let mut map = HashMap::with_capacity(7 + c.difficulties.len());
|
||||||
|
map.insert("user".into(), s.user.id.to_string());
|
||||||
|
map.insert("device_user_provided".into(), s.device_user_provided);
|
||||||
|
map.insert(
|
||||||
|
"device_software_recognised".into(),
|
||||||
|
s.device_software_recognised,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"threads".into(),
|
||||||
|
s.threads.map_or_else(|| "-".into(), |v| v.to_string()),
|
||||||
|
);
|
||||||
|
map.insert("submitted_at".into(), s.submitted_at.to_string());
|
||||||
|
map.insert("submission_type".into(), s.submission_type.to_string());
|
||||||
|
for d in c.difficulties.iter() {
|
||||||
|
let bench = s
|
||||||
|
.benches
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.difficulty == *d as i32)
|
||||||
|
.map_or_else(|| "-".into(), |v| v.duration.to_string());
|
||||||
|
map.insert(format!("Difficulty: {d}"), bench);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
//pub struct CSVSurveyResp {
|
||||||
|
// pub user: Uuid,
|
||||||
|
// pub device_user_provided: String,
|
||||||
|
// pub device_software_recognised: String,
|
||||||
|
// pub id: usize,
|
||||||
|
// pub threads: Option<usize>,
|
||||||
|
// pub submitted_at: i64,
|
||||||
|
// pub submission_type: SubmissionType,
|
||||||
|
// pub benches: String,
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//impl From<SurveyResponse> for CSVSurveyResp {
|
||||||
|
// fn from(s: SurveyResponse) -> Self {
|
||||||
|
// let mut benches = String::default();
|
||||||
|
// for b in s.benches.iter() {
|
||||||
|
// benches = format!("{benches} ({})", b.to_csv_resp());
|
||||||
|
// }
|
||||||
|
// Self {
|
||||||
|
// user: s.user.id,
|
||||||
|
// device_software_recognised: s.device_software_recognised,
|
||||||
|
// device_user_provided: s.device_user_provided,
|
||||||
|
// id: s.id,
|
||||||
|
// threads: s.threads,
|
||||||
|
// submission_type: s.submission_type,
|
||||||
|
// benches,
|
||||||
|
// submitted_at: s.submitted_at,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
struct InnerCampaign {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
difficulties: Vec<i32>,
|
||||||
|
created_at: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Campaign {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub difficulties: Vec<u32>,
|
||||||
|
pub created_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<InnerCampaign> for Campaign {
|
||||||
|
fn from(i: InnerCampaign) -> Self {
|
||||||
|
Self {
|
||||||
|
id: i.id,
|
||||||
|
name: i.name,
|
||||||
|
difficulties: i.difficulties.iter().map(|d| *d as u32).collect(),
|
||||||
|
created_at: i.created_at.unix_timestamp(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ use lazy_static::lazy_static;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod archive;
|
||||||
mod data;
|
mod data;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod pages;
|
mod pages;
|
||||||
|
@ -70,7 +71,7 @@ pub type AppData = actix_web::web::Data<Arc<crate::data::Data>>;
|
||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env::set_var("RUST_LOG", "info");
|
//env::set_var("RUST_LOG", "info");
|
||||||
|
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
@ -84,6 +85,9 @@ async fn main() -> std::io::Result<()> {
|
||||||
sqlx::migrate!("./migrations/").run(&data.db).await.unwrap();
|
sqlx::migrate!("./migrations/").run(&data.db).await.unwrap();
|
||||||
let data = actix_web::web::Data::new(data);
|
let data = actix_web::web::Data::new(data);
|
||||||
|
|
||||||
|
let arch = archive::Archiver::new(&data.settings);
|
||||||
|
arch.archive(&data).await.unwrap();
|
||||||
|
|
||||||
let ip = settings.server.get_ip();
|
let ip = settings.server.get_ip();
|
||||||
println!("Starting server on: http://{}", ip);
|
println!("Starting server on: http://{}", ip);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue