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))) }