UPDATE: split the webapp in a widget and the app itself
This splits the webapp in: * IronCalc (the widget to be published on npmjs) * The frontend for our "service" * Adds "dummy code" for the backend using sqlite
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
378f8351d3
commit
8215cfc9fb
1
webapp/app.ironcalc.com/server/.gitignore
vendored
Normal file
1
webapp/app.ironcalc.com/server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/*
|
||||
2705
webapp/app.ironcalc.com/server/Cargo.lock
generated
Normal file
2705
webapp/app.ironcalc.com/server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
webapp/app.ironcalc.com/server/Cargo.toml
Normal file
14
webapp/app.ironcalc.com/server/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "ironcalc_server"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rocket = "0.5"
|
||||
rand = "0.8"
|
||||
ironcalc = { path = "../../../xlsx/"}
|
||||
|
||||
[dependencies.rocket_db_pools]
|
||||
version = "0.2.0"
|
||||
features = ["sqlx_sqlite"]
|
||||
|
||||
7
webapp/app.ironcalc.com/server/README.md
Normal file
7
webapp/app.ironcalc.com/server/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# IronCalc AppServer
|
||||
|
||||
This is the Application server deployed at https://app.ironcalc.com
|
||||
|
||||
It is a simple Rocket server. It is assumed to run alongside a file-server
|
||||
|
||||
All /api/ RPCs will go to this server
|
||||
2
webapp/app.ironcalc.com/server/Rocket.toml
Normal file
2
webapp/app.ironcalc.com/server/Rocket.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[default.databases.ironcalc]
|
||||
url = "ironcalc.sqlite"
|
||||
1
webapp/app.ironcalc.com/server/init_db.sql
Normal file
1
webapp/app.ironcalc.com/server/init_db.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE TABLE models (hash TEXT, bytes BLOB);
|
||||
BIN
webapp/app.ironcalc.com/server/ironcalc.sqlite
Normal file
BIN
webapp/app.ironcalc.com/server/ironcalc.sqlite
Normal file
Binary file not shown.
48
webapp/app.ironcalc.com/server/src/database.rs
Normal file
48
webapp/app.ironcalc.com/server/src/database.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::io;
|
||||
|
||||
use rocket_db_pools::Connection;
|
||||
|
||||
use rocket_db_pools::{sqlx, Database};
|
||||
|
||||
#[derive(Database)]
|
||||
#[database("ironcalc")]
|
||||
pub struct IronCalcDB(sqlx::SqlitePool);
|
||||
|
||||
pub async fn get_model_list_from_db(mut db: Connection<IronCalcDB>) -> Result<Vec<String>, io::Error> {
|
||||
let row: Vec<(String, )> = sqlx::query_as("SELECT * FROM models")
|
||||
.fetch_all(&mut **db)
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(row.into_iter().map(|s| s.0).collect())
|
||||
}
|
||||
|
||||
pub async fn add_model(
|
||||
mut db: Connection<IronCalcDB>,
|
||||
hash: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<(), io::Error> {
|
||||
sqlx::query("INSERT INTO models (hash, bytes) VALUES (?, ?)")
|
||||
.bind(hash)
|
||||
.bind(bytes)
|
||||
.execute(&mut **db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to save to the database: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn select_model(
|
||||
mut db: Connection<IronCalcDB>,
|
||||
hash: &str,
|
||||
) -> Result<Vec<u8>, io::Error> {
|
||||
let row: (Vec<u8>,) = sqlx::query_as("SELECT bytes FROM models WHERE hash = ?")
|
||||
.bind(hash)
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(row.0)
|
||||
}
|
||||
38
webapp/app.ironcalc.com/server/src/id.rs
Normal file
38
webapp/app.ironcalc.com/server/src/id.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
const CHARS: [char; 64] = [
|
||||
'_', '!', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
||||
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
|
||||
fn random(size: usize) -> Vec<u8> {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let mut result: Vec<u8> = vec![0; size];
|
||||
|
||||
rng.fill(&mut result[..]);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn new_id() -> String {
|
||||
let size = 15;
|
||||
let mask = CHARS.len() - 1;
|
||||
let step: usize = 5;
|
||||
let mut id = String::new();
|
||||
|
||||
loop {
|
||||
let bytes = random(step);
|
||||
|
||||
for &byte in &bytes {
|
||||
let byte = byte as usize & mask;
|
||||
|
||||
id.push(CHARS[byte]);
|
||||
|
||||
if id.len() >= size + 2 {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
id.push('-');
|
||||
}
|
||||
}
|
||||
133
webapp/app.ironcalc.com/server/src/main.rs
Normal file
133
webapp/app.ironcalc.com/server/src/main.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
mod database;
|
||||
mod id;
|
||||
|
||||
use std::io::{self, BufWriter, Cursor, Write};
|
||||
|
||||
use database::{add_model, get_model_list_from_db, select_model, IronCalcDB};
|
||||
use ironcalc::base::Model as IModel;
|
||||
use ironcalc::export::save_xlsx_to_writer;
|
||||
use ironcalc::import::load_from_xlsx_bytes;
|
||||
use rocket::data::{Data, ToByteUnit};
|
||||
use rocket::http::{ContentType, Header};
|
||||
use rocket::response::Responder;
|
||||
|
||||
const MAX_SIZE_MB: u8 = 20;
|
||||
|
||||
use rocket_db_pools::{Connection, Database};
|
||||
|
||||
#[derive(Responder)]
|
||||
struct FileResponder {
|
||||
inner: Vec<u8>,
|
||||
content_type: ContentType,
|
||||
disposition: Header<'static>,
|
||||
}
|
||||
|
||||
/// Return an xlsx version of the app.
|
||||
#[post("/api/download", data = "<data>")]
|
||||
async fn download(data: Data<'_>) -> io::Result<FileResponder> {
|
||||
println!("Download xlsx");
|
||||
|
||||
let bytes = data
|
||||
.open(MAX_SIZE_MB.megabytes())
|
||||
.into_bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
if !bytes.is_complete() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The file was not fully uploaded",
|
||||
));
|
||||
};
|
||||
|
||||
let model = IModel::from_bytes(&bytes).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Error creating model, '{e}'"))
|
||||
})?;
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
{
|
||||
let cursor = Cursor::new(&mut buffer);
|
||||
let mut writer = BufWriter::new(cursor);
|
||||
save_xlsx_to_writer(&model, &mut writer).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Error saving model: '{e}'"))
|
||||
})?;
|
||||
writer.flush().unwrap();
|
||||
}
|
||||
|
||||
let content_type = ContentType::new(
|
||||
"application",
|
||||
"vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
);
|
||||
|
||||
let disposition = Header::new(
|
||||
"Content-Disposition".to_string(),
|
||||
"attachment; filename=\"data.xlsx\"".to_string(),
|
||||
);
|
||||
|
||||
println!("Download: success. ");
|
||||
|
||||
Ok(FileResponder {
|
||||
inner: buffer,
|
||||
content_type,
|
||||
disposition,
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves the model on a file called
|
||||
#[post("/api/share", data = "<data>")]
|
||||
async fn share(db: Connection<IronCalcDB>, data: Data<'_>) -> io::Result<String> {
|
||||
println!("start share");
|
||||
let hash = id::new_id();
|
||||
let bytes = data.open(MAX_SIZE_MB.megabytes()).into_bytes().await?;
|
||||
if !bytes.is_complete() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"file was not fully uploaded",
|
||||
));
|
||||
}
|
||||
add_model(db, &hash, &bytes).await?;
|
||||
println!("done share: '{}'", hash);
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
#[get("/api/model/<hash>")]
|
||||
async fn get_model(db: Connection<IronCalcDB>, hash: &str) -> io::Result<Vec<u8>> {
|
||||
let bytes = select_model(db, hash).await.unwrap();
|
||||
println!("Select model: '{}'", hash);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[get("/api/list")]
|
||||
async fn get_model_list(db: Connection<IronCalcDB>) -> io::Result<String> {
|
||||
let model_list = get_model_list_from_db(db).await.unwrap();
|
||||
println!("Model list: '{:?}'", model_list);
|
||||
Ok(model_list.join(","))
|
||||
}
|
||||
|
||||
#[post("/api/upload/<name>", data = "<data>")]
|
||||
async fn upload(data: Data<'_>, name: &str) -> io::Result<Vec<u8>> {
|
||||
println!("start upload");
|
||||
let bytes = data.open(MAX_SIZE_MB.megabytes()).into_bytes().await?;
|
||||
if !bytes.is_complete() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"file was not fully uploaded",
|
||||
));
|
||||
}
|
||||
let workbook = load_from_xlsx_bytes(&bytes, name.trim_end_matches(".xlsx"), "en", "UTC")
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Error loading model: '{e}'")))?;
|
||||
let model = IModel::from_workbook(workbook).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Error creating model: '{e}'"))
|
||||
})?;
|
||||
println!("end upload");
|
||||
Ok(model.to_bytes())
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.attach(IronCalcDB::init())
|
||||
.mount("/", routes![upload, download, share, get_model, get_model_list])
|
||||
}
|
||||
Reference in New Issue
Block a user