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 } 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 } 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.Parse() 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) { 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 path := strings.TrimPrefix(r.URL.Path, "/") 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-1) if err != nil { errlog.Println(err) return404(w,r) err = nil return } 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 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) error { _, err := db.Exec("INSERT INTO comic(title, image, description, tags) VALUES(?, ?, ?, ?);", title, image, description, tags) return 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)) if options.RunManager { log.Fatal(startManager(options.Address, options.Port, options.DBPath, options.MediaPath)) return } // 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) // errors http.HandleFunc("/404", return404) http.HandleFunc("/500", return500) uri := fmt.Sprintf("%s:%d", options.Address, options.Port) log.Println("listening to http://" + uri) log.Fatal(http.ListenAndServe(uri, logRequest(http.DefaultServeMux))) }