(init) init commit
This commit is contained in:
commit
84ee7ceecb
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
|
@ -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;
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedState {
|
||||
pub template: TemplateState
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TemplateState {
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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 %}
|
Loading…
Reference in New Issue