(init) init commit

This commit is contained in:
b1ek 2024-02-18 00:53:47 +10:00
commit 84ee7ceecb
Signed by: blek
GPG Key ID: 14546221E3595D0C
12 changed files with 1525 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1228
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "sandy"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
askama = "0.12.1"
rand = "0.8.5"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
static_dir = "0.2.0"
tokio = { version = "1.36.0", features = ["full"] }
warp = "0.3.6"

36
src/main.rs Normal file
View File

@ -0,0 +1,36 @@
#![forbid(unsafe_code)]
use std::{fs::create_dir_all, net::SocketAddr, process::Stdio};
use web::state::{SharedState, TemplateState};
mod web;
fn check() {
let res = std::process::Command::new("python")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
if res.is_err() {
panic!("Python is not installed. Make sure that `python` executable exists in $PATH and is Python 3");
}
if create_dir_all("/tmp/sandy-tmp").is_err() {
panic!("Cannot create temporary directory on /tmp/sandy-tmp");
}
}
#[tokio::main]
async fn main() {
check();
let state = SharedState {
template: TemplateState {}
};
let routes = web::routes(state);
println!("Running on 0.0.0.0:80");
warp::serve(routes).run("0.0.0.0:80".parse::<SocketAddr>().unwrap()).await;
}

15
src/web.rs Normal file
View File

@ -0,0 +1,15 @@
use static_dir::static_dir;
use warp::{reject::Rejection, reply::Reply, Filter};
use self::state::SharedState;
pub mod state;
mod pages;
mod executor;
pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
pages::routes(state.clone())
.or(executor::routes(state))
.or(static_dir!("static"))
}

62
src/web/executor.rs Normal file
View File

@ -0,0 +1,62 @@
use std::{fs, io::Read, process::{Command, Stdio}};
use rand::{distributions::Alphanumeric, Rng};
use serde::{Serialize, Deserialize};
use warp::{reject::Rejection, reply::{json, with_status, Reply}, Filter, http::StatusCode};
use crate::SharedState;
#[derive(Serialize, Deserialize)]
struct ExecutorData {
code: String,
lang: String
}
async fn executor(_state: SharedState, data: ExecutorData) -> Result<Box<dyn Reply>, Rejection> {
if data.lang != "python" {
return Ok(
Box::new(
with_status(
json(&"only python supported".to_string()),
StatusCode::BAD_REQUEST
)
)
)
}
let name = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(64)
.map(char::from)
.collect::<String>();
fs::write(format!("/tmp/sandy-tmp/{name}"), &data.code).unwrap();
let mut out = Command::new("python")
.arg(format!("/tmp/sandy-tmp/{name}"))
.stdout(Stdio::piped())
.spawn()
.unwrap();
let exit_status = out.wait().unwrap();
let mut buf = vec![];
out.stdout.unwrap().read_to_end(&mut buf).unwrap();
let mut stdout = String::from_utf8(buf).unwrap();
stdout += format!("\n---\nCommand exited with code {}", exit_status.code().unwrap()).as_str();
Ok(Box::new(warp::reply::json(&stdout)))
}
fn executor_f(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::post()
.map(move || state.clone())
.and(warp::body::json())
.and_then(executor)
}
pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
executor_f(state)
}

35
src/web/pages.rs Normal file
View File

@ -0,0 +1,35 @@
use warp::{reject::Rejection, reply::{Html, Reply}, Filter};
use askama::Template;
use crate::SharedState;
use super::state::TemplateState;
#[derive(Template)]
#[template( path = "index.html" )]
pub struct Index {
pub state: TemplateState
}
impl Index {
pub async fn handler(state: SharedState) -> Result<Html<String>, Rejection> {
let template = Index {
state: state.template
};
Ok(warp::reply::html(template.render().unwrap()))
}
pub fn filter(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path!()
.and(warp::path::end())
.and(warp::get())
.map(move || state.clone())
.and_then(Index::handler)
}
}
pub fn routes(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
Index::filter(state)
}

10
src/web/state.rs Normal file
View File

@ -0,0 +1,10 @@
#[derive(Debug, Clone)]
pub struct SharedState {
pub template: TemplateState
}
#[derive(Debug, Clone)]
pub struct TemplateState {
}

43
static/script/editor.js Normal file
View File

@ -0,0 +1,43 @@
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.8.3/min/vs' }});
window.MonacoEnvironment = { getWorkerUrl: () => proxy };
window.code = '';
let proxy = URL.createObjectURL(new Blob([`
self.MonacoEnvironment = {
baseUrl: 'https://unpkg.com/monaco-editor@0.8.3/min/'
};
importScripts('https://unpkg.com/monaco-editor@0.8.3/min/vs/base/worker/workerMain.js');
`], { type: 'text/javascript' }));
require(["vs/editor/editor.main"], function () {
let init_lang = 'python';
let editor = monaco.editor.create(document.getElementById('container'), {
value: '# put code here',
language: init_lang,
theme: 'vs-dark'
});
editor.addListener('didType', () => {
window.code = editor.getValue();
});
monaco.languages.getLanguages().forEach(x => {
let el = document.createElement('option');
el.id = x.id;
el.innerText = x.id;
if (x.id == init_lang) el.selected = true;
document.getElementById('lang').appendChild(el);
})
document.getElementById('lang').onchange = (e) => {
monaco.editor.setModelLanguage(editor.getModel(), e.target.value)
}
document.getElementById('run').onclick = (e) => {
executeCode(window.code, editor.getModel().getLanguageIdentifier().language)
}
});

17
static/script/executor.js Normal file
View File

@ -0,0 +1,17 @@
async function executeCode(code, lang) {
const data = await fetch(
window.location.protocol + '//' + window.location.hostname + '/exec',
{
method: 'POST',
body: JSON.stringify({
code,
lang
}),
headers: {
'Content-Type': 'application/json'
}
}
)
const out = await data.json();
document.getElementById('output').value = out;
}

20
templates/base.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>sandy</title>
<style>
html, body, textarea {
background: #1e1e1e;
color: #eee;
}
</style>
{% block head %}{% endblock %}
</head>
<body>
<h1>Sandy</h1>
{% block body %}{% endblock %}
<script src="https://unpkg.com/monaco-editor@0.8.3/min/vs/loader.js"></script>
{% block script %}{% endblock %}
</body>
</html>

43
templates/index.html Normal file
View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block head %}
<style>
#container {
height: 60vh;
width: 100%;
}
#output {
width: calc(100% - 10px);
height: 40vh;
border: 1px solid #e1e1e11e;
border-radius: 4px;
padding: 5px 10px;
}
.top-bar {
display: flex;
justify-content: left;
}
</style>
{% endblock %}
{% block body %}
<div class="top-bar">
<div>
Language:
<select id="lang"></select>
</div>
<button id="run">
Run!
</button>
</div>
<div id='container'></div>
<textarea id="output" readonly></textarea>
{% endblock %}
{% block script %}
<script src="/script/executor.js"></script>
<script src="/script/editor.js"></script>
{% endblock %}