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 |