(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