aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--experimental/webtry/DESIGN.md70
-rw-r--r--experimental/webtry/main.cpp12
-rw-r--r--experimental/webtry/res/css/webtry.css32
-rw-r--r--experimental/webtry/res/js/webtry.js144
-rw-r--r--experimental/webtry/result.cpp1
-rw-r--r--experimental/webtry/templates/content.html19
-rw-r--r--experimental/webtry/templates/template.cpp2
-rw-r--r--experimental/webtry/webtry.go214
8 files changed, 422 insertions, 72 deletions
diff --git a/experimental/webtry/DESIGN.md b/experimental/webtry/DESIGN.md
index 0bc2145128..4c0cee277a 100644
--- a/experimental/webtry/DESIGN.md
+++ b/experimental/webtry/DESIGN.md
@@ -133,34 +133,51 @@ Initial setup of the database, the user, and the only table:
CREATE DATABASE webtry;
USE webtry;
CREATE USER 'webtry'@'%' IDENTIFIED BY '<password is in valentine>';
- GRANT SELECT, INSERT, UPDATE ON webtry.webtry TO 'webtry'@'%';
- GRANT SELECT, INSERT, UPDATE ON webtry.workspace TO 'webtry'@'%';
- GRANT SELECT, INSERT, UPDATE ON webtry.workspacetry TO 'webtry'@'%';
+ GRANT SELECT, INSERT, UPDATE ON webtry.webtry TO 'webtry'@'%';
+ GRANT SELECT, INSERT, UPDATE ON webtry.workspace TO 'webtry'@'%';
+ GRANT SELECT, INSERT, UPDATE ON webtry.workspacetry TO 'webtry'@'%';
+ GRANT SELECT, INSERT, UPDATE ON webtry.source_images TO 'webtry'@'%';
// If this gets changed also update the sqlite create statement in webtry.go.
CREATE TABLE webtry (
- code TEXT DEFAULT '' NOT NULL,
- create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
- hash CHAR(64) DEFAULT '' NOT NULL,
- PRIMARY KEY(hash)
+ code TEXT DEFAULT '' NOT NULL,
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ hash CHAR(64) DEFAULT '' NOT NULL,
+ source_image_id INTEGER DEFAULT 0 NOT NULL,
+ PRIMARY KEY(hash),
+
+ FOREIGN KEY (source) REFERENCES sources(id)
);
CREATE TABLE workspace (
name CHAR(64) DEFAULT '' NOT NULL,
create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
- PRIMARY KEY(name)
+ PRIMARY KEY(name),
);
CREATE TABLE workspacetry (
- name CHAR(64) DEFAULT '' NOT NULL,
- create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
- hash CHAR(64) DEFAULT '' NOT NULL,
- hidden INTEGER DEFAULT 0 NOT NULL,
+ name CHAR(64) DEFAULT '' NOT NULL,
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ hash CHAR(64) DEFAULT '' NOT NULL,
+ source_image_id INTEGER DEFAULT 0 NOT NULL,
+ hidden INTEGER DEFAULT 0 NOT NULL,
- FOREIGN KEY (name) REFERENCES workspace(name)
+ FOREIGN KEY (name) REFERENCES workspace(name),
);
+ CREATE TABLE source_images (
+ id INTEGER PRIMARY KEY NOT NULL,
+ image MEDIUMBLOB DEFAULT '' NOT NULL, -- Stored as PNG.
+ width INTEGER DEFAULT 0 NOT NULL,
+ height INTEGER DEFAULT 0 NOT NULL,
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ hidden INTEGER DEFAULT 0 NOT NULL
+ );
+
+ ALTER TABLE webtry ADD COLUMN source_image_id INTEGER DEFAULT 0 NOT NULL AFTER hash;
+ ALTER TABLE workspacetry ADD COLUMN source_image_id INTEGER DEFAULT 0 NOT NULL AFTER hash;
+
Common queries webtry.go will use:
INSERT INTO webtry (code, hash) VALUES('int i = 0;...', 'abcdef...');
@@ -183,6 +200,10 @@ Common queries for workspaces:
SELECT name FROM workspace GROUP BY name;
+Common queries for sources:
+
+ SELECT id, image, width, height, create_ts FROM source_images ORDER BY create_ts DESC LIMIT 100;
+
Password for the database will be stored in the metadata instance, if the
metadata server can't be found, i.e. running locally, then a local sqlite
database will be used. To see the current password stored in metadata and the
@@ -202,6 +223,29 @@ the metadata server:
N.B. If you need to change the MySQL password that webtry uses, you must change
it both in MySQL and the value stored in the metadata server.
+Source Images
+-------------
+
+For every try the user can select an optional source image to use as an input.
+The id of the source image is just an integer and is stored in the database
+along with the other try information, such as the code.
+
+The actual image itself is also stored in a separate table, 'sources', in the
+database. On startup we check that all the images are available in 'inout',
+and write out the images if not. Since they are all written to 'inout' we can
+use the same /i/ image handler to serve them.
+
+When a user uploads an image it is decoded and converted to PNG and stored
+as a binary blob in the database.
+
+The bitmap is available to user code as a module level variable:
+
+ SkBitmap source;
+
+The bitmap is read, decoded and stored in source before the seccomp jail is
+instantiated.
+
+
Squid
-----
diff --git a/experimental/webtry/main.cpp b/experimental/webtry/main.cpp
index 7ccb9322f9..44f8aab70f 100644
--- a/experimental/webtry/main.cpp
+++ b/experimental/webtry/main.cpp
@@ -6,6 +6,7 @@
#include "SkData.h"
#include "SkForceLinking.h"
#include "SkGraphics.h"
+#include "SkImageDecoder.h"
#include "SkImageEncoder.h"
#include "SkImageInfo.h"
#include "SkStream.h"
@@ -16,6 +17,10 @@
__SK_FORCE_IMAGE_DECODER_LINKING;
DEFINE_string(out, "", "Filename of the PNG to write to.");
+DEFINE_string(source, "", "Filename of the source image.");
+
+// Defined in template.cpp.
+extern SkBitmap source;
static bool install_syscall_filter() {
struct sock_filter filter[] = {
@@ -89,6 +94,13 @@ int main(int argc, char** argv) {
perror("The --out flag must have an argument.");
return 1;
}
+
+ if (FLAGS_source.count() == 1) {
+ if (!SkImageDecoder::DecodeFile(FLAGS_source[0], &source)) {
+ perror("Unable to read the source image.");
+ }
+ }
+
SkFILEWStream stream(FLAGS_out[0]);
SkImageInfo info = SkImageInfo::MakeN32(256, 256, kPremul_SkAlphaType);
diff --git a/experimental/webtry/res/css/webtry.css b/experimental/webtry/res/css/webtry.css
index 3b04d7dcbf..09751df532 100644
--- a/experimental/webtry/res/css/webtry.css
+++ b/experimental/webtry/res/css/webtry.css
@@ -80,6 +80,38 @@ pre, code {
float: none;
}
+#chooseList {
+ display: flex;
+ flex-flow: row wrap;
+}
+
+#chooseSource {
+ display: none;
+ background: ivory;
+ padding: 1em;
+ border: solid lightgray 2px;
+}
+
+#chooseSource.show {
+ display: block;
+}
+
+#selectedSource {
+ display: none;
+}
+
+#selectedSource.show {
+ display: block;
+}
+
+#sourceCode {
+ display: none;
+}
+
+#sourceCode.show {
+ display: block;
+}
+
#gitInfo {
float: right;
font-size: 70%;
diff --git a/experimental/webtry/res/js/webtry.js b/experimental/webtry/res/js/webtry.js
index b1fe3dd806..b24501e143 100644
--- a/experimental/webtry/res/js/webtry.js
+++ b/experimental/webtry/res/js/webtry.js
@@ -18,29 +18,33 @@
*/
(function() {
function onLoad() {
- var run = document.getElementById('run');
- var permalink = document.getElementById('permalink');
- var embed = document.getElementById('embed');
- var embedButton = document.getElementById('embedButton');
- var code = document.getElementById('code');
- var output = document.getElementById('output');
- var stdout = document.getElementById('stdout');
- var img = document.getElementById('img');
- var tryHistory = document.getElementById('tryHistory');
- var parser = new DOMParser();
- var tryTemplate = document.getElementById('tryTemplate');
+ var run = document.getElementById('run');
+ var permalink = document.getElementById('permalink');
+ var embed = document.getElementById('embed');
+ var embedButton = document.getElementById('embedButton');
+ var code = document.getElementById('code');
+ var output = document.getElementById('output');
+ var stdout = document.getElementById('stdout');
+ var img = document.getElementById('img');
+ var tryHistory = document.getElementById('tryHistory');
+ var parser = new DOMParser();
+ var tryTemplate = document.getElementById('tryTemplate');
+ var sourcesTemplate = document.getElementById('sourcesTemplate');
- var editor = CodeMirror.fromTextArea(code, {
- theme: "default",
- lineNumbers: true,
- matchBrackets: true,
- mode: "text/x-c++src",
- indentUnit: 4,
- });
+ var enableSource = document.getElementById('enableSource');
+ var selectedSource = document.getElementById('selectedSource');
+ var sourceCode = document.getElementById('sourceCode');
+ var chooseSource = document.getElementById('chooseSource');
+ var chooseList = document.getElementById('chooseList');
+
+ // Id of the source image to use, 0 if no source image is used.
+ var sourceId = 0;
+
+ sourceId = parseInt(enableSource.getAttribute('data-id'));
+ if (sourceId) {
+ sourceSelectByID(sourceId);
+ }
- // Match the initial textarea size.
- editor.setSize(editor.defaultCharWidth() * code.cols,
- editor.defaultTextHeight() * code.rows);
function beginWait() {
document.body.classList.add('waiting');
@@ -54,6 +58,101 @@
}
+ function sourceSelectByID(id) {
+ sourceId = id;
+ if (id > 0) {
+ enableSource.checked = true;
+ selectedSource.innerHTML = '<img with=64 height=64 src="/i/image-'+sourceId+'.png" />';
+ selectedSource.classList.add('show');
+ sourceCode.classList.add('show');
+ chooseSource.classList.remove('show');
+ } else {
+ enableSource.checked = false;
+ selectedSource.classList.remove('show');
+ sourceCode.classList.remove('show');
+ }
+ }
+
+
+ /**
+ * A selection has been made in the choiceList.
+ */
+ function sourceSelect() {
+ sourceSelectByID(parseInt(this.getAttribute('data-id')));
+ }
+
+
+ /**
+ * Callback when the loading of the image sources is complete.
+ *
+ * Fills in the list of images from the data returned.
+ */
+ function sourcesComplete(e) {
+ endWait();
+ // The response is JSON of the form:
+ // [
+ // {"id": 1},
+ // {"id": 3},
+ // ...
+ // ]
+ body = JSON.parse(e.target.response);
+ // Clear out the old list if present.
+ while (chooseList.firstChild) {
+ chooseList.removeChild(chooseList.firstChild);
+ }
+ body.forEach(function(source) {
+ var id = 'i'+source.id;
+ var imgsrc = '/i/image-'+source.id+'.png';
+ var clone = sourcesTemplate.content.cloneNode(true);
+ clone.querySelector('img').src = imgsrc;
+ clone.querySelector('button').setAttribute('id', id);
+ clone.querySelector('button').setAttribute('data-id', source.id);
+ chooseList.insertBefore(clone, chooseList.firstChild);
+ chooseList.querySelector('#'+id).addEventListener('click', sourceSelect, true);
+ });
+ chooseSource.classList.add('show');
+ }
+
+
+ /**
+ * Toggle the use of a source image, or select a new source image.
+ *
+ * If enabling source images then load the list of available images via
+ * XHR.
+ */
+ function sourceClick(e) {
+ selectedSource.classList.remove('show');
+ sourceCode.classList.remove('show');
+ if (enableSource.checked) {
+ beginWait();
+ var req = new XMLHttpRequest();
+ req.addEventListener('load', sourcesComplete);
+ req.addEventListener('error', xhrError);
+ req.overrideMimeType('application/json');
+ req.open('GET', '/sources/', true);
+ req.send();
+ } else {
+ sourceId = 0;
+ }
+ }
+
+ enableSource.addEventListener('click', sourceClick, true);
+ selectedSource.addEventListener('click', sourceClick, true);
+
+
+ var editor = CodeMirror.fromTextArea(code, {
+ theme: "default",
+ lineNumbers: true,
+ matchBrackets: true,
+ mode: "text/x-c++src",
+ indentUnit: 4,
+ });
+
+ // Match the initial textarea size.
+ editor.setSize(editor.defaultCharWidth() * code.cols,
+ editor.defaultTextHeight() * code.rows);
+
+
/**
* Callback when there's an XHR error.
* @param e The callback event.
@@ -100,6 +199,7 @@
code.value = body.code;
editor.setValue(body.code);
img.src = '/i/'+body.hash+'.png';
+ sourceSelectByID(body.source);
if (permalink) {
permalink.href = '/c/' + body.hash;
}
@@ -172,7 +272,7 @@
req.overrideMimeType('application/json');
req.open('POST', '/', true);
req.setRequestHeader('content-type', 'application/json');
- req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName}));
+ req.send(JSON.stringify({'code': editor.getValue(), 'name': workspaceName, 'source': sourceId}));
}
run.addEventListener('click', onSubmitCode);
diff --git a/experimental/webtry/result.cpp b/experimental/webtry/result.cpp
index d06ef9c3da..cc1c2f6516 100644
--- a/experimental/webtry/result.cpp
+++ b/experimental/webtry/result.cpp
@@ -7,6 +7,7 @@
#include "SkStream.h"
#include "SkSurface.h"
+SkBitmap source;
void draw(SkCanvas* canvas) {
#line 1
diff --git a/experimental/webtry/templates/content.html b/experimental/webtry/templates/content.html
index 8638cf2bd1..0f2177a38a 100644
--- a/experimental/webtry/templates/content.html
+++ b/experimental/webtry/templates/content.html
@@ -1,5 +1,23 @@
<section id=content>
+
+ <template id=sourcesTemplate>
+ <button id="" class=source><img width=64 height=64 src=''></button>
+ </template>
+ <input type="checkbox" id="enableSource" data-id="{{.Source}}"> Use an input bitmap.
+ <br>
+ <button id=selectedSource></button>
+ <pre id=sourceCode>SkBitmap source;</pre>
+ <div id=chooseSource>
+ Choose an image below or upload a new one to use as an input bitmap.
+ <div id="chooseList">
+ </div>
+ <form action="/sources/" method="post" accept-charset="utf-8" enctype="multipart/form-data">
+ <input type="file" accept="image/*" name="upload" value="" id="upload">
+ <input type="submit" value="Add Image">
+ </form>
+ </div>
+
<pre>
<textarea spellcheck=false name='code' id='code' rows='15' cols='100'>{{.Code}}</textarea>
</pre>
@@ -9,6 +27,7 @@
<input type='button' value='Embed' id='embedButton' disabled/>
<input type="text" value="" id="embed" readonly style="display:none;">
+ <br>
<p>
<img touch-action='none' class='zoom' id='img' src='{{if .Hash}}/i/{{.Hash}}.png{{end}}'/>
diff --git a/experimental/webtry/templates/template.cpp b/experimental/webtry/templates/template.cpp
index 67d2c04dc9..c1f40f30e7 100644
--- a/experimental/webtry/templates/template.cpp
+++ b/experimental/webtry/templates/template.cpp
@@ -158,4 +158,6 @@
#include "SkXfermode.h"
#include "SkXfermodeImageFilter.h"
+SkBitmap source;
+
{{.Code}}
diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go
index 2c3d7c92e6..19eeb4c501 100644
--- a/experimental/webtry/webtry.go
+++ b/experimental/webtry/webtry.go
@@ -5,10 +5,15 @@ import (
"crypto/md5"
"database/sql"
"encoding/base64"
+ "encoding/binary"
"encoding/json"
"flag"
"fmt"
htemplate "html/template"
+ "image"
+ _ "image/gif"
+ _ "image/jpeg"
+ "image/png"
"io/ioutil"
"log"
"math/rand"
@@ -70,7 +75,7 @@ var (
iframeLink = regexp.MustCompile("^/iframe/([a-f0-9]+)$")
// imageLink is the regex that matches URLs paths that are direct links to PNGs.
- imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
+ imageLink = regexp.MustCompile("^/i/([a-z0-9-]+.png)$")
// tryInfoLink is the regex that matches URLs paths that are direct links to data about a single try.
tryInfoLink = regexp.MustCompile("^/json/([a-f0-9]+)$")
@@ -221,14 +226,28 @@ func init() {
log.Printf("ERROR: Failed to open: %q\n", err)
panic(err)
}
- sql := `CREATE TABLE webtry (
- code TEXT DEFAULT '' NOT NULL,
- create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
- hash CHAR(64) DEFAULT '' NOT NULL,
+ sql := `CREATE TABLE source_images (
+ id INTEGER PRIMARY KEY NOT NULL,
+ image MEDIUMBLOB DEFAULT '' NOT NULL, -- formatted as a PNG.
+ width INTEGER DEFAULT 0 NOT NULL,
+ height INTEGER DEFAULT 0 NOT NULL,
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ hidden INTEGER DEFAULT 0 NOT NULL
+ )`
+ _, err = db.Exec(sql)
+ log.Printf("Info: status creating sqlite table for sources: %q\n", err)
+
+ sql = `CREATE TABLE webtry (
+ code TEXT DEFAULT '' NOT NULL,
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ hash CHAR(64) DEFAULT '' NOT NULL,
+ source_image_id INTEGER DEFAULT 0 NOT NULL,
+
PRIMARY KEY(hash)
)`
_, err = db.Exec(sql)
log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
+
sql = `CREATE TABLE workspace (
name CHAR(64) DEFAULT '' NOT NULL,
create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
@@ -236,13 +255,15 @@ func init() {
)`
_, err = db.Exec(sql)
log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
+
sql = `CREATE TABLE workspacetry (
- name CHAR(64) DEFAULT '' NOT NULL,
- create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
- hash CHAR(64) DEFAULT '' NOT NULL,
- hidden INTEGER DEFAULT 0 NOT NULL,
+ name CHAR(64) DEFAULT '' NOT NULL,
+ create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ hash CHAR(64) DEFAULT '' NOT NULL,
+ hidden INTEGER DEFAULT 0 NOT NULL,
+ source_image_id INTEGER DEFAULT 0 NOT NULL,
- FOREIGN KEY (name) REFERENCES workspace(name)
+ FOREIGN KEY (name) REFERENCES workspace(name)
)`
_, err = db.Exec(sql)
log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
@@ -258,6 +279,34 @@ func init() {
}
}()
+ writeOutAllSourceImages()
+}
+
+func writeOutAllSourceImages() {
+ // Pull all the source images from the db and write them out to inout.
+ rows, err := db.Query("SELECT id, image, create_ts FROM source_images ORDER BY create_ts DESC")
+
+ if err != nil {
+ log.Printf("ERROR: Failed to open connection to SQL server: %q\n", err)
+ panic(err)
+ }
+ for rows.Next() {
+ var id int
+ var image []byte
+ var create_ts time.Time
+ if err := rows.Scan(&id, &image, &create_ts); err != nil {
+ log.Printf("Error: failed to fetch from database: %q", err)
+ continue
+ }
+ filename := fmt.Sprintf("../../../inout/image-%d.png", id)
+ if _, err := os.Stat(filename); os.IsExist(err) {
+ log.Printf("Skipping write since file exists: %q", filename)
+ continue
+ }
+ if err := ioutil.WriteFile(filename, image, 0666); err != nil {
+ log.Printf("Error: failed to write image file: %q", err)
+ }
+ }
}
// Titlebar is used in titlebar template expansion.
@@ -270,6 +319,7 @@ type Titlebar struct {
type userCode struct {
Code string
Hash string
+ Source int
Titlebar Titlebar
}
@@ -283,10 +333,11 @@ func expandToFile(filename string, code string, t *template.Template) error {
return t.Execute(f, userCode{Code: code, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}})
}
-// expandCode expands the template into a file and calculate the MD5 hash.
-func expandCode(code string) (string, error) {
+// expandCode expands the template into a file and calculates the MD5 hash.
+func expandCode(code string, source int) (string, error) {
h := md5.New()
h.Write([]byte(code))
+ binary.Write(h, binary.LittleEndian, int64(source))
hash := fmt.Sprintf("%x", h.Sum(nil))
// At this point we are running in skia/experimental/webtry, making cache a
// peer directory to skia.
@@ -360,20 +411,96 @@ func reportTryError(w http.ResponseWriter, r *http.Request, err error, message,
w.Write(resp)
}
-func writeToDatabase(hash string, code string, workspaceName string) {
+func writeToDatabase(hash string, code string, workspaceName string, source int) {
if db == nil {
return
}
- if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
+ if _, err := db.Exec("INSERT INTO webtry (code, hash, source_image_id) VALUES(?, ?, ?)", code, hash, source); err != nil {
log.Printf("ERROR: Failed to insert code into database: %q\n", err)
}
if workspaceName != "" {
- if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil {
+ if _, err := db.Exec("INSERT INTO workspacetry (name, hash, source_image_id) VALUES(?, ?, ?)", workspaceName, hash, source); err != nil {
log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
}
}
}
+type Sources struct {
+ Id int `json:"id"`
+}
+
+// sourcesHandler serves up the PNG of a specific try.
+func sourcesHandler(w http.ResponseWriter, r *http.Request) {
+ log.Printf("Sources Handler: %q\n", r.URL.Path)
+ if r.Method == "GET" {
+ rows, err := db.Query("SELECT id, create_ts FROM source_images WHERE hidden=0 ORDER BY create_ts DESC")
+
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to query sources: %s.", err), 500)
+ }
+ sources := make([]Sources, 0, 0)
+ for rows.Next() {
+ var id int
+ var create_ts time.Time
+ if err := rows.Scan(&id, &create_ts); err != nil {
+ log.Printf("Error: failed to fetch from database: %q", err)
+ continue
+ }
+ sources = append(sources, Sources{Id: id})
+ }
+
+ resp, err := json.Marshal(sources)
+ if err != nil {
+ reportError(w, r, err, "Failed to serialize a response.")
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.Write(resp)
+
+ } else if r.Method == "POST" {
+ if err := r.ParseMultipartForm(1000000); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500)
+ return
+ }
+ if _, ok := r.MultipartForm.File["upload"]; !ok {
+ http.Error(w, "Invalid upload.", 500)
+ return
+ }
+ if len(r.MultipartForm.File["upload"]) != 1 {
+ http.Error(w, "Wrong number of uploads.", 500)
+ return
+ }
+ f, err := r.MultipartForm.File["upload"][0].Open()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load image: %s.", err), 500)
+ return
+ }
+ defer f.Close()
+ m, _, err := image.Decode(f)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to decode image: %s.", err), 500)
+ return
+ }
+ var b bytes.Buffer
+ png.Encode(&b, m)
+ bounds := m.Bounds()
+ width := bounds.Max.Y - bounds.Min.Y
+ height := bounds.Max.X - bounds.Min.X
+ if _, err := db.Exec("INSERT INTO source_images (image, width, height) VALUES(?, ?, ?)", b.Bytes(), width, height); err != nil {
+ log.Printf("ERROR: Failed to insert sources into database: %q\n", err)
+ http.Error(w, fmt.Sprintf("Failed to store image: %s.", err), 500)
+ return
+ }
+ go writeOutAllSourceImages()
+
+ // Now redirect back to where we came from.
+ http.Redirect(w, r, r.Referer(), 302)
+ } else {
+ http.NotFound(w, r)
+ return
+ }
+}
+
// imageHandler serves up the PNG of a specific try.
func imageHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Image Handler: %q\n", r.URL.Path)
@@ -393,6 +520,7 @@ func imageHandler(w http.ResponseWriter, r *http.Request) {
type Try struct {
Hash string `json:"hash"`
+ Source int
CreateTS string `json:"create_ts"`
}
@@ -431,6 +559,7 @@ type Workspace struct {
Name string
Code string
Hash string
+ Source int
Tries []Try
Titlebar Titlebar
}
@@ -452,13 +581,14 @@ func newWorkspace() (string, error) {
}
// getCode returns the code for a given hash, or the empty string if not found.
-func getCode(hash string) (string, error) {
+func getCode(hash string) (string, int, error) {
code := ""
- if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
+ source := 0
+ if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil {
log.Printf("ERROR: Code for hash is missing: %q\n", err)
- return code, err
+ return code, source, err
}
- return code, nil
+ return code, source, nil
}
func workspaceHandler(w http.ResponseWriter, r *http.Request) {
@@ -469,7 +599,7 @@ func workspaceHandler(w http.ResponseWriter, r *http.Request) {
name := ""
if len(match) == 2 {
name = match[1]
- rows, err := db.Query("SELECT create_ts, hash FROM workspacetry WHERE name=? ORDER BY create_ts", name)
+ rows, err := db.Query("SELECT create_ts, hash, source_image_id FROM workspacetry WHERE name=? ORDER BY create_ts", name)
if err != nil {
reportError(w, r, err, "Failed to select.")
return
@@ -477,23 +607,25 @@ func workspaceHandler(w http.ResponseWriter, r *http.Request) {
for rows.Next() {
var hash string
var create_ts time.Time
- if err := rows.Scan(&create_ts, &hash); err != nil {
+ var source int
+ if err := rows.Scan(&create_ts, &hash, &source); err != nil {
log.Printf("Error: failed to fetch from database: %q", err)
continue
}
- tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
+ tries = append(tries, Try{Hash: hash, Source: source, CreateTS: create_ts.Format("2006-02-01")})
}
}
var code string
var hash string
+ source := 0
if len(tries) == 0 {
code = DEFAULT_SAMPLE
} else {
hash = tries[len(tries)-1].Hash
- code, _ = getCode(hash)
+ code, source, _ = getCode(hash)
}
w.Header().Set("Content-Type", "text/html")
- if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
+ if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
log.Printf("ERROR: Failed to expand template: %q\n", err)
}
} else if r.Method == "POST" {
@@ -518,8 +650,9 @@ func hasPreProcessor(code string) bool {
}
type TryRequest struct {
- Code string `json:"code"`
- Name string `json:"name"` // Optional name of the workspace the code is in.
+ Code string `json:"code"`
+ Name string `json:"name"` // Optional name of the workspace the code is in.
+ Source int `json:"source"` // ID of the source image, 0 if none.
}
// iframeHandler handles the GET and POST of the main page.
@@ -540,21 +673,22 @@ func iframeHandler(w http.ResponseWriter, r *http.Request) {
return
}
var code string
- code, err := getCode(hash)
+ code, source, err := getCode(hash)
if err != nil {
http.NotFound(w, r)
return
}
// Expand the template.
w.Header().Set("Content-Type", "text/html")
- if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash}); err != nil {
+ if err := iframeTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source}); err != nil {
log.Printf("ERROR: Failed to expand template: %q\n", err)
}
}
type TryInfo struct {
- Hash string `json:"hash"`
- Code string `json:"code"`
+ Hash string `json:"hash"`
+ Code string `json:"code"`
+ Source int `json:"source"`
}
// tryInfoHandler returns information about a specific try.
@@ -570,14 +704,15 @@ func tryInfoHandler(w http.ResponseWriter, r *http.Request) {
return
}
hash := match[1]
- code, err := getCode(hash)
+ code, source, err := getCode(hash)
if err != nil {
http.NotFound(w, r)
return
}
m := TryInfo{
- Hash: hash,
- Code: code,
+ Hash: hash,
+ Code: code,
+ Source: source,
}
resp, err := json.Marshal(m)
if err != nil {
@@ -599,6 +734,7 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Main Handler: %q\n", r.URL.Path)
if r.Method == "GET" {
code := DEFAULT_SAMPLE
+ source := 0
match := directLink.FindStringSubmatch(r.URL.Path)
var hash string
if len(match) == 2 && r.URL.Path != "/" {
@@ -608,14 +744,14 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Update 'code' with the code found in the database.
- if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
+ if err := db.QueryRow("SELECT code, source_image_id FROM webtry WHERE hash=?", hash).Scan(&code, &source); err != nil {
http.NotFound(w, r)
return
}
}
// Expand the template.
w.Header().Set("Content-Type", "text/html")
- if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
+ if err := indexTemplate.Execute(w, userCode{Code: code, Hash: hash, Source: source, Titlebar: Titlebar{GitHash: gitHash, GitInfo: gitInfo}}); err != nil {
log.Printf("ERROR: Failed to expand template: %q\n", err)
}
} else if r.Method == "POST" {
@@ -641,12 +777,12 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
reportTryError(w, r, err, "Preprocessor macros aren't allowed.", "")
return
}
- hash, err := expandCode(LineNumbers(request.Code))
+ hash, err := expandCode(LineNumbers(request.Code), request.Source)
if err != nil {
reportTryError(w, r, err, "Failed to write the code to compile.", hash)
return
}
- writeToDatabase(hash, request.Code, request.Name)
+ writeToDatabase(hash, request.Code, request.Name, request.Source)
message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
if err != nil {
message = cleanCompileOutput(message, hash)
@@ -661,6 +797,9 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
}
message += linkMessage
cmd := hash + " --out " + hash + ".png"
+ if request.Source > 0 {
+ cmd += fmt.Sprintf(" --source image-%d.png", request.Source)
+ }
if *useChroot {
cmd = "schroot -c webtry --directory=/inout -- /inout/" + cmd
} else {
@@ -706,6 +845,7 @@ func main() {
http.HandleFunc("/recent/", autogzip.HandleFunc(recentHandler))
http.HandleFunc("/iframe/", autogzip.HandleFunc(iframeHandler))
http.HandleFunc("/json/", autogzip.HandleFunc(tryInfoHandler))
+ http.HandleFunc("/sources/", autogzip.HandleFunc(sourcesHandler))
// Resources are served directly
// TODO add support for caching/etags/gzip