igar/main.go

446 lines
9.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"html/template"
"crypto/sha512"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"sync"
"time"
"encoding/hex"
"mime"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/gorilla/feeds"
"github.com/google/uuid"
//"github.com/labstack/echo/middleware"
)
type (
JSTime time.Time
Media struct {
Type string `json:"type"`
File string `json:"file"`
Height int `json:"height"`
Width int `json:"width"`
}
Entry struct {
Date JSTime `json:"date"`
Description string `json:"description"`
Post string `json:"post"`
Likes int `json:"likes"`
Tags []string `json:"tags"`
Media []Media `json:"media"`
Prev *Entry `json:"-"`
Next *Entry `json:"-"`
// Users []string
}
Response struct {
Ok bool
Message string
}
TemplateRenderer struct {
template *template.Template
}
)
type (
entryMap struct {
filename string
modtime time.Time
mapped map[string]*Entry
sorted []*Entry
mu sync.Mutex
}
entryList []*Entry
)
const (
jsTimeLite = "2006-01-02"
jsTimeLayout = "2006-01-02 15:04:05"
adminUsername = "danolo"
adminPassword = "bd4cad796950f50352225de3c773d8f3c39622bc17f34ad661eabe615cdf6d32751c5751e0648dc17d890f40330018334a2ae899878f200f6dc80121ddb70cc9"
)
var (
list *entryMap = &entryMap{
filename: "img/list.json",
modtime: time.Unix(0, 0),
}
)
// FUNCTIONS
func genName() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
// JSTime
func (ct *JSTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), `"`)
nt, err := time.Parse(jsTimeLayout, s)
*ct = JSTime(nt)
return
}
func (ct JSTime) MarshalJSON() ([]byte, error) {
return []byte(ct.String()), nil
}
func (ct JSTime) String() string {
t := time.Time(ct)
return fmt.Sprintf("%q", t.Format(jsTimeLayout))
}
func (ct JSTime) Unix() int64 {
return time.Time(ct).Unix()
}
// Entry
func (entry Entry) Count() map[string]int {
count := make(map[string]int)
for _, media := range entry.Media {
count[media.Type] += 1
}
return count
}
func (entry Entry) Len() int { return len(entry.Media) }
// TemplateRenderer
func (renderer *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return renderer.template.ExecuteTemplate(w, name, data)
}
// entryMap
func (list *entryMap) Len() int { return len(list.mapped) }
func (list *entryMap) modified() (mod bool) {
info, err := os.Stat(list.filename)
if err == nil {
mod = list.modtime.Before(info.ModTime())
list.modtime = info.ModTime()
} else {
mod = true
}
return
}
func (list *entryMap) write() (err error) {
data, err := json.MarshalIndent(list.mapped, "", " ")
if err == nil {
err = ioutil.WriteFile(list.filename, data, 0644)
}
if err == nil {
list.modtime = time.Now()
}
return
}
func (list *entryMap) read() (err error) {
if list.modified() {
if data, err := ioutil.ReadFile(list.filename); err == nil {
// read map
list.mapped = make(map[string]*Entry)
err = json.Unmarshal(data, &list.mapped)
// sort list
list.sorted = make([]*Entry, 0, len(list.mapped))
for _, entry:= range list.mapped {
list.sorted = append(list.sorted, entry)
}
sort.Sort(entryList(list.sorted))
// assign prev/next
for index, value := range list.sorted {
if index-1 >= 0 {
value.Next = list.sorted[index-1]
}
if index+1 < len(list.sorted) {
value.Prev = list.sorted[index+1]
}
}
}
}
return
}
func (list *entryMap) getSortedEntries() entryList {
list.mu.Lock()
defer list.mu.Unlock()
list.read()
return entryList(list.sorted)
}
func (list *entryMap) readSliceList(oval int, nval int) (result entryList, err error) {
if nval <= 0 {
nval = list.Len()
}
if oval <= 0 {
oval = 0
}
if err == nil {
entries := list.getSortedEntries()
if oval >= list.Len() {
result = nil
} else if oval+nval >= list.Len() {
result = entries[oval:]
} else {
result = entries[oval : oval+nval]
}
}
return
}
func (list *entryMap) addEntry(entry *Entry) (err error) {
list.mu.Lock()
defer list.mu.Unlock()
list.read()
if entry != nil {
list.mapped[entry.Post] = entry
err = list.write()
}
return
}
func (list *entryMap) getEntry(uuid string) (result *Entry, err error) {
list.mu.Lock()
defer list.mu.Unlock()
if err = list.read(); err == nil {
result = list.mapped[uuid]
}
return
}
func (list *entryMap) getFeed() (feed *feeds.Feed) {
feed = &feeds.Feed {
Title: "danoloan.es igar",
Link: &feeds.Link { Href: "https://danoloan.es/igar/" },
Description: "bitácora fotográfica de danoloan",
Author: &feeds.Author { Name: "danoloan", Email: "danolo@danoloan.es" },
}
feed.Items = make([]*feeds.Item, 0, list.Len())
for _, elem := range list.getSortedEntries() {
item := &feeds.Item {
Title: elem.Description,
Link: &feeds.Link{ Href: fmt.Sprintf("https://danoloan.es/igar/view?uuid=%s", elem.Post) },
Description: fmt.Sprintf("<p>%s</p>\n<div>\n", elem.Description),
Created: time.Time(elem.Date),
}
for _, media := range elem.Media {
if media.Type == "GraphImage" {
// TODO ruta relativa al proxy
item.Description = fmt.Sprintf("%s\n\t<img src=/igar/img/%s/%s>", item.Description, elem.Post, media.File)
}
}
item.Description = fmt.Sprintf("%s\n</div>", item.Description)
feed.Items = append(feed.Items, item)
}
return
}
// entryList
func (list entryList) Len() int { return len(list) }
func (list entryList) Swap(i, j int) { list[i], list[j] = list[j], list[i] }
func (list entryList) Less(i, j int) bool {
return time.Time(list[i].Date).After(time.Time(list[j].Date))
}
// CONTROLLERS
func ListController(c echo.Context) (res error) {
var err error
var oval, nval int
oval, err = strconv.Atoi(c.QueryParam("o"))
if err != nil {
oval = 0
}
nval, err = strconv.Atoi(c.QueryParam("n"))
if err != nil {
nval = 0
}
slice, err := list.readSliceList(oval, nval)
if err == nil {
res = c.JSON(http.StatusOK, slice)
} else {
response := Response{false, "Error fetching song list"}
res = c.JSON(http.StatusInternalServerError, response)
}
return
}
func ViewController(c echo.Context) (err error) {
uuid := c.QueryParam("uuid")
entry, err := list.getEntry(uuid)
if err == nil {
err = c.Render(http.StatusOK, "view.html", entry)
}
if err != nil {
c.Logger().Error(err)
}
return
}
func RSSController(c echo.Context) (err error) {
if blob, err := list.getFeed().ToRss(); err == nil {
c.Blob(http.StatusOK, "application/xml", []byte(blob))
}
return
}
func JSONController(c echo.Context) (err error) {
if blob, err := list.getFeed().ToJSON(); err == nil {
c.Blob(http.StatusOK, "application/json", []byte(blob))
}
return
}
func AtomController(c echo.Context) (err error) {
if blob, err := list.getFeed().ToAtom(); err == nil {
c.Blob(http.StatusOK, "application/xml", []byte(blob))
}
return
}
// TODO pura mierda
func PostController(c echo.Context) (err error) {
desc := c.FormValue("desc")
date := c.FormValue("date")
file, err := c.FormFile("file")
entry := &Entry {
Description: desc,
Post: genName(),
}
if date != "" {
time, _ := time.Parse(jsTimeLite, date)
entry.Date = JSTime(time)
} else {
entry.Date = JSTime(time.Now())
}
if err != nil {
return
}
media := Media {
Type: "GraphImage",
File: genName(),
}
extension, err := mime.ExtensionsByType(file.Header.Get("Content-Type"))
if err == nil {
media.File = fmt.Sprintf("%s%s", media.File, extension[0])
}
entry.Media = append(entry.Media, media)
src, err := file.Open()
if err != nil {
return
}
defer src.Close()
postdir := fmt.Sprintf("img/%s", entry.Post)
filepath := fmt.Sprintf("%s/%s", postdir, media.File)
os.MkdirAll(postdir, os.ModePerm)
dst, err := os.Create(filepath)
if err != nil {
return
}
defer dst.Close()
list.addEntry(entry)
if _, err = io.Copy(dst, src); err != nil {
return
}
if err = exec.Command("node", "thumbs/make_thumb.js", postdir).Run(); err != nil {
c.Logger().Error(err.Error())
}
return c.String(http.StatusCreated, "")
}
func auth(username, password string, c echo.Context) (bool, error) {
hash := sha512.Sum512([]byte(password))
hashString := hex.EncodeToString(hash[:])
if username == adminUsername && hashString == adminPassword {
return true, nil
}
return false, nil
}
func main() {
e := echo.New()
e.Renderer = &TemplateRenderer{
template: template.Must(template.ParseGlob("templates/*.html")),
}
e.Static("/", "index.html")
e.GET("/list", ListController)
e.GET("/view", ViewController)
e.GET("/rss", RSSController)
e.GET("/json", JSONController)
e.GET("/atom", AtomController)
e.Static("/static", "static")
e.Static("/img", "img")
admin := e.Group("/admin", middleware.BasicAuth(auth))
admin.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "admin.html", struct { List entryList }{
List: list.getSortedEntries(),
})
})
admin.POST("/post", PostController)
//admin.DELETE("/post", DeleteController)
admin.Static("/static", "admin/static")
e.Logger.Fatal(e.Start(":40404"))
}