diff options
-rw-r--r-- | experimental/webtry/DESIGN.md | 70 | ||||
-rw-r--r-- | experimental/webtry/main.cpp | 12 | ||||
-rw-r--r-- | experimental/webtry/res/css/webtry.css | 32 | ||||
-rw-r--r-- | experimental/webtry/res/js/webtry.js | 144 | ||||
-rw-r--r-- | experimental/webtry/result.cpp | 1 | ||||
-rw-r--r-- | experimental/webtry/templates/content.html | 19 | ||||
-rw-r--r-- | experimental/webtry/templates/template.cpp | 2 | ||||
-rw-r--r-- | experimental/webtry/webtry.go | 214 |
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 |