Got basis for web done

This commit is contained in:
Tim Shannon 2016-04-06 16:31:22 +00:00
parent acfa4ff7fe
commit 7d2fa0a6ef
11 changed files with 657 additions and 13 deletions

261
bindata.go Normal file
View File

@ -0,0 +1,261 @@
// Code generated by go-bindata.
// sources:
// web/css/pure-min.css
// web/index.html
// web/js/index.js
// web/js/ractive.min.js
// DO NOT EDIT!
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// bindataRead reads the given file from disk. It returns an error on failure.
func bindataRead(path, name string) ([]byte, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
err = fmt.Errorf("Error reading asset %s at %s: %v", name, path, err)
}
return buf, err
}
type asset struct {
bytes []byte
info os.FileInfo
}
// webCssPureMinCss reads file data from disk. It returns an error on failure.
func webCssPureMinCss() (*asset, error) {
path := "/home/tshannon/workspace/go/src/git.townsourced.com/ironsmith/web/css/pure-min.css"
name := "web/css/pure-min.css"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// webIndexHtml reads file data from disk. It returns an error on failure.
func webIndexHtml() (*asset, error) {
path := "/home/tshannon/workspace/go/src/git.townsourced.com/ironsmith/web/index.html"
name := "web/index.html"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// webJsIndexJs reads file data from disk. It returns an error on failure.
func webJsIndexJs() (*asset, error) {
path := "/home/tshannon/workspace/go/src/git.townsourced.com/ironsmith/web/js/index.js"
name := "web/js/index.js"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// webJsRactiveMinJs reads file data from disk. It returns an error on failure.
func webJsRactiveMinJs() (*asset, error) {
path := "/home/tshannon/workspace/go/src/git.townsourced.com/ironsmith/web/js/ractive.min.js"
name := "web/js/ractive.min.js"
bytes, err := bindataRead(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"web/css/pure-min.css": webCssPureMinCss,
"web/index.html": webIndexHtml,
"web/js/index.js": webJsIndexJs,
"web/js/ractive.min.js": webJsRactiveMinJs,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"web": &bintree{nil, map[string]*bintree{
"css": &bintree{nil, map[string]*bintree{
"pure-min.css": &bintree{webCssPureMinCss, map[string]*bintree{}},
}},
"index.html": &bintree{webIndexHtml, map[string]*bintree{}},
"js": &bintree{nil, map[string]*bintree{
"index.js": &bintree{webJsIndexJs, map[string]*bintree{}},
"ractive.min.js": &bintree{webJsRactiveMinJs, map[string]*bintree{}},
}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

View File

@ -32,12 +32,6 @@ func (p *Project) errHandled(err error) bool {
return true return true
} }
defer func() { defer func() {
err = p.ds.Close()
if err != nil {
log.Printf("Error closing the datastore for project %s: %s\n", p.id(), err)
}
p.ds = nil
//clean up version folder if it exists //clean up version folder if it exists
if p.version != "" { if p.version != "" {
@ -156,6 +150,11 @@ func (p *Project) load() {
p.fetch() p.fetch()
if p.errHandled(p.ds.Close()) {
return
}
p.ds = nil
//full cycle completed //full cycle completed
if p.poll > 0 { if p.poll > 0 {
@ -197,7 +196,7 @@ func (p *Project) fetch() {
return return
} }
if p.version == lVer { if p.version == "" || p.version == lVer {
// no new build clean up temp dir // no new build clean up temp dir
p.errHandled(os.RemoveAll(tempDir)) p.errHandled(os.RemoveAll(tempDir))
@ -302,9 +301,5 @@ func (p *Project) release() {
//build successfull, remove working dir //build successfull, remove working dir
p.errHandled(os.RemoveAll(p.workingDir())) p.errHandled(os.RemoveAll(p.workingDir()))
if p.errHandled(p.ds.Close()) {
return
}
vlog("Project: %s Version %s built, tested, and released successfully.\n", p.id(), p.version) vlog("Project: %s Version %s built, tested, and released successfully.\n", p.id(), p.version)
} }

139
error.go Normal file
View File

@ -0,0 +1,139 @@
// Copyright 2016 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
)
const (
acceptHTML = "text/html"
)
// Err404 is a standard 404 error response
var Err404 = errors.New("Resource not found")
func errHandled(err error, w http.ResponseWriter) bool {
if err == nil {
return false
}
var status, errMsg string
errMsg = err.Error()
switch err.(type) {
case *Fail:
status = statusFail
case *http.ProtocolError, *json.SyntaxError, *json.UnmarshalTypeError:
//Hardcoded external errors which can bubble up to the end users
// without exposing internal server information, make them failures
err = FailFromErr(err)
status = statusFail
errMsg = fmt.Sprintf("We had trouble parsing your input, please check your input and try again: %s", err)
default:
status = statusError
errMsg = "An internal server error has occurred"
}
if status == statusFail {
respondJsendCode(w, &JSend{
Status: status,
Message: errMsg,
Data: err.(*Fail).Data,
}, err.(*Fail).HTTPStatus)
} else {
respondJsend(w, &JSend{
Status: status,
Message: errMsg,
})
}
return true
}
// four04 is a standard 404 response if request header accepts text/html
// they'll get a 404 page, otherwise a json response
func four04(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "application/json")
response := &JSend{
Status: statusFail,
Message: "Resource not found",
Data: r.URL.String(),
}
w.WriteHeader(http.StatusNotFound)
result, err := json.Marshal(response)
if err != nil {
log.Printf("Error marshalling 404 response: %s", err)
return
}
_, err = w.Write(result)
if err != nil {
log.Printf("Error in four04: %s", err)
}
}
// Fail is an error whose contents can be exposed to the client and is usually the result
// of incorrect client input
type Fail struct {
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
HTTPStatus int `json:"-"` //gets set in the error response
}
func (f *Fail) Error() string {
return f.Message
}
// NewFail creates a new failure, data is optional
func NewFail(message string, data ...interface{}) error {
return &Fail{
Message: message,
Data: data,
HTTPStatus: 0,
}
}
// FailFromErr returns a new failure based on the passed in error, data is optional
// if passed in error is nil, then nil is returned
func FailFromErr(err error, data ...interface{}) error {
if err == nil {
return nil
}
return NewFail(err.Error(), data...)
}
// IsEqual tests whether an error is equal to another error / failure
func (f *Fail) IsEqual(err error) bool {
if err == nil {
return false
}
return err.Error() == f.Error()
}
// IsFail tests whether the passed in error is a failure
func IsFail(err error) bool {
if err == nil {
return false
}
switch err.(type) {
case *Fail:
return true
default:
return false
}
}

100
json.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright 2016 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
)
const (
statusSuccess = "success"
statusError = "error"
statusFail = "fail"
)
const maxJSONSize = 1 << 20 //10MB
var errInputTooLarge = &Fail{
Message: "Input size is too large, please check your input and try again",
HTTPStatus: http.StatusRequestEntityTooLarge,
}
// JSend is the standard format for a response from townsourced
type JSend struct {
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Failures []error `json:"failures,omitempty"`
More bool `json:"more,omitempty"` // more data exists for this request
}
//respondJsend marshalls the input into a json byte array
// and writes it to the reponse with appropriate header
func respondJsend(w http.ResponseWriter, response *JSend) {
respondJsendCode(w, response, 0)
}
// respondJsendCode is the same as respondJSend, but lets you specify a status code
func respondJsendCode(w http.ResponseWriter, response *JSend, statusCode int) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "application/json")
if len(response.Failures) > 0 && response.Message == "" {
response.Message = "One or more item has failed. Check the individual failures for details."
}
result, err := json.MarshalIndent(response, "", " ")
if err != nil {
log.Printf("Error marshalling response: %s", err)
result, _ = json.Marshal(&JSend{
Status: statusError,
Message: "An internal error occurred, and we'll look into it.",
})
}
if statusCode <= 0 {
switch response.Status {
case statusFail:
w.WriteHeader(http.StatusBadRequest)
case statusError:
w.WriteHeader(http.StatusInternalServerError)
}
//default is status 200
} else {
w.WriteHeader(statusCode)
}
_, err = w.Write(result)
if err != nil {
log.Printf("Error writing jsend response: %s", err)
}
}
func parseInput(r *http.Request, result interface{}) error {
lr := &io.LimitedReader{R: r.Body, N: maxJSONSize + 1}
buff, err := ioutil.ReadAll(lr)
if err != nil {
return err
}
if lr.N == 0 {
return errInputTooLarge
}
if len(buff) == 0 {
return nil
}
err = json.Unmarshal(buff, result)
if err != nil {
return err
}
return nil
}

View File

@ -197,7 +197,9 @@ func (p *projectList) closeAll() {
defer p.RUnlock() defer p.RUnlock()
for i := range p.data { for i := range p.data {
_ = p.data[i].ds.Close() if p.data[i].ds != nil {
_ = p.data[i].ds.Close()
}
} }
} }

View File

@ -4,7 +4,12 @@
package main package main
import "net/http" import (
"bytes"
"net/http"
"path"
"time"
)
var webRoot *http.ServeMux var webRoot *http.ServeMux
@ -71,7 +76,47 @@ func (m *methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
/*
Routes
/project/<project-id>/<version>/<stage>
/project/ - list all projects
/project/<project-id> - list all versions in a project, triggers new builds
/project/<project-id>/<version> - list combined output of all stages
/project/<project-id>/<version>/<stage. - list output of a given stage of a given version
*/
func routes() { func routes() {
webRoot = http.NewServeMux() webRoot = http.NewServeMux()
webRoot.Handle("/", &methodHandler{
get: rootGet,
})
webRoot.Handle("/project/", &methodHandler{
get: projectGet,
})
}
func rootGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
//send index.html
serveAsset(w, r, "web/index.html")
return
}
serveAsset(w, r, path.Join("web", r.URL.Path))
}
func serveAsset(w http.ResponseWriter, r *http.Request, asset string) {
data, err := Asset(asset)
if err != nil {
http.NotFound(w, r)
return
}
http.ServeContent(w, r, r.URL.Path, time.Time{}, bytes.NewReader(data))
} }

11
web/css/pure-min.css vendored Normal file

File diff suppressed because one or more lines are too long

23
web/index.html Normal file
View File

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Ironsmith - A simple, script driven continuous integration tool">
<title>Ironsmith - A simple, script driven continuous integration tool</title>
<link rel="stylesheet" href="/css/pure-min.css">
<style>
</style>
</head>
<body>
<script id="tMain" type="text/ractive">
</script>
<script src="/js/ractive.min.js"></script>
<script src="/js/index.js"></script>
</body>
</html>

19
web/js/index.js Normal file
View File

@ -0,0 +1,19 @@
// Copyright 2016 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/* jshint strict: true */
(function() {
"use strict";
var r = new Ractive({
el: "body",
template: "#tMain",
data: function() {
return {
};
},
});
})();

9
web/js/ractive.min.js vendored Normal file

File diff suppressed because one or more lines are too long

40
webProject.go Normal file
View File

@ -0,0 +1,40 @@
// Copyright 2016 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"fmt"
"net/http"
"net/url"
"strings"
)
// /project/<project-id>/<version>/<stage>
func splitPath(path string) (project, version, stage string) {
s := strings.Split(path, "/")
if len(s) < 3 {
return
}
project, _ = url.QueryUnescape(s[2])
if len(s) < 4 {
return
}
version, _ = url.QueryUnescape(s[3])
if len(s) < 5 {
return
}
stage, _ = url.QueryUnescape(s[4])
return
}
func projectGet(w http.ResponseWriter, r *http.Request) {
prj, ver, stg := splitPath(r.URL.Path)
fmt.Printf("Project: %s Version: %s Stage: %s\n", prj, ver, stg)
}