diff --git a/main.go b/main.go index e153ae39f06042a4a80a7a7efd64e3c7e7b4c1ef..8e3ff474ab63667e5bbcda75de23b853d3a600dc 100644 --- a/main.go +++ b/main.go @@ -1,496 +1,42 @@ package main import ( - "crypto/rand" "database/sql" - "encoding/base64" - "encoding/json" - "fmt" "github.com/go-redis/redis" _ "github.com/lib/pq" - "html/template" - "io" - "mime/multipart" "net/http" - "os" - "path/filepath" - "time" - "path" ) -func writeBody(reader io.ReadCloser, path string) error { - out, err := os.Create(path) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, reader) - if err != nil { - return err - } - return out.Close() -} - -func detectMimeType(path string) (string, error) { - file, err := os.Open(path) - if err != nil { - return "", err - } - - buffer := make([]byte, 512) - _, err = file.Read(buffer) - if err != nil { - return "", err - } - - return http.DetectContentType(buffer), nil -} - -func generateId() string { - buffer := make([]byte, 4) - rand.Read(buffer) - - return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buffer) -} - -func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileHeader) (Image, error) { - id := generateId() - path := filepath.Join(config.SourceFolder, id) - - err := writeBody(body, path) - if err != nil { - return Image{}, err - } - - mimeType, err := detectMimeType(path) - if err != nil { - return Image{}, err - } - - image := Image{ - Id: id, - OriginalName: filepath.Base(fileHeader.Filename), - CreatedAt: time.Now(), - MimeType: mimeType, - } - return image, nil -} - -type UserInfo struct { - Id string - Name string - Email string -} - -func parseUser(r *http.Request) UserInfo { - return UserInfo{ - r.Header.Get("X-Auth-Subject"), - r.Header.Get("X-Auth-Username"), - r.Header.Get("X-Auth-Email"), - } -} - -type UploadData struct { - User UserInfo - Results []Result -} - -func returnResult(w http.ResponseWriter, templateName string, data interface{}) error { - pageTemplate, err := template.ParseFiles( - "templates/_base.html", - "templates/_header.html", - "templates/_navigation.html", - "templates/_footer.html", - fmt.Sprintf("templates/%s", templateName), - ) - if err != nil { - return err - } - - err = pageTemplate.Execute(w, data) - if err != nil { - return err - } - - return nil -} - func main() { config := NewConfigFromEnv() - client := redis.NewClient(&redis.Options{ - Addr: config.Redis.Address, - Password: config.Redis.Password, - }) - db, err := sql.Open(config.Database.Format, config.Database.Url) if err != nil { panic(err) } - imageServer := http.FileServer(http.Dir(config.TargetFolder)) - assetServer := http.FileServer(http.Dir("assets")) - - http.HandleFunc("/upload/", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - user := parseUser(r) - - err := r.ParseMultipartForm(32 << 20) - if err != nil { - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{{ - Success: false, - Errors: []string{err.Error()}, - }}, - }); err != nil { - panic(err) - } - } - - var images []Image - var ids []string - - m := r.MultipartForm - files := m.File["file"] - for _, header := range files { - file, err := header.Open() - if err != nil { - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{{ - Success: false, - Errors: []string{err.Error()}, - }}, - }); err != nil { - panic(err) - } - return - } - image, err := createImage(&config, file, header) - if err != nil { - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{{ - Success: false, - Errors: []string{err.Error()}, - }}, - }); err != nil { - panic(err) - } - return - } - - images = append(images, image) - ids = append(ids, image.Id) - } - - pubsub := client.Subscribe(config.ResultChannel) - waiting := make(map[string]bool) - for _, image := range images { - _, err = db.Exec("INSERT INTO images (id, owner, created_at, original_name, type) VALUES ($1, $2, $3, $4, $5)", image.Id, user.Id, image.CreatedAt, image.OriginalName, image.MimeType) - if err != nil { - panic(err) - } - - data, err := json.Marshal(image) - if err != nil { - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{{ - Success: false, - Errors: []string{err.Error()}, - }}, - }); err != nil { - panic(err) - } - return - } - - fmt.Printf("Created task %s at %d\n", image.Id, time.Now().Unix()) - client.RPush(fmt.Sprintf("queue:%s", config.ImageQueue), data) - fmt.Printf("Submitted task %s at %d\n", image.Id, time.Now().Unix()) - - waiting[image.Id] = true - } - - var results []Result - for len(waiting) != 0 { - message, err := pubsub.ReceiveMessage() - if err != nil { - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{{ - Success: false, - Errors: []string{err.Error()}, - }}, - }); err != nil { - panic(err) - } - return - } - - result := Result{} - err = json.Unmarshal([]byte(message.Payload), &result) - if err != nil { - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{{ - Success: false, - Errors: []string{err.Error()}, - }}, - }); err != nil { - panic(err) - } - return - } - - fmt.Printf("Returned task %s at %d\n", result.Id, time.Now().Unix()) - - if _, ok := waiting[result.Id]; ok { - delete(waiting, result.Id) - - results = append(results, result) - } - } - - if err = returnResult(w, "upload.html", UploadData{ - user, - results, - }); err != nil { - panic(err) - } - return - } else { - user := parseUser(r) - if err = returnResult(w, "upload.html", UploadData{ - user, - []Result{}, - }); err != nil { - panic(err) - } - } - }) - - http.Handle("/i/", http.StripPrefix("/i/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - user := parseUser(r) - - type ImageDetailData struct { - User UserInfo - Image Image - IsMine bool - } - - _, imageId := path.Split(r.URL.Path) - result, err := db.Query(` - SELECT - id, - owner, - coalesce(title, ''), - coalesce(description, ''), - coalesce(created_at, to_timestamp(0)), - coalesce(original_name, ''), - coalesce(type, '') - FROM images - WHERE id = $1 - `, imageId) - if err != nil { - panic(err) - } - - var info Image - if result.Next() { - var owner string - err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType) - if err != nil { - panic(err) - } - - switch r.PostFormValue("action") { - case "update": - _, err = db.Exec( - "UPDATE images SET title = $1, description = $2 WHERE id = $3 AND owner = $4", - r.PostFormValue("title"), - r.PostFormValue("description"), - info.Id, - user.Id, - ) - if err != nil { - panic(err) - } - return - case "delete": - _, err = db.Exec("DELETE FROM images WHERE id = $1 AND owner = $2", info.Id, user.Id) - if err != nil { - panic(err) - } - for _, definition := range config.Sizes { - os.Remove(path.Join(config.TargetFolder, fmt.Sprintf("%s%s", info.Id, definition.Suffix))) - } - return - default: - if err = returnResult(w, "image_detail.html", ImageDetailData{ - user, - info, - owner == user.Id, - }); err != nil { - panic(err) - } - return - } - } - - w.WriteHeader(http.StatusNotFound) - fmt.Fprint(w, "Image not found") - }))) - - http.Handle("/a/", http.StripPrefix("/a/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := parseUser(r) - - type AlbumImage struct { - Id string - Title string - Description string - Position int - } - - type Album struct { - Id string - Title string - Description string - CreatedAt time.Time - Images []AlbumImage - } - - type AlbumDetailData struct { - User UserInfo - Album Album - IsMine bool - } - - _, albumId := path.Split(r.URL.Path) - result, err := db.Query(` - SELECT - id, - owner, - coalesce(title, ''), - coalesce(description, ''), - coalesce(created_at, to_timestamp(0)) - FROM albums - WHERE id = $1 - `, albumId) - if err != nil { - panic(err) - } - - var info Album - if result.Next() { - var owner string - err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt) - if err != nil { - panic(err) - } - - result, err := db.Query(` - SELECT - image, - title, - description, - position - FROM album_images - WHERE album = $1 - ORDER BY position ASC - `, albumId) - if err != nil { - panic(err) - } - - for result.Next() { - var image AlbumImage - err := result.Scan(&image.Id, &owner, &image.Title, &image.Description, &image.Position) - if err != nil { - panic(err) - } - - info.Images = append(info.Images, image) - } - - if err = returnResult(w, "album_detail.html", AlbumDetailData{ - user, - info, - owner == user.Id, - }); err != nil { - panic(err) - } - - return - } - - w.WriteHeader(http.StatusNotFound) - fmt.Fprint(w, "Image not found") - }))) - - http.HandleFunc("/me/images/", func(w http.ResponseWriter, r *http.Request) { - user := parseUser(r) - - type ImageListData struct { - User UserInfo - Images []Image - } - - result, err := db.Query(` - SELECT - id, - coalesce(title, ''), - coalesce(description, ''), - coalesce(created_at, to_timestamp(0)), - coalesce(original_name, ''), - coalesce(type, '') - FROM images - WHERE owner = $1 - `, user.Id) - if err != nil { - panic(err) - } - - var images []Image - for result.Next() { - var info Image - err := result.Scan(&info.Id, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType) - if err != nil { - panic(err) - } - images = append(images, info) - } + pageContext := PageContext{ + &config, + redis.NewClient(&redis.Options{ + Addr: config.Redis.Address, + Password: config.Redis.Password, + }), + db, + http.FileServer(http.Dir(config.TargetFolder)), + http.FileServer(http.Dir("assets")), + } - if err = returnResult(w, "me_images.html", ImageListData{ - user, - images, - }); err != nil { - panic(err) - } - }) + http.Handle("/upload/", pageUpload(pageContext)) - http.Handle("/assets/", http.StripPrefix("/assets/", assetServer)) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - user := parseUser(r) + http.Handle("/i/", http.StripPrefix("/i/", pageImageDetail(pageContext))) + http.Handle("/me/i/", http.StripPrefix("/me/i/", pageImageDetail(pageContext))) - type IndexData struct { - User UserInfo - } + http.Handle("/a/", http.StripPrefix("/a/", pageAlbumDetail(pageContext))) + http.Handle("/me/a/", http.StripPrefix("/me/a/", pageAlbumDetail(pageContext))) - if err = returnResult(w, "index.html", IndexData{ - user, - }); err != nil { - panic(err) - } - } else { - imageServer.ServeHTTP(w, r) - } - }) + http.Handle("/me/images/", pageImageList(pageContext)) + http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets")))) + http.Handle("/", pageIndex(pageContext)) err = http.ListenAndServe(":8080", nil) if err != nil { diff --git a/page_album_detail.go b/page_album_detail.go new file mode 100644 index 0000000000000000000000000000000000000000..a3050c9262c1b6c95df7684e0080df0585b33bf5 --- /dev/null +++ b/page_album_detail.go @@ -0,0 +1,81 @@ +package main + +import ( + "net/http" + "fmt" + "time" + "path" +) + +type AlbumDetailData struct { + User UserInfo + Album Album + IsMine bool +} + +func pageAlbumDetail(ctx PageContext) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := parseUser(r) + + _, albumId := path.Split(r.URL.Path) + result, err := ctx.Database.Query(` + SELECT + id, + owner, + coalesce(title, ''), + coalesce(description, ''), + coalesce(created_at, to_timestamp(0)) + FROM albums + WHERE id = $1 + `, albumId) + if err != nil { + panic(err) + } + + var info Album + if result.Next() { + var owner string + err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt) + if err != nil { + panic(err) + } + + result, err := ctx.Database.Query(` + SELECT + image, + title, + description, + position + FROM album_images + WHERE album = $1 + ORDER BY position ASC + `, albumId) + if err != nil { + panic(err) + } + + for result.Next() { + var image AlbumImage + err := result.Scan(&image.Id, &owner, &image.Title, &image.Description, &image.Position) + if err != nil { + panic(err) + } + + info.Images = append(info.Images, image) + } + + if err = formatTemplate(w, "album_detail.html", AlbumDetailData{ + user, + info, + owner == user.Id, + }); err != nil { + panic(err) + } + + return + } + + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, "Album not found") + }) +} diff --git a/page_album_list.go b/page_album_list.go new file mode 100644 index 0000000000000000000000000000000000000000..62d66ddad5020ae5ff1f8d517364d77e6c5e44c2 --- /dev/null +++ b/page_album_list.go @@ -0,0 +1,10 @@ +package main + +import ( + "net/http" +) + +func pageAlbumList(ctx PageContext) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }) +} diff --git a/page_image_detail.go b/page_image_detail.go new file mode 100644 index 0000000000000000000000000000000000000000..0ed2fa1c88e2557c96b7eb9a01de95d420887da0 --- /dev/null +++ b/page_image_detail.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + _ "github.com/lib/pq" + "net/http" + "os" + "path" +) + +type ImageDetailData struct { + User UserInfo + Image Image + IsMine bool +} + +func pageImageDetail(ctx PageContext) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := parseUser(r) + + _, imageId := path.Split(r.URL.Path) + result, err := ctx.Database.Query(` + SELECT + id, + owner, + coalesce(title, ''), + coalesce(description, ''), + coalesce(created_at, to_timestamp(0)), + coalesce(original_name, ''), + coalesce(type, '') + FROM images + WHERE id = $1 + `, imageId) + if err != nil { + panic(err) + } + + var info Image + if result.Next() { + var owner string + err := result.Scan(&info.Id, &owner, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType) + if err != nil { + panic(err) + } + + switch r.PostFormValue("action") { + case "update": + _, err = ctx.Database.Exec( + "UPDATE images SET title = $1, description = $2 WHERE id = $3 AND owner = $4", + r.PostFormValue("title"), + r.PostFormValue("description"), + info.Id, + user.Id, + ) + if err != nil { + panic(err) + } + return + case "delete": + _, err = ctx.Database.Exec("DELETE FROM images WHERE id = $1 AND owner = $2", info.Id, user.Id) + if err != nil { + panic(err) + } + for _, definition := range ctx.Config.Sizes { + os.Remove(path.Join(ctx.Config.TargetFolder, fmt.Sprintf("%s%s", info.Id, definition.Suffix))) + } + return + default: + if err = formatTemplate(w, "image_detail.html", ImageDetailData{ + user, + info, + owner == user.Id, + }); err != nil { + panic(err) + } + return + } + } + + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, "Image not found") + }) +} diff --git a/page_image_list.go b/page_image_list.go new file mode 100644 index 0000000000000000000000000000000000000000..d3592d6308d224e8b17d003bf2c30aa0b8c32a8b --- /dev/null +++ b/page_image_list.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" +) + +type ImageListData struct { + User UserInfo + Images []Image +} + +func pageImageList(ctx PageContext) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := parseUser(r) + + result, err := ctx.Database.Query(` + SELECT + id, + coalesce(title, ''), + coalesce(description, ''), + coalesce(created_at, to_timestamp(0)), + coalesce(original_name, ''), + coalesce(type, '') + FROM images + WHERE owner = $1 + `, user.Id) + if err != nil { + panic(err) + } + + var images []Image + for result.Next() { + var info Image + err := result.Scan(&info.Id, &info.Title, &info.Description, &info.CreatedAt, &info.OriginalName, &info.MimeType) + if err != nil { + panic(err) + } + images = append(images, info) + } + + if err = formatTemplate(w, "image_list.html", ImageListData{ + user, + images, + }); err != nil { + panic(err) + } + }) +} diff --git a/page_index.go b/page_index.go new file mode 100644 index 0000000000000000000000000000000000000000..7586d20fcdff3f8480862b051ad57d5a55f2f918 --- /dev/null +++ b/page_index.go @@ -0,0 +1,25 @@ +package main + +import ( + "net/http" +) + +type IndexData struct { + User UserInfo +} + +func pageIndex(ctx PageContext) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + user := parseUser(r) + + if err := formatTemplate(w, "index.html", IndexData{ + user, + }); err != nil { + panic(err) + } + } else { + ctx.Images.ServeHTTP(w, r) + } + }) +} diff --git a/page_upload.go b/page_upload.go new file mode 100644 index 0000000000000000000000000000000000000000..b4acedde6d46a58da58145c20078c3ab98edeba2 --- /dev/null +++ b/page_upload.go @@ -0,0 +1,221 @@ +package main + +import ( + "net/http" + "fmt" + "time" + "encoding/json" + "io" + "mime/multipart" + "path/filepath" + "os" + "encoding/base64" + "crypto/rand" +) + +type UploadData struct { + User UserInfo + Results []Result +} + +func detectMimeType(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + + buffer := make([]byte, 512) + _, err = file.Read(buffer) + if err != nil { + return "", err + } + + return http.DetectContentType(buffer), nil +} + +func generateId() string { + buffer := make([]byte, 4) + rand.Read(buffer) + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buffer) +} + +func writeBody(reader io.ReadCloser, path string) error { + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, reader) + if err != nil { + return err + } + return out.Close() +} + +func createImage(config *Config, body io.ReadCloser, fileHeader *multipart.FileHeader) (Image, error) { + id := generateId() + path := filepath.Join(config.SourceFolder, id) + + err := writeBody(body, path) + if err != nil { + return Image{}, err + } + + mimeType, err := detectMimeType(path) + if err != nil { + return Image{}, err + } + + image := Image{ + Id: id, + OriginalName: filepath.Base(fileHeader.Filename), + CreatedAt: time.Now(), + MimeType: mimeType, + } + return image, nil +} + +func pageUpload(ctx PageContext) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + user := parseUser(r) + + err := r.ParseMultipartForm(32 << 20) + if err != nil { + if err = formatTemplate(w, "upload.html", UploadData{ + user, + []Result{{ + Success: false, + Errors: []string{err.Error()}, + }}, + }); err != nil { + panic(err) + } + } + + var images []Image + var ids []string + + m := r.MultipartForm + files := m.File["file"] + for _, header := range files { + file, err := header.Open() + if err != nil { + if err = formatTemplate(w, "upload.html", UploadData{ + user, + []Result{{ + Success: false, + Errors: []string{err.Error()}, + }}, + }); err != nil { + panic(err) + } + return + } + image, err := createImage(ctx.Config, file, header) + if err != nil { + if err = formatTemplate(w, "upload.html", UploadData{ + user, + []Result{{ + Success: false, + Errors: []string{err.Error()}, + }}, + }); err != nil { + panic(err) + } + return + } + + images = append(images, image) + ids = append(ids, image.Id) + } + + pubsub := ctx.Redis.Subscribe(ctx.Config.ResultChannel) + waiting := make(map[string]bool) + for _, image := range images { + _, err = ctx.Database.Exec("INSERT INTO images (id, owner, created_at, original_name, type) VALUES ($1, $2, $3, $4, $5)", image.Id, user.Id, image.CreatedAt, image.OriginalName, image.MimeType) + if err != nil { + panic(err) + } + + data, err := json.Marshal(image) + if err != nil { + if err = formatTemplate(w, "upload.html", UploadData{ + user, + []Result{{ + Success: false, + Errors: []string{err.Error()}, + }}, + }); err != nil { + panic(err) + } + return + } + + fmt.Printf("Created task %s at %d\n", image.Id, time.Now().Unix()) + ctx.Redis.RPush(fmt.Sprintf("queue:%s", ctx.Config.ImageQueue), data) + fmt.Printf("Submitted task %s at %d\n", image.Id, time.Now().Unix()) + + waiting[image.Id] = true + } + + var results []Result + for len(waiting) != 0 { + message, err := pubsub.ReceiveMessage() + if err != nil { + if err = formatTemplate(w, "upload.html", UploadData{ + user, + []Result{{ + Success: false, + Errors: []string{err.Error()}, + }}, + }); err != nil { + panic(err) + } + return + } + + result := Result{} + err = json.Unmarshal([]byte(message.Payload), &result) + if err != nil { + if err = formatTemplate(w, "upload.html", UploadData{ + user, + []Result{{ + Success: false, + Errors: []string{err.Error()}, + }}, + }); err != nil { + panic(err) + } + return + } + + fmt.Printf("Returned task %s at %d\n", result.Id, time.Now().Unix()) + + if _, ok := waiting[result.Id]; ok { + delete(waiting, result.Id) + + results = append(results, result) + } + } + + if err = formatTemplate(w, "upload.html", UploadData{ + user, + results, + }); err != nil { + panic(err) + } + return + } else { + user := parseUser(r) + if err := formatTemplate(w, "upload.html", UploadData{ + user, + []Result{}, + }); err != nil { + panic(err) + } + } + }) +} diff --git a/templates/me_images.html b/templates/image_list.html similarity index 100% rename from templates/me_images.html rename to templates/image_list.html diff --git a/util.go b/util.go new file mode 100644 index 0000000000000000000000000000000000000000..04d90064258df846fc367c8c70f62b1d8f28c6d4 --- /dev/null +++ b/util.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "net/http" + "html/template" + "time" + "database/sql" + "github.com/go-redis/redis" + "encoding/base64" + "io" + "mime/multipart" + "path/filepath" + "os" + "crypto/rand" +) + +type UserInfo struct { + Id string + Name string + Email string +} + +type PageContext struct { + Config *Config + Redis *redis.Client + Database *sql.DB + Images http.Handler + AssetServer http.Handler +} + +type AlbumImage struct { + Id string + Title string + Description string + Position int +} + +type Album struct { + Id string + Title string + Description string + CreatedAt time.Time + Images []AlbumImage +} + +func parseUser(r *http.Request) UserInfo { + return UserInfo{ + r.Header.Get("X-Auth-Subject"), + r.Header.Get("X-Auth-Username"), + r.Header.Get("X-Auth-Email"), + } +} + +func formatTemplate(w http.ResponseWriter, templateName string, data interface{}) error { + pageTemplate, err := template.ParseFiles( + "templates/_base.html", + "templates/_header.html", + "templates/_navigation.html", + "templates/_footer.html", + fmt.Sprintf("templates/%s", templateName), + ) + if err != nil { + return err + } + + err = pageTemplate.Execute(w, data) + if err != nil { + return err + } + + return nil +}