aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--cmd/web/main.go20
-rw-r--r--go.mod3
-rw-r--r--go.sum6
-rw-r--r--internal/db/redis/image.go21
-rw-r--r--internal/db/redis/redis.go2
-rw-r--r--internal/fs/fs.go18
-rw-r--r--internal/image/handler.go33
-rw-r--r--internal/image/image_test.go42
-rw-r--r--internal/image/model.go1
-rw-r--r--internal/image/repo.go1
-rw-r--r--internal/image/service.go46
-rw-r--r--static/css/style.css818
-rw-r--r--templates/pages/image.tmpl12
-rw-r--r--templates/parts/_layout.tmpl30
14 files changed, 1026 insertions, 27 deletions
diff --git a/cmd/web/main.go b/cmd/web/main.go
index 3f16dec..f215d1e 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -4,29 +4,43 @@ import (
"log"
"net/http"
"os"
+ "path"
"github.com/go-chi/chi"
"git.ofmax.li/iserv/internal/db/redis"
+ "git.ofmax.li/iserv/internal/fs"
"git.ofmax.li/iserv/internal/image"
+ "go.ofmax.li/tmpl"
)
func main() {
connPool := redis.CreatePool("localhost:6379")
db := redis.NewRedisImageRepo(connPool)
+ renderer, err := tmpl.NewHTMLTmpl("templates")
+ if err != nil {
+ log.Fatal(err)
+ }
+
storagePath, err := os.Getwd()
if err != nil {
log.Fatal("couldn't find directory to write images to")
}
- imageService := image.NewService(db, storagePath)
+ // Image
+ imageService := image.NewService(db, storagePath, renderer)
imageHandler := image.NewHandler(imageService)
+ imageFile := fs.NewHandler(storagePath)
+ // Static Files
+ staticFiles := fs.NewHandler(path.Join(storagePath, "static"))
r := chi.NewRouter()
- r.Get("/i", imageHandler.GetImage)
- r.Post("/i", imageHandler.PostImage)
+ r.Get("/i/{fileName}", imageHandler.GetImage)
+ r.Post("/u", imageHandler.PostImage)
+ r.Get("/f/*", imageFile)
+ r.Get("/static/*", staticFiles)
log.Print("starting imageserv")
log.Fatal(http.ListenAndServe(":8080", r))
}
diff --git a/go.mod b/go.mod
index e7eba4c..0209999 100644
--- a/go.mod
+++ b/go.mod
@@ -11,10 +11,13 @@ require (
github.com/gomodule/redigo v2.0.0+incompatible
github.com/matoous/go-nanoid v1.3.0
github.com/muyo/sno v1.1.0
+ github.com/pkg/errors v0.9.1 // indirect
github.com/rafaeljusto/redigomock v2.3.0+incompatible
github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/testify v1.5.1
gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1
+ go.ofmax.li/environ v0.1.0
+ go.ofmax.li/tmpl v0.1.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
google.golang.org/api v0.21.0 // indirect
)
diff --git a/go.sum b/go.sum
index 9a023b6..e8a75e7 100644
--- a/go.sum
+++ b/go.sum
@@ -53,6 +53,8 @@ github.com/matoous/go-nanoid v1.3.0 h1:ynznZVSo9t0E8BTYLZx9geceRYZr8yLIrkOv3C/CU
github.com/matoous/go-nanoid v1.3.0/go.mod h1:fvGBnhcQ+zcrB3qJIG32PAN11J/y1IYkGX2/VeHzuH0=
github.com/muyo/sno v1.1.0 h1:WLsHreJnNFtc29Z2grb9Y9A6nXLrDJS0KqpWZzTYGLg=
github.com/muyo/sno v1.1.0/go.mod h1:Hjr5WzLleYD7bJYvQGo5UK7hpE2kSvMbihc0bMWF2bU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -67,6 +69,10 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1 h1:Jjns+kL26DLZhyrxqBZHJn1ymX33jtfnmv2xufSXOaA=
gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1/go.mod h1:GPGILReyQ0kLhYL5BcbmvTUGCmHC+upQgUZBQMqvcDY=
+go.ofmax.li/environ v0.1.0 h1:O5FEy8lPef4BpdiMNvDJUH7tqQaZmjQvwpDVv6rqmBI=
+go.ofmax.li/environ v0.1.0/go.mod h1:MJzTQgo3011WbIjWpEDQiQg2p3dYdIiKGDFvMSOHTKA=
+go.ofmax.li/tmpl v0.1.0 h1:itGhtIes5ba/s/0x0i9x+e/6my6MDTkd9KUhwRr/cVc=
+go.ofmax.li/tmpl v0.1.0/go.mod h1:gzv8lp+KPxqP6f7FqoBdGl8UwXANmuLOXxyOaKNUaQg=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
diff --git a/internal/db/redis/image.go b/internal/db/redis/image.go
index 6814fb7..1f4e8fa 100644
--- a/internal/db/redis/image.go
+++ b/internal/db/redis/image.go
@@ -9,7 +9,7 @@ import (
"git.ofmax.li/iserv/internal/image"
)
-const V1FilePathFmt = "filepath:up/%s"
+const V1FilePathFmt = "v1imagepost:%s"
// ImageRepo deps. for storage
type ImageRepo struct {
@@ -23,13 +23,30 @@ func NewRedisImageRepo(conn *redis.Pool) *ImageRepo {
}
}
+func fileKey(filename, V1FilePathFmt string) string {
+ return fmt.Sprintf(V1FilePathFmt, filename)
+}
+
func (r *ImageRepo) AddNewFile(filename string, meta *image.PostMeta, timeout int) error {
conn := r.db.Get()
defer conn.Close()
- key := fmt.Sprintf(V1FilePathFmt, filename)
+ key := fileKey(filename, V1FilePathFmt)
_, err := conn.Do("HMSET", redis.Args{}.Add(key).AddFlat(meta)...)
if err != nil {
log.Fatal(err)
}
return err
}
+
+func (r *ImageRepo) GetFile(fileUrl string) (*image.PostMeta, error) {
+ conn := r.db.Get()
+ defer conn.Close()
+ imageMeta := &image.PostMeta{}
+ key := fileKey(fileUrl, V1FilePathFmt)
+ res, err := redis.Values(conn.Do("HGETALL", key))
+ if err != nil {
+ return &image.PostMeta{}, err
+ }
+ err = redis.ScanStruct(res, imageMeta)
+ return imageMeta, err
+}
diff --git a/internal/db/redis/redis.go b/internal/db/redis/redis.go
index d769bb4..f9147be 100644
--- a/internal/db/redis/redis.go
+++ b/internal/db/redis/redis.go
@@ -5,7 +5,7 @@ import (
"github.com/gomodule/redigo/redis"
- "gitlab.com/grumps/environ"
+ "go.ofmax.li/environ"
)
var redisServer = environ.GetEnv("AUTH_REDIS", "localhost:6379")
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 0000000..7b2704b
--- /dev/null
+++ b/internal/fs/fs.go
@@ -0,0 +1,18 @@
+package fs
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/go-chi/chi"
+)
+
+func NewHandler(path string) http.HandlerFunc {
+ filesDir := http.Dir(path)
+ return func(w http.ResponseWriter, r *http.Request) {
+ rctx := chi.RouteContext(r.Context())
+ pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
+ fs := http.StripPrefix(pathPrefix, http.FileServer(filesDir))
+ fs.ServeHTTP(w, r)
+ }
+}
diff --git a/internal/image/handler.go b/internal/image/handler.go
index 8988a58..2db4c40 100644
--- a/internal/image/handler.go
+++ b/internal/image/handler.go
@@ -5,6 +5,8 @@ import (
"io/ioutil"
"log"
"net/http"
+
+ "github.com/go-chi/chi"
)
var fileTypes = map[string]string{
@@ -27,9 +29,22 @@ type imageHandler struct {
service Servicer
}
+// GetImage handler for returning an image
func (h *imageHandler) GetImage(w http.ResponseWriter, r *http.Request) {
- log.Print("serving image")
- http.ServeFile(w, r, "foo.jpg")
+ fileID := chi.URLParam(r, "fileName")
+ fileMeta, err := h.service.GetFile(fileID)
+ if err != nil {
+ w.WriteHeader(400)
+ log.Printf("error: %+v", err)
+ w.Write([]byte("WTF Incorrect Content Type"))
+ }
+ fileUrl := fmt.Sprintf("/f/%s", fileMeta.FilePath)
+ data := struct {
+ ImageUrl string
+ }{
+ fileUrl,
+ }
+ h.service.Render(w, "image.tmpl", data)
}
// PostImage handler for creating an image post
@@ -39,6 +54,7 @@ func (h *imageHandler) PostImage(w http.ResponseWriter, r *http.Request) {
file, handler, err := r.FormFile("file")
if err != nil {
log.Printf("%s", err)
+ return
}
defer file.Close()
fileBytes, err := ioutil.ReadAll(file)
@@ -63,9 +79,14 @@ func (h *imageHandler) PostImage(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Incorrect Content Type"))
return
}
- fileID, err := h.service.NewID()
- fileName := fmt.Sprintf("%s.%s", fileID, extension)
- h.service.AddFile(fileName, fileBytes)
+ fileName, fileID, err := h.service.AddFile(extension, fileType, fileBytes)
+ if err != nil {
+ log.Printf("failed to write file")
+ w.WriteHeader(500)
+ w.Write([]byte("An Internal Error Occured"))
+ return
+ }
w.WriteHeader(201)
- w.Write([]byte("ok"))
+ w.Write([]byte(fmt.Sprintf("%s %s", fileName, fileID)))
+ return
}
diff --git a/internal/image/image_test.go b/internal/image/image_test.go
index 4a1aea9..3425213 100644
--- a/internal/image/image_test.go
+++ b/internal/image/image_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io/ioutil"
+ "log"
"mime/multipart"
"net/http"
"net/http/httptest"
@@ -11,6 +12,7 @@ import (
"os"
"path"
"path/filepath"
+ "runtime"
"strings"
"testing"
@@ -20,8 +22,22 @@ import (
db "git.ofmax.li/iserv/internal/db/redis"
"git.ofmax.li/iserv/internal/image"
+ "go.ofmax.li/tmpl"
)
+var (
+ projectRoot string
+)
+
+func init() {
+ _, filename, _, _ := runtime.Caller(0)
+ projectRoot := path.Join(path.Dir(filename), "../..")
+ err := os.Chdir(projectRoot)
+ if err != nil {
+ panic(err)
+ }
+}
+
// from go lang src https://golang.org/src/mime/multipart/writer.go
// there seems to be no way to set the content type.
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
@@ -67,7 +83,7 @@ func prepareRequest(t *testing.T, filename, mimeType string) (*multipart.Writer,
return writer, body, err
}
-func TestImage(t *testing.T) {
+func TestPostImage(t *testing.T) {
// setup redis for tests
conn := redigomock.NewConn()
pool := &redis.Pool{
@@ -75,7 +91,7 @@ func TestImage(t *testing.T) {
Dial: func() (redis.Conn, error) { return conn, nil },
MaxIdle: 10,
}
- conn.Command("HMSET", redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData())
+ conn.Command("HMSET", redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData())
repo := db.NewRedisImageRepo(pool)
// setup image package
@@ -83,13 +99,20 @@ func TestImage(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ cwd, _ := os.Getwd()
+ base := filepath.Join(cwd, "templates")
+ renderer, err := tmpl.NewHTMLTmpl(base)
+ if err != nil {
+ log.Fatal(err)
+ }
defer os.RemoveAll(imageBuildDir)
- imageService := image.NewService(repo, imageBuildDir)
+ imageService := image.NewService(repo, imageBuildDir, renderer)
imageHandler := image.NewHandler(imageService)
handler := http.HandlerFunc(imageHandler.PostImage)
// prep test with png
- writer, body, err := prepareRequest(t, "t.png", "image/png")
+ testImagePath := filepath.Join(projectRoot, "internal/image/t.png")
+ writer, body, err := prepareRequest(t, testImagePath, "image/png")
if err != nil {
t.Fatal(err)
}
@@ -107,7 +130,8 @@ func TestImage(t *testing.T) {
}
// incorrect mime
- writer, body, err = prepareRequest(t, "t.pdf", "application/pdf")
+ testImagePDF := filepath.Join(projectRoot, "internal/image/t.pdf")
+ writer, body, err = prepareRequest(t, testImagePDF, "application/pdf")
if err != nil {
t.Fatal(err)
}
@@ -124,7 +148,7 @@ func TestImage(t *testing.T) {
}
// sneaky mimetype check
- writer, body, err = prepareRequest(t, "t.png", "image/jpeg")
+ writer, body, err = prepareRequest(t, testImagePath, "image/jpeg")
if err != nil {
t.Fatal(err)
}
@@ -141,7 +165,6 @@ func TestImage(t *testing.T) {
}
// test image write path && ext
- // TODO
pngs, err := filepath.Glob(filepath.Join(imageBuildDir))
if err != nil {
t.Fatal(err)
@@ -149,4 +172,9 @@ func TestImage(t *testing.T) {
if len(pngs) != 1 {
t.Error("expected only 1 image to be found")
}
+
+}
+
+func TestGetImage(t *testing.T) {
+
}
diff --git a/internal/image/model.go b/internal/image/model.go
index 002ad8a..0d1cd60 100644
--- a/internal/image/model.go
+++ b/internal/image/model.go
@@ -5,4 +5,5 @@ type PostMeta struct {
FilePath string `redis:"file_path"`
CreatedAt string `redis:"created_at"`
UserID string `redis:"user_id"`
+ MimeType string `redis:"mime_type"`
}
diff --git a/internal/image/repo.go b/internal/image/repo.go
index a65ad49..39f6a56 100644
--- a/internal/image/repo.go
+++ b/internal/image/repo.go
@@ -3,4 +3,5 @@ package image
// Repo storage interface
type Repo interface {
AddNewFile(filename string, meta *PostMeta, timeout int) error
+ GetFile(fileUrl string) (*PostMeta, error)
}
diff --git a/internal/image/service.go b/internal/image/service.go
index dabbfe7..f953bfa 100644
--- a/internal/image/service.go
+++ b/internal/image/service.go
@@ -1,24 +1,32 @@
package image
import (
+ "fmt"
"io/ioutil"
"log"
+ "net/http"
"path"
"time"
"github.com/matoous/go-nanoid"
+ "github.com/pkg/errors"
+
+ "go.ofmax.li/tmpl"
)
// Servicer image management
type Servicer interface {
NewID() (string, error)
- AddFile(filename string, fileBytes []byte) error
+ AddFile(extension, fileType string, fileBytes []byte) (string, string, error)
+ GetFile(fileUrl string) (*PostMeta, error)
+ Render(w http.ResponseWriter, templateName string, data interface{}) error
}
// NewService new image service
-func NewService(repo Repo, storagePath string) *Service {
+func NewService(repo Repo, storagePath string, renderer *tmpl.HTML) *Service {
return &Service{repo,
storagePath,
+ renderer,
}
}
@@ -26,6 +34,12 @@ func NewService(repo Repo, storagePath string) *Service {
type Service struct {
db Repo
storagePath string
+ tmpl *tmpl.HTML
+}
+
+// Render renders templates
+func (is *Service) Render(w http.ResponseWriter, templateName string, data interface{}) error {
+ return is.tmpl.Render(w, templateName, data)
}
// NewID create an uniqueish ID
@@ -34,22 +48,38 @@ func (is *Service) NewID() (string, error) {
}
// AddFile writes to disk, writes meta to db
-func (is *Service) AddFile(fileName string, fileBytes []byte) error {
+func (is *Service) AddFile(extension, fileType string, fileBytes []byte) (string, string, error) {
+ fileID, err := is.NewID()
+ if err != nil {
+ return "", "", errors.Wrap(err, "generated id for fileID failed")
+ }
+ fileName := fmt.Sprintf("%s.%s", fileID, extension)
filePath := path.Join(is.storagePath, fileName)
if err := ioutil.WriteFile(filePath, fileBytes, 0750); err != nil {
log.Fatal(err)
- return err
+ return "", "", errors.Wrap(err, "couldn't write image file")
+ }
+ postID, err := is.NewID()
+ if err != nil {
+ return "", "", errors.Wrap(err, "generating postid for uuid")
}
t := time.Now().UTC()
postMeta := &PostMeta{
FilePath: fileName,
CreatedAt: t.Format(time.RFC3339),
UserID: "1",
+ MimeType: fileType,
}
- is.db.AddNewFile(fileName, postMeta, 946080000)
- if err := is.db.AddNewFile(fileName, postMeta, 946080000); err != nil {
+ is.db.AddNewFile(postID, postMeta, 946080000)
+ if err := is.db.AddNewFile(postID, postMeta, 946080000); err != nil {
log.Fatal(err)
- return err
+ return "", "", errors.Wrap(err, "couldn't write to redis")
}
- return nil
+ return fileName, postID, nil
+}
+
+// GetFile fetch file from db interface
+func (is *Service) GetFile(fileUrl string) (*PostMeta, error) {
+ result, err := is.db.GetFile(fileUrl)
+ return result, err
}
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..9075829
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,818 @@
+/* The W3C Core Styles Copyright (c) 1998 W3C (mit, inria, Keio), All Rights
+Reserved. W3C liability, trademark, document use and software licensing rules
+apply. See http://www.w3.org/Consortium/Legal/ipr-notice.html
+
+This stylesheet has been served in a form to avoid known bugs in your user
+agent's css implementation. Copying and serving this stylesheet to other
+agents may lead to unexpected results. Please refer to its address rather than
+copy it. */
+
+
+
+
+
+ /* begin body type */ /* 1 - humanist sans a */
+
+body {
+ font-size: 1em;
+ font-weight: normal;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ font-family: Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .58;
+ }
+
+p, blockquote, ul, ol, dl, dd, li, dir, menu {
+ /* inherit from body */
+ }
+
+small {
+ font-size: .92em;
+ }
+
+big {
+ font-size: 1.17em;
+ }
+
+pre {
+ font-family: 'Monotype.com', Courier New, monospace;
+ }
+
+ol li {
+ list-style-type: decimal;
+ }
+
+ol ol li {
+ list-style-type: lower-alpha;
+ }
+
+ol ol ol li {
+ list-style-type: lower-roman;
+ }
+
+table, tbody, tr, td {
+ font-size: 1em;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ font-family: Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .58;
+ }
+
+ /* end body type */
+
+
+
+
+ /* begin header type */ /* 7 - humanist sans b */
+
+h1 {
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ font-size: 2em;
+ font-weight: 400;
+ font-style: normal;
+ text-decoration: none;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ }
+
+h2 {
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ font-size: 1.75em;
+ font-weight: 400;
+ font-style: normal;
+ text-decoration: none;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ }
+
+h3 {
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ font-size: 1.58em;
+ font-weight: 400;
+ font-style: normal;
+ text-decoration: none;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ }
+
+h4 {
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ font-size: 1.33em;
+ font-weight: 500;
+ font-style: normal;
+ text-decoration: none;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ }
+
+h5, dt {
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ font-size: 1.17em;
+ font-weight: 600;
+ font-style: normal;
+ text-decoration: none;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ }
+
+h6 {
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ font-size: 1em;
+ font-weight: 700;
+ font-style: normal;
+ text-decoration: none;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ }
+
+tfoot, thead {
+ font-size: 1em;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ }
+
+th {
+ vertical-align: baseline;
+ font-size: 1em;
+ font-weight: bold;
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ font-family: Trebuchet ms, Verdana, Myriad Web, Syntax, sans-serif;
+ font-size-adjust: .53;
+ }
+
+hr {
+ visibility: hidden;
+ }
+
+ /* end header type */
+
+
+
+
+ /* begin affordances */ /* 3 - mint reverse */
+
+a, address, blockquote, body, cite, code, dd, del, dfn,
+div, dl, dt, em, form, h1, h2, h3, h4, h5, h6, iframe, img, kbd,
+li, object, ol, p, q, samp, small, span, strong, sub, sup, ul, var,
+applet, big, center, dir, font, hr, menu, pre,
+abbr, acronym, bdo, button, fieldset, ins, label {
+ word-spacing: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ text-decoration: none;
+ border-color: #ccc;
+ border-style: none;
+ }
+
+body {
+ color: #D0FFD0;
+ background: #212121;
+ }
+
+em {
+ font-style: normal;
+ font-weight: bold;
+ color: #D0FFD0;
+ background: #212121;
+ }
+
+strong {
+ font-style: italic;
+ background: #212121;
+ font-weight: bold;
+ color: #D0FFD0;
+ }
+
+em strong, strong em {
+ text-transform: uppercase;
+ font-style: normal;
+ font-weight: bolder;
+ background: #212121;
+ color: #D0FFD0;
+ }
+
+b {
+ font-weight: bold;
+ }
+
+i {
+ font-style: italic;
+ }
+
+.warning {
+ text-transform: none;
+ font-style: normal;
+ font-weight: bolder;
+ background: yellow;
+ color: black;
+ }
+
+del {
+ text-decoration: line-through;
+ background: #900;
+ }
+
+ins {
+ text-decoration: none;
+ background: #060;
+ }
+
+var, cite, dfn, .note {
+ font-style: italic;
+ }
+
+address {
+ font-style: normal;
+ letter-spacing: .1em;
+ }
+
+acronym {
+ font-variant: small-caps;
+ letter-spacing: 0.1em;
+ }
+
+h1, h2, h3, h4, h5, h6, dt, th, thead, tfoot {
+ color: rgb(245,245,245);
+ background: #212121;
+ }
+
+hr {
+ color: #ccc;
+ }
+
+#colophon {
+ display: none;
+ }
+
+col, colgroup, table, tbody, td, tr {
+ color: #D0FFD0;
+ text-decoration: none;
+ border-color: #ccc;
+ border-style: none;
+ background: #212121;
+ }
+
+a:link {
+ text-decoration: none;
+ font-weight: bold;
+ color: #F60;
+ background: #212121;
+ }
+
+a:visited {
+ text-decoration: none;
+ font-weight: bold;
+ color: #C96;
+ background: #212121;
+ }
+
+a:active {
+ text-decoration: none;
+ font-weight: bold;
+ color: #ffc;
+ background: #212121;
+ }
+
+a:hover {
+ text-decoration: none;
+ color: #FC0;
+ background: #212121;
+ }
+
+a.offsite {
+ text-decoration: none;
+ font-weight: normal;
+ color: #F60;
+ background: #212121;
+ }
+
+ /* end affordances */
+
+
+
+
+ /* begin vertical */ /* 1 - pentatonic a */
+
+a, address, blockquote, cite, code, dd, del, dfn,
+div, dl, dt, em, form, h1, h2, h3, h4, h5, h6, iframe, img, kbd,
+li, object, ol, p, q, samp, small, span, strong, sub, sup, ul, var,
+applet, b, big, center, dir, font, hr, i, menu, pre, s, strike, tt,
+u, abbr, acronym, bdo, button, fieldset, ins, label {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ vertical-align: baseline;
+ }
+
+body {
+ line-height: 1.58em;
+ }
+
+div > p:first-child, body > p:first-child, td > p:first-child {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p, div + p, p.initial {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h1, h2, h3, h4, h5, h6 {
+ line-height: 1.33em;
+ }
+
+h1 {
+ margin-top: 1.33em;
+ margin-bottom: .33em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h2 {
+ margin-top: 1.75em;
+ margin-bottom: .33em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h3 {
+ margin-top: 1.58em;
+ margin-bottom: .33em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h4 {
+ margin-top: 1.33em;
+ margin-bottom: .33em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h5 {
+ margin-top: 1.17em;
+ margin-bottom: .33em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+h6 {
+ margin-top: 1em;
+ margin-bottom: .33em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+p {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+blockquote, fieldset, form, ul, ol, dl, dir, menu {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+dt {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+dd {
+ margin-top: 0;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+ul ul, ol ol, li address, li dl, li ol, li p, li ul, li dir, li hr, li menu, li pre, li h1, li h2, li h3, li h4, li h5, li h6, dd address, dd dl, dd ol, dd p, dd ul, dd dir, dd hr, dd menu, dd pre, dd h1, dd h2, dd h3, dd h4, dd h5, dd h6 {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+li {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+address {
+ margin-top: 1.58em;
+ margin-bottom: 1.58em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+pre {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ line-height: 1.25em;
+ }
+
+hr {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ height: 1px;
+ }
+
+div, center {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+.subhead {
+ margin-top: .75em;
+ margin-bottom: .75em;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+.stb {
+ margin-top: 2.17em;
+ margin-bottom: .75em;
+ padding-top: 2.17em;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ border-style: solid
+ }
+
+.mtb {
+ margin-top: 3.08em;
+ margin-bottom: .75em;
+ padding-top: 3.08em;
+ padding-bottom: 0;
+ border-top: .1em;
+ border-bottom: 0;
+ border-style: solid
+ }
+
+.ltb {
+ margin-top: 4.34em;
+ margin-bottom: .75em;
+ padding-top: 4.34em;
+ padding-bottom: 0;
+ border-top: .25em;
+ border-bottom: 0;
+ border-style: solid
+ }
+
+col, colgroup, table, tbody, td, tfoot, th, thead, tr {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+td, th {
+ line-height: 1.33em;
+ }
+
+ /* end vertical */
+
+
+
+
+ /* begin horizontal */ /* 1 - compact */
+
+a, address, blockquote, cite, code, dd, del, dfn,
+div, dl, dt, em, form, h1, h2, h3, h4, h5, h6, iframe, img, kbd,
+li, object, ol, p, q, samp, small, span, strong, sub, sup, ul, var,
+applet, b, big, center, dir, font, hr, i, menu, pre, s, strike, tt,
+u, abbr, acronym, bdo, button, fieldset, ins, label {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ float: none;
+ clear: none;
+ list-style-position: outside;
+ }
+
+address, blockquote, dl, fieldset, form, ol,
+p, ul, dir, hr, menu, pre {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+div > p:first-child, body > p:first-child, td > p:first-child {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ text-indent: 0;
+ }
+
+h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p, div + p, p.initial {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ text-indent: 0;
+ }
+
+h1 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+h2 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+h3 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+h4 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+h5 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+h6 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+p {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-indent: 0;
+ }
+
+blockquote {
+ margin-left: 1.58em;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+address {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+pre {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+li {
+ margin-left: 3.16em;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+dt {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+dd {
+ margin-left: 1.58em;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+ul ul, ol ol, li address, li dl, li ol, li p, li ul, li dir, li hr, li menu, li pre, li h1, li h2, li h3, li h4, li h5, li h6, dd address, dd dl, dd ol, dd p, dd ul, dd dir, dd hr, dd menu, dd pre, dd h1, dd h2, dd h3, dd h4, dd h5, dd h6 {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+hr {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ width: 100%;
+ }
+
+center {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ text-align: left;
+ }
+
+col, colgroup, table, tbody, td, tfoot, th, thead, tr {
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-left: 0;
+ border-right: 0;
+ float: none;
+ clear: none;
+ }
+
+table {
+ text-align: left;
+ }
+
+td {
+ text-align: left;
+ }
+
+th {
+ text-align: left;
+ }
+
+caption {
+ text-align: left;
+ }
+
+ /* end horizontal */
+
+
+
+
+ /* begin body box */ /* 5 - 'ninths symmetric' */
+
+body {
+ margin-top: 1.58em;
+ margin-left: 11%;
+ margin-right: 8%;
+ margin-bottom: 1.58em;
+ padding-top: 0;
+ padding-left: 0;
+ padding-right: 0;
+ padding-bottom: 0;
+ border-top: 0;
+ border-left: 0;
+ border-bottom: 0;
+ border-right: 0;
+ width: auto;
+ }
+ /* end body box */
+
+
+
+img {
+ max-width: 80%;
+}
diff --git a/templates/pages/image.tmpl b/templates/pages/image.tmpl
new file mode 100644
index 0000000..4c9b7b0
--- /dev/null
+++ b/templates/pages/image.tmpl
@@ -0,0 +1,12 @@
+{{ define "content" }}
+<body>
+ <div>
+ <h3>Image</h3>
+ </div>
+ <div>
+ <figure>
+ <img src="http://localhost:8080{{ .ImageUrl }}">
+ </figure>
+ </div>
+</body>
+{{ end }}
diff --git a/templates/parts/_layout.tmpl b/templates/parts/_layout.tmpl
new file mode 100644
index 0000000..4098a30
--- /dev/null
+++ b/templates/parts/_layout.tmpl
@@ -0,0 +1,30 @@
+{{ define "base" }}
+<!DOCTYPE html>
+<html lang="en">
+ <!-- Basic Page Needs
+ –––––––––––––––––––––––––––––––––––––––––––––––––– -->
+ <meta charset="utf-8">
+ <title>iserv - an image site</title>
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <!-- Mobile Specific Metas
+ –––––––––––––––––––––––––––––––––––––––––––––––––– -->
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <!-- FONT
+ <link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
+
+ –––––––––––––––––––––––––––––––––––––––––––––––––– -->
+ <!-- CSS
+ <link rel="stylesheet" href="css/normalize.css">
+ –––––––––––––––––––––––––––––––––––––––––––––––––– -->
+ <link rel="stylesheet" href="/static/css/style.css">
+
+ <!-- Favicon
+ –––––––––––––––––––––––––––––––––––––––––––––––––– -->
+ <!-- <link rel="icon" type="image/png" href="images/favicon.png"> -->
+
+</head>
+{{ template "content" . }}
+{{ end }}