| Index: experimental/webtry/webtry.go
|
| diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go
|
| index 2c3d7c92e6e07e234a35d2a69da33744b8e10836..19eeb4c501d5a88e67a465a6d133af70a5b59240 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
|
|
|