446 lines
9.1 KiB
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"))
|
|
}
|