comics/comics.go

509 lines
11 KiB
Go

package main
import (
"os"
"log"
"fmt"
"flag"
"errors"
"strconv"
"strings"
"math/rand"
"net/http"
"database/sql"
"html/template"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
var errlog *log.Logger = nil
var db *sql.DB = nil
var options Options = Options{}
var NoSuchComicErr Err = Err{msg: "no such comic found"}
const dbSquema string = `
CREATE TABLE IF NOT EXISTS comic (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
datetime DATETIME DEFAULT CURRENT_TIMESTAMP,
title CHAR(50),
image CHAR(200),
description TEXT,
tags TEXT
);`
type Options struct {
DBPath string
MediaPath string
TemplatesPath string
Address string
Port int
Publish bool
Title string
ImagePath string
RunManager bool
UploadSize int64
SiteURL string
SecretPath string
Username string
Password string
CreateUser bool
ManagerRoot string
}
type Comic struct {
ID int
DateTime string
Title string
Image string
Description string
Tags string
}
type Context struct {
Comics []struct{
No int
Title string
Date string
Image string
}
Comic *Comic
Current int
Previous int
Combo int
Next int
First int
Last int
Title string
UserName string
SiteURL string
Message string
}
type Err struct {
msg string
}
func (o * Options) Parse() error {
flag.StringVar(&o.DBPath, "d", "./db.sqlite", "Sets path to database file")
flag.StringVar(&o.MediaPath, "m", "./media/", "Sets path to media directory")
flag.StringVar(&o.TemplatesPath, "t", "./templates/", "Sets path to templates directory")
flag.StringVar(&o.Address, "a", "127.0.0.1", "Defines the address the web server will listen to")
flag.IntVar(&o.Port, "p", 8080, "Defines the port the web server will listen to")
flag.BoolVar(&o.Publish, "u", false, "Creates new commics. Needs both -i and -l")
flag.StringVar(&o.Title, "l", "", "Title for new comic")
flag.StringVar(&o.ImagePath, "i", "", "Image path for new comic")
flag.BoolVar(&o.RunManager, "r", false, "Runs the content manager instead of the site")
flag.Int64Var(&o.UploadSize, "z", 10, "Max upload size for posts")
flag.StringVar(&o.SiteURL, "0", "https://comics.blackram.works", "URL of the site once deployed")
flag.StringVar(&o.SecretPath, "s", "./secret.key", "Sets path to secret key file")
flag.StringVar(&o.Username, "w", "", "Username for the new user")
flag.StringVar(&o.Password, "q", "", "Password for the new user")
flag.BoolVar(&o.CreateUser, "c", false, "Creates a new user. Needs -w and -q")
flag.StringVar(&o.ManagerRoot, "o", "manager", "Manager root page")
flag.Parse()
if o.CreateUser {
o.RunManager = true
}
if _, err := os.Stat(o.TemplatesPath); errors.Is(err, os.ErrNotExist) {
log.Fatal(err)
}
if _, err := os.Stat(o.MediaPath); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(o.MediaPath, os.ModePerm)
if err != nil {
log.Fatal(err)
}
}
return nil
}
func (e * Err) Error() string {
return e.msg
}
func (e * Err) With(i string) Err {
return Err{msg:fmt.Sprintf("%s with %s", e.msg, i)}
}
func (c *Comic) readRow(db * sql.Rows) error {
return db.Scan(&c.ID, &c.DateTime, &c.Title, &c.Image,
&c.Description, &c.Tags)
}
func getComic(id int) (*Comic, error) {
id -= 1
c := new(Comic)
nferr := NoSuchComicErr.With(fmt.Sprintf("id \"%d\"", id))
if id < 0 {
return c, &nferr
}
comics, err := allComics()
if err != nil {
return c, err
}
if len(comics) <= id {
return c, &nferr
}
*c = comics[id]
return c, err
}
func readRows(rows *sql.Rows) ([]Comic, error) {
comics := []Comic{}
for rows.Next() {
c := Comic{}
err := c.readRow(rows)
if err != nil {
return comics, nil
}
comics = append(comics, c)
}
return comics, nil
}
func allComics() ([]Comic, error) {
rows, err := db.Query("SELECT * FROM comic ORDER BY datetime ASC")
if err != nil {
return nil, err
}
defer rows.Close()
return readRows(rows)
}
func executeTemplate(w http.ResponseWriter, name string, data interface{}) error {
t, err := template.ParseFiles(filepath.Join(options.TemplatesPath, fmt.Sprintf("%s.html", name)))
if err != nil {
return err
}
tmp := new(strings.Builder)
err = t.Execute(tmp, data)
if err != nil {
return err
}
_, err = w.Write([]byte(tmp.String()))
return err
}
func comicView(w http.ResponseWriter, r * http.Request) {
var err error
if len(r.URL.Path) > 1 && r.URL.Path[len(r.URL.Path)-1] == '/' {
r.URL.Path = r.URL.Path[len(r.URL.Path)-2:]
}
path := strings.TrimPrefix(r.URL.Path, "/")
if path == options.ManagerRoot {
http.Redirect(w, r, managerPath("/index"), http.StatusSeeOther)
return
}
if len(path) >= 5 {
path = path[:5]
}
defer func() {
if err != nil {
errlog.Println(err)
return404(w,r)
return
}
}()
c := &Comic{}
context := Context{
Comics: nil,
Comic: c,
Title: "Black Ram Comics",
}
i := 1
if len(path) > 0{
i, err = strconv.Atoi(path)
if err != nil {
return
}
}
context.Current = i
comics, err := allComics()
if err != nil {
return500(w,r)
err = nil
return
}
if len(comics) > 0 {
c, err = getComic(i)
if err != nil {
errlog.Println(err)
return404(w,r)
err = nil
return
}
u, err := c.getAuthor()
if err == nil {
context.UserName = u.Name
}
context.Comic = c
context.Title = fmt.Sprintf("Black Ram Comics: %s", c.Title)
} else {
context.Comic = nil
}
context.First = 1
context.Last = len(comics)
context.Previous = i
if i > 1 {
context.Previous = i - 1
}
context.Next = i
if i < (len(comics)) {
context.Next = i + 1
}
err = executeTemplate(w, "comic", &context)
}
func allView(w http.ResponseWriter, r * http.Request) {
var err error
defer func() {
if err != nil {
errlog.Println(err)
return500(w,r)
err = nil
return
}
} ()
context := Context{
Comics: nil,
Title: "All Issues",
}
comics, err := allComics()
if err != nil {
return
}
for i, comic := range comics {
context.Comics = append(context.Comics, struct {
No int
Title string
Date string
Image string
}{
Title: comic.Title,
No: i+1,
Date: comic.DateTime,
Image: comic.Image,
})
}
err = executeTemplate(w, "comic", &context)
}
func latestView(w http.ResponseWriter, r * http.Request) {
comics, err := allComics()
if err != nil {
return500(w, r)
}
if len(comics) < 1 {
return404(w, r)
} else {
r.URL.Path = fmt.Sprintf("/%d", len(comics)-1)
comicView(w, r)
}
}
func firstView(w http.ResponseWriter, r * http.Request) {
comics, err := allComics()
if err != nil {
return500(w, r)
}
if len(comics) < 1 {
return404(w, r)
} else {
r.URL.Path = fmt.Sprintf("/0")
comicView(w, r)
}
}
func randomView(w http.ResponseWriter, r * http.Request) {
comics, err := allComics()
if err != nil {
errlog.Println(err)
return500(w, r)
return
}
if len(comics) < 1 {
return404(w, r)
} else {
r.URL.Path = fmt.Sprintf("/%d", rand.Intn(len(comics))+1)
comicView(w, r)
}
}
// Taken from: https://gist.github.com/hoitomt/c0663af8c9443f2a8294
func logRequest(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
handler.ServeHTTP(w, r)
})
}
func returnPlainText(w http.ResponseWriter, t string) error {
w.Header().Set("Content-Type", "text/plain")
_, err := w.Write([]byte(t))
return err
}
func returnError(w http.ResponseWriter, r * http.Request, status int) {
var err error
msg := fmt.Sprintf("%d", status)
w.WriteHeader(status)
defer func() {
if err != nil {
returnPlainText(w, msg)
}
}()
t, err := template.ParseFiles(filepath.Join(options.TemplatesPath, "error.html"))
if err != nil {
return
}
c := struct {
Code string
Title string
}{
Code: msg,
Title: "BRC: " + msg,
}
err = t.Execute(w, &c)
}
func return401(w http.ResponseWriter, r * http.Request) {
returnError(w, r, http.StatusUnauthorized)
}
func return404(w http.ResponseWriter, r * http.Request) {
returnError(w, r, http.StatusNotFound)
}
func return500(w http.ResponseWriter, r * http.Request) {
returnError(w, r, http.StatusInternalServerError)
}
func newComic(title string, image string, description string, tags string) (int, error) {
res, err := db.Exec("INSERT INTO comic(title, image, description, tags) VALUES(?, ?, ?, ?);",
title, image, description, tags)
if err != nil {
return -1, nil
}
id := int64(0)
if id, err = res.LastInsertId(); err != nil {
return 0, err
}
return int(id), err
}
func deleteComic(id int) error {
_, err := db.Exec("DELETE FROM comic WHERE id = ?;", id)
return err
}
func main() {
var err error
errlog = log.New(log.Writer(), "[ERROR] ", log.Flags())
options.Parse()
log.Println("using database path \"" + options.DBPath + "\"")
db, err = sql.Open("sqlite3", options.DBPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(dbSquema)
if err != nil {
log.Fatal(err)
}
if options.Publish {
if len(options.ImagePath) < 1 {
panic("missing -i")
}
if len(options.Title) < 1 {
panic("missing -l")
}
_, err = newComic(options.Title, options.ImagePath, "", "")
if err != nil {
log.Fatal(err)
}
log.Println(fmt.Sprintf("comic \"%s\" created", options.Title))
return
}
// files
tPath := string(filepath.Join(options.TemplatesPath, "/static/"))
log.Println(fmt.Sprintf("using templates path \"%s\"", tPath))
fs := http.FileServer(http.Dir(tPath))
http.Handle("/static/", http.StripPrefix("/static/", fs))
log.Println(fmt.Sprintf("using media path \"%s\"", options.MediaPath))
fs = http.FileServer(http.Dir(options.MediaPath))
http.Handle("/media/", http.StripPrefix("/media/", fs))
// errors
http.HandleFunc("/401", return401)
http.HandleFunc("/404", return404)
http.HandleFunc("/500", return500)
// manager
if options.RunManager {
err = startManager(options.Address, options.Port, options.DBPath, options.MediaPath)
if err != nil {
log.Fatal(err)
}
}
// views
http.HandleFunc("/", comicView)
http.HandleFunc("/latest", latestView)
http.HandleFunc("/first", firstView)
http.HandleFunc("/random", randomView)
http.HandleFunc("/about", firstView)
http.HandleFunc("/all", allView)
http.HandleFunc("/blog", firstView)
uri := fmt.Sprintf("%s:%d", options.Address, options.Port)
log.Println("listening to http://" + uri)
log.Fatal(http.ListenAndServe(uri, logRequest(http.DefaultServeMux)))
}