OLD | NEW |
1 package main | 1 package main |
2 | 2 |
3 import ( | 3 import ( |
4 "bytes" | 4 "bytes" |
5 "crypto/md5" | 5 "crypto/md5" |
6 "database/sql" | 6 "database/sql" |
7 "encoding/base64" | 7 "encoding/base64" |
8 "encoding/json" | 8 "encoding/json" |
9 "flag" | 9 "flag" |
10 "fmt" | 10 "fmt" |
11 _ "github.com/go-sql-driver/mysql" | 11 _ "github.com/go-sql-driver/mysql" |
12 _ "github.com/mattn/go-sqlite3" | 12 _ "github.com/mattn/go-sqlite3" |
13 htemplate "html/template" | 13 htemplate "html/template" |
14 "io/ioutil" | 14 "io/ioutil" |
15 "log" | 15 "log" |
| 16 "math/rand" |
16 "net/http" | 17 "net/http" |
17 "os" | 18 "os" |
18 "os/exec" | 19 "os/exec" |
19 "path/filepath" | 20 "path/filepath" |
20 "regexp" | 21 "regexp" |
21 "strings" | 22 "strings" |
22 "text/template" | 23 "text/template" |
23 "time" | 24 "time" |
24 ) | 25 ) |
25 | 26 |
(...skipping 12 matching lines...) Expand all Loading... |
38 MAX_TRY_SIZE = 64000 | 39 MAX_TRY_SIZE = 64000 |
39 ) | 40 ) |
40 | 41 |
41 var ( | 42 var ( |
42 // codeTemplate is the cpp code template the user's code is copied into. | 43 // codeTemplate is the cpp code template the user's code is copied into. |
43 codeTemplate *template.Template = nil | 44 codeTemplate *template.Template = nil |
44 | 45 |
45 // indexTemplate is the main index.html page we serve. | 46 // indexTemplate is the main index.html page we serve. |
46 indexTemplate *htemplate.Template = nil | 47 indexTemplate *htemplate.Template = nil |
47 | 48 |
48 » // recentTemplate is a list of recent images. | 49 » // recentTemplate is a list of recent images. |
49 recentTemplate *htemplate.Template = nil | 50 recentTemplate *htemplate.Template = nil |
50 | 51 |
| 52 // workspaceTemplate is the page for workspaces, a series of webtrys. |
| 53 workspaceTemplate *htemplate.Template = nil |
| 54 |
51 // db is the database, nil if we don't have an SQL database to store dat
a into. | 55 // db is the database, nil if we don't have an SQL database to store dat
a into. |
52 db *sql.DB = nil | 56 db *sql.DB = nil |
53 | 57 |
54 // directLink is the regex that matches URLs paths that are direct links
. | 58 // directLink is the regex that matches URLs paths that are direct links
. |
55 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$") | 59 directLink = regexp.MustCompile("^/c/([a-f0-9]+)$") |
56 | 60 |
57 // imageLink is the regex that matches URLs paths that are direct links
to PNGs. | 61 // imageLink is the regex that matches URLs paths that are direct links
to PNGs. |
58 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$") | 62 imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$") |
| 63 |
| 64 // workspaceLink is the regex that matches URLs paths for workspaces. |
| 65 workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$") |
| 66 |
| 67 workspaceNameAdj = []string{ |
| 68 "autumn", "hidden", "bitter", "misty", "silent", "empty", "dry",
"dark", |
| 69 "summer", "icy", "delicate", "quiet", "white", "cool", "spring",
"winter", |
| 70 "patient", "twilight", "dawn", "crimson", "wispy", "weathered",
"blue", |
| 71 "billowing", "broken", "cold", "damp", "falling", "frosty", "gre
en", |
| 72 "long", "late", "lingering", "bold", "little", "morning", "muddy
", "old", |
| 73 "red", "rough", "still", "small", "sparkling", "throbbing", "shy
", |
| 74 "wandering", "withered", "wild", "black", "young", "holy", "soli
tary", |
| 75 "fragrant", "aged", "snowy", "proud", "floral", "restless", "div
ine", |
| 76 "polished", "ancient", "purple", "lively", "nameless", |
| 77 } |
| 78 |
| 79 workspaceNameNoun = []string{ |
| 80 "waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "
morning", |
| 81 "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "gli
tter", |
| 82 "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "br
ook", |
| 83 "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "
firefly", |
| 84 "feather", "grass", "haze", "mountain", "night", "pond", "darkne
ss", |
| 85 "snowflake", "silence", "sound", "sky", "shape", "surf", "thunde
r", |
| 86 "violet", "water", "wildflower", "wave", "water", "resonance", "
sun", |
| 87 "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "pap
er", |
| 88 "frog", "smoke", "star", |
| 89 } |
59 ) | 90 ) |
60 | 91 |
61 // flags | 92 // flags |
62 var ( | 93 var ( |
63 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the
schroot jail.") | 94 useChroot = flag.Bool("use_chroot", false, "Run the compiled code in the
schroot jail.") |
64 port = flag.String("port", ":8000", "HTTP service address (e.g., ':
8000')") | 95 port = flag.String("port", ":8000", "HTTP service address (e.g., ':
8000')") |
65 ) | 96 ) |
66 | 97 |
67 // lineNumbers adds #line numbering to the user's code. | 98 // lineNumbers adds #line numbering to the user's code. |
68 func LineNumbers(c string) string { | 99 func LineNumbers(c string) string { |
(...skipping 24 matching lines...) Expand all Loading... |
93 indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/
index.html")) | 124 indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/
index.html")) |
94 if err != nil { | 125 if err != nil { |
95 panic(err) | 126 panic(err) |
96 } | 127 } |
97 | 128 |
98 recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates
/recent.html")) | 129 recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates
/recent.html")) |
99 if err != nil { | 130 if err != nil { |
100 panic(err) | 131 panic(err) |
101 } | 132 } |
102 | 133 |
| 134 workspaceTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templa
tes/workspace.html")) |
| 135 if err != nil { |
| 136 panic(err) |
| 137 } |
| 138 |
103 // Connect to MySQL server. First, get the password from the metadata se
rver. | 139 // Connect to MySQL server. First, get the password from the metadata se
rver. |
104 // See https://developers.google.com/compute/docs/metadata#custom. | 140 // See https://developers.google.com/compute/docs/metadata#custom. |
105 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/i
nstance/attributes/password", nil) | 141 req, err := http.NewRequest("GET", "http://metadata/computeMetadata/v1/i
nstance/attributes/password", nil) |
106 if err != nil { | 142 if err != nil { |
107 panic(err) | 143 panic(err) |
108 } | 144 } |
109 client := http.Client{} | 145 client := http.Client{} |
110 req.Header.Add("X-Google-Metadata-Request", "True") | 146 req.Header.Add("X-Google-Metadata-Request", "True") |
111 if resp, err := client.Do(req); err == nil { | 147 if resp, err := client.Do(req); err == nil { |
112 password, err := ioutil.ReadAll(resp.Body) | 148 password, err := ioutil.ReadAll(resp.Body) |
113 if err != nil { | 149 if err != nil { |
114 log.Printf("ERROR: Failed to read password from metadata
server: %q\n", err) | 150 log.Printf("ERROR: Failed to read password from metadata
server: %q\n", err) |
115 panic(err) | 151 panic(err) |
116 } | 152 } |
117 // The IP address of the database is found here: | 153 // The IP address of the database is found here: |
118 // https://console.developers.google.com/project/31977622648/
sql/instances/webtry/overview | 154 // https://console.developers.google.com/project/31977622648/
sql/instances/webtry/overview |
119 // And 3306 is the default port for MySQL. | 155 // And 3306 is the default port for MySQL. |
120 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.8
3.52:3306)/webtry?parseTime=true", password)) | 156 db, err = sql.Open("mysql", fmt.Sprintf("webtry:%s@tcp(173.194.8
3.52:3306)/webtry?parseTime=true", password)) |
121 if err != nil { | 157 if err != nil { |
122 log.Printf("ERROR: Failed to open connection to SQL serv
er: %q\n", err) | 158 log.Printf("ERROR: Failed to open connection to SQL serv
er: %q\n", err) |
123 panic(err) | 159 panic(err) |
124 } | 160 } |
125 } else { | 161 } else { |
| 162 log.Printf("INFO: Failed to find metadata, unable to connect to
MySQL server (Expected when running locally): %q\n", err) |
126 // Fallback to sqlite for local use. | 163 // Fallback to sqlite for local use. |
127 db, err = sql.Open("sqlite3", "./webtry.db") | 164 db, err = sql.Open("sqlite3", "./webtry.db") |
128 if err != nil { | 165 if err != nil { |
129 log.Printf("ERROR: Failed to open: %q\n", err) | 166 log.Printf("ERROR: Failed to open: %q\n", err) |
130 panic(err) | 167 panic(err) |
131 } | 168 } |
132 sql := `CREATE TABLE webtry ( | 169 sql := `CREATE TABLE webtry ( |
133 code TEXT DEFAULT '' NOT NULL, | 170 code TEXT DEFAULT '' NOT NULL, |
134 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, | 171 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
135 hash CHAR(64) DEFAULT '' NOT NULL, | 172 hash CHAR(64) DEFAULT '' NOT NULL, |
136 PRIMARY KEY(hash) | 173 PRIMARY KEY(hash) |
137 )` | 174 )` |
138 » » db.Exec(sql) | 175 » » _, err = db.Exec(sql) |
139 » » log.Printf("INFO: Failed to find metadata, unable to connect to
MySQL server (Expected when running locally): %q\n", err) | 176 » » log.Printf("Info: status creating sqlite table for webtry: %q\n"
, err) |
| 177 » » sql = `CREATE TABLE workspace ( |
| 178 name TEXT DEFAULT '' NOT NULL, |
| 179 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
| 180 PRIMARY KEY(name) |
| 181 )` |
| 182 » » _, err = db.Exec(sql) |
| 183 » » log.Printf("Info: status creating sqlite table for workspace: %q
\n", err) |
| 184 » » sql = `CREATE TABLE workspacetry ( |
| 185 name TEXT DEFAULT '' NOT NULL, |
| 186 create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, |
| 187 hash CHAR(64) DEFAULT '' NOT NULL, |
| 188 hidden INTEGER DEFAULT 0 NOT NULL, |
| 189 FOREIGN KEY (name) REFERENCES workspace(name) |
| 190 )` |
| 191 » » _, err = db.Exec(sql) |
| 192 » » log.Printf("Info: status creating sqlite table for workspace try
: %q\n", err) |
140 } | 193 } |
141 } | 194 } |
142 | 195 |
143 // userCode is used in template expansion. | 196 // userCode is used in template expansion. |
144 type userCode struct { | 197 type userCode struct { |
145 UserCode string | 198 UserCode string |
146 } | 199 } |
147 | 200 |
148 // expandToFile expands the template and writes the result to the file. | 201 // expandToFile expands the template and writes the result to the file. |
149 func expandToFile(filename string, code string, t *template.Template) error { | 202 func expandToFile(filename string, code string, t *template.Template) error { |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
224 } | 277 } |
225 log.Printf("Error: %s\n%s", message, err.Error()) | 278 log.Printf("Error: %s\n%s", message, err.Error()) |
226 resp, err := json.Marshal(m) | 279 resp, err := json.Marshal(m) |
227 if err != nil { | 280 if err != nil { |
228 http.Error(w, "Failed to serialize a response", 500) | 281 http.Error(w, "Failed to serialize a response", 500) |
229 return | 282 return |
230 } | 283 } |
231 w.Write(resp) | 284 w.Write(resp) |
232 } | 285 } |
233 | 286 |
234 func writeToDatabase(hash string, code string) { | 287 func writeToDatabase(hash string, code string, workspaceName string) { |
235 if db == nil { | 288 if db == nil { |
236 return | 289 return |
237 } | 290 } |
238 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", cod
e, hash); err != nil { | 291 if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", cod
e, hash); err != nil { |
239 log.Printf("ERROR: Failed to insert code into database: %q\n", e
rr) | 292 log.Printf("ERROR: Failed to insert code into database: %q\n", e
rr) |
240 } | 293 } |
| 294 if workspaceName != "" { |
| 295 if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALU
ES(?, ?)", workspaceName, hash); err != nil { |
| 296 log.Printf("ERROR: Failed to insert into workspacetry ta
ble: %q\n", err) |
| 297 } |
| 298 } |
241 } | 299 } |
242 | 300 |
243 func cssHandler(w http.ResponseWriter, r *http.Request) { | 301 func cssHandler(w http.ResponseWriter, r *http.Request) { |
244 http.ServeFile(w, r, "css/webtry.css") | 302 http.ServeFile(w, r, "css/webtry.css") |
245 } | 303 } |
246 | 304 |
247 // imageHandler serves up the PNG of a specific try. | 305 // imageHandler serves up the PNG of a specific try. |
248 func imageHandler(w http.ResponseWriter, r *http.Request) { | 306 func imageHandler(w http.ResponseWriter, r *http.Request) { |
249 log.Printf("Image Handler: %q\n", r.URL.Path) | 307 log.Printf("Image Handler: %q\n", r.URL.Path) |
250 if r.Method != "GET" { | 308 if r.Method != "GET" { |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
287 log.Printf("Error: failed to fetch from database: %q", e
rr) | 345 log.Printf("Error: failed to fetch from database: %q", e
rr) |
288 continue | 346 continue |
289 } | 347 } |
290 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form
at("2006-02-01")}) | 348 recent = append(recent, Try{Hash: hash, CreateTS: create_ts.Form
at("2006-02-01")}) |
291 } | 349 } |
292 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil { | 350 if err := recentTemplate.Execute(w, Recent{Tries: recent}); err != nil { |
293 log.Printf("ERROR: Failed to expand template: %q\n", err) | 351 log.Printf("ERROR: Failed to expand template: %q\n", err) |
294 } | 352 } |
295 } | 353 } |
296 | 354 |
| 355 type Workspace struct { |
| 356 Name string |
| 357 Code string |
| 358 Tries []Try |
| 359 } |
| 360 |
| 361 func newWorkspace() (string, error) { |
| 362 for i := 0; i < 10; i++ { |
| 363 adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))] |
| 364 noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))] |
| 365 suffix := rand.Intn(1000) |
| 366 name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix) |
| 367 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", n
ame); err == nil { |
| 368 return name, nil |
| 369 } else { |
| 370 log.Printf("ERROR: Failed to insert workspace into datab
ase: %q\n", err) |
| 371 } |
| 372 } |
| 373 return "", fmt.Errorf("Failed to create a new workspace") |
| 374 } |
| 375 |
| 376 func getCode(hash string) string { |
| 377 code := "" |
| 378 if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan
(&code); err != nil { |
| 379 log.Printf("ERROR: Code for hash is missing: %q\n", err) |
| 380 } |
| 381 return code |
| 382 } |
| 383 |
| 384 func workspaceHandler(w http.ResponseWriter, r *http.Request) { |
| 385 // POST creates a new workspace and redirect to the newly created worksp
ace. |
| 386 // GET w/o a namespace name just gets the 'create' page. |
| 387 // GET w/name gets that workspace doc, which is just a list of tries. |
| 388 // What's the difference? The 'create' flow has a 'Create' button |
| 389 // and no code. A workspace has a 'Run' button |
| 390 // and shows the code for the most recent try. Also includes a list |
| 391 // of all the tries in this workspace with a fingernail for each one. |
| 392 // Run is actually handled by a POST to /. |
| 393 log.Printf("Workspace Handler: %q\n", r.URL.Path) |
| 394 if r.Method == "GET" { |
| 395 tries := []Try{} |
| 396 match := workspaceLink.FindStringSubmatch(r.URL.Path) |
| 397 name := "" |
| 398 if len(match) == 2 { |
| 399 log.Printf("Got a valid match %q\n", match) |
| 400 name = match[1] |
| 401 // Load all the tries for this workspace. |
| 402 rows, err := db.Query("SELECT create_ts, hash FROM works
pacetry WHERE name=? ORDER BY create_ts DESC ", name) |
| 403 if err != nil { |
| 404 reportError(w, r, err, "Failed to select.") |
| 405 return |
| 406 } |
| 407 for rows.Next() { |
| 408 var hash string |
| 409 var create_ts time.Time |
| 410 if err := rows.Scan(&create_ts, &hash); err != n
il { |
| 411 log.Printf("Error: failed to fetch from
database: %q", err) |
| 412 continue |
| 413 } |
| 414 tries = append(tries, Try{Hash: hash, CreateTS:
create_ts.Format("2006-02-01")}) |
| 415 } |
| 416 } |
| 417 var code string |
| 418 if len(tries) == 0 { |
| 419 code = DEFAULT_SAMPLE |
| 420 } else { |
| 421 code = getCode(tries[len(tries)-1].Hash) |
| 422 } |
| 423 // If len(tries) == 0 then return the 'default code', otherwise
include the most recent code. |
| 424 // We don't worry about existence of a workspace with this name
since trying to |
| 425 // add a new try will fail. |
| 426 |
| 427 if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, C
ode: code, Name: name}); err != nil { |
| 428 log.Printf("ERROR: Failed to expand template: %q\n", err
) |
| 429 } |
| 430 // Update POST to / so that it takes a JSON document that includ
es not only |
| 431 // the code but an optional 'name' parameter. If the name is pre
sent the |
| 432 // POST handler should also add a row to the workspacetry table. |
| 433 } else if r.Method == "POST" { |
| 434 // Create a name and try to insert it into the db. |
| 435 // Redirect to /w/<name> |
| 436 if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", "
autumn-breeze-123"); err != nil { |
| 437 log.Printf("ERROR: Failed to insert workspace into datab
ase: %q\n", err) |
| 438 } |
| 439 name, err := newWorkspace() |
| 440 if err != nil { |
| 441 http.Error(w, "Failed to create a new workspace.", 500) |
| 442 return |
| 443 } |
| 444 http.Redirect(w, r, "/w/"+name, 302) |
| 445 } |
| 446 } |
| 447 |
297 // hasPreProcessor returns true if any line in the code begins with a # char. | 448 // hasPreProcessor returns true if any line in the code begins with a # char. |
298 func hasPreProcessor(code string) bool { | 449 func hasPreProcessor(code string) bool { |
299 lines := strings.Split(code, "\n") | 450 lines := strings.Split(code, "\n") |
300 for _, s := range lines { | 451 for _, s := range lines { |
301 if strings.HasPrefix(strings.TrimSpace(s), "#") { | 452 if strings.HasPrefix(strings.TrimSpace(s), "#") { |
302 return true | 453 return true |
303 } | 454 } |
304 } | 455 } |
305 return false | 456 return false |
306 } | 457 } |
307 | 458 |
| 459 type TryRequest struct { |
| 460 Code string `json:"code"` |
| 461 Name string `json:"name"` |
| 462 } |
| 463 |
308 // mainHandler handles the GET and POST of the main page. | 464 // mainHandler handles the GET and POST of the main page. |
309 func mainHandler(w http.ResponseWriter, r *http.Request) { | 465 func mainHandler(w http.ResponseWriter, r *http.Request) { |
310 log.Printf("Main Handler: %q\n", r.URL.Path) | 466 log.Printf("Main Handler: %q\n", r.URL.Path) |
311 if r.Method == "GET" { | 467 if r.Method == "GET" { |
312 code := DEFAULT_SAMPLE | 468 code := DEFAULT_SAMPLE |
313 match := directLink.FindStringSubmatch(r.URL.Path) | 469 match := directLink.FindStringSubmatch(r.URL.Path) |
314 if len(match) == 2 && r.URL.Path != "/" { | 470 if len(match) == 2 && r.URL.Path != "/" { |
315 hash := match[1] | 471 hash := match[1] |
316 if db == nil { | 472 if db == nil { |
317 http.NotFound(w, r) | 473 http.NotFound(w, r) |
(...skipping 15 matching lines...) Expand all Loading... |
333 n, err := buf.ReadFrom(r.Body) | 489 n, err := buf.ReadFrom(r.Body) |
334 if err != nil { | 490 if err != nil { |
335 reportError(w, r, err, "Failed to read a request body.") | 491 reportError(w, r, err, "Failed to read a request body.") |
336 return | 492 return |
337 } | 493 } |
338 if n == MAX_TRY_SIZE { | 494 if n == MAX_TRY_SIZE { |
339 err := fmt.Errorf("Code length equal to, or exceeded, %d
", MAX_TRY_SIZE) | 495 err := fmt.Errorf("Code length equal to, or exceeded, %d
", MAX_TRY_SIZE) |
340 reportError(w, r, err, "Code too large.") | 496 reportError(w, r, err, "Code too large.") |
341 return | 497 return |
342 } | 498 } |
343 » » code := string(buf.Bytes()) | 499 » » request := TryRequest{} |
344 » » if hasPreProcessor(code) { | 500 » » if err := json.Unmarshal(buf.Bytes(), &request); err != nil { |
| 501 » » » reportError(w, r, err, "Coulnd't decode JSON.") |
| 502 » » » return |
| 503 » » } |
| 504 » » if hasPreProcessor(request.Code) { |
345 err := fmt.Errorf("Found preprocessor macro in code.") | 505 err := fmt.Errorf("Found preprocessor macro in code.") |
346 reportError(w, r, err, "Preprocessor macros aren't allow
ed.") | 506 reportError(w, r, err, "Preprocessor macros aren't allow
ed.") |
347 return | 507 return |
348 } | 508 } |
349 » » hash, err := expandCode(LineNumbers(code)) | 509 » » hash, err := expandCode(LineNumbers(request.Code)) |
350 if err != nil { | 510 if err != nil { |
351 reportError(w, r, err, "Failed to write the code to comp
ile.") | 511 reportError(w, r, err, "Failed to write the code to comp
ile.") |
352 return | 512 return |
353 } | 513 } |
354 » » writeToDatabase(hash, code) | 514 » » writeToDatabase(hash, request.Code, request.Name) |
355 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), t
rue) | 515 message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), t
rue) |
356 if err != nil { | 516 if err != nil { |
357 reportError(w, r, err, "Failed to compile the code:\n"+m
essage) | 517 reportError(w, r, err, "Failed to compile the code:\n"+m
essage) |
358 return | 518 return |
359 } | 519 } |
360 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true) | 520 linkMessage, err := doCmd(fmt.Sprintf(LINK, hash, hash), true) |
361 if err != nil { | 521 if err != nil { |
362 reportError(w, r, err, "Failed to link the code:\n"+link
Message) | 522 reportError(w, r, err, "Failed to link the code:\n"+link
Message) |
363 return | 523 return |
364 } | 524 } |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
396 reportError(w, r, err, "Failed to serialize a response."
) | 556 reportError(w, r, err, "Failed to serialize a response."
) |
397 return | 557 return |
398 } | 558 } |
399 w.Write(resp) | 559 w.Write(resp) |
400 } | 560 } |
401 } | 561 } |
402 | 562 |
403 func main() { | 563 func main() { |
404 flag.Parse() | 564 flag.Parse() |
405 http.HandleFunc("/i/", imageHandler) | 565 http.HandleFunc("/i/", imageHandler) |
| 566 http.HandleFunc("/w/", workspaceHandler) |
406 http.HandleFunc("/recent/", recentHandler) | 567 http.HandleFunc("/recent/", recentHandler) |
407 http.HandleFunc("/css/", cssHandler) | 568 http.HandleFunc("/css/", cssHandler) |
408 http.HandleFunc("/", mainHandler) | 569 http.HandleFunc("/", mainHandler) |
409 log.Fatal(http.ListenAndServe(*port, nil)) | 570 log.Fatal(http.ListenAndServe(*port, nil)) |
410 } | 571 } |
OLD | NEW |