lets-go:4-x

This commit is contained in:
tamsin johnson 2024-01-25 12:41:08 -08:00
parent fd34f7ba42
commit 569f2fc0a9
5 changed files with 183 additions and 21 deletions

View File

@ -1,10 +1,13 @@
package main
import (
"errors"
"fmt"
"html/template"
//"html/template"
"net/http"
"strconv"
"snippetbox.chaosfem.tw/internal/models"
)
// home ...
@ -14,22 +17,32 @@ func (app *application) home(w http.ResponseWriter, r *http.Request) {
return
}
files := []string{
"./ui/html/base.tmpl",
"./ui/html/partials/nav.tmpl",
"./ui/html/pages/home.tmpl",
}
ts, err := template.ParseFiles(files...)
snippets, err := app.snippets.Latest()
if err != nil {
app.serverError(w, r, err)
return
}
err = ts.ExecuteTemplate(w, "base", nil)
if err != nil {
app.serverError(w, r, err)
for _, snippet := range snippets {
fmt.Fprintf(w, "%+v", snippet)
}
// files := []string{
// "./ui/html/base.tmpl",
// "./ui/html/partials/nav.tmpl",
// "./ui/html/pages/home.tmpl",
// }
// ts, err := template.ParseFiles(files...)
// if err != nil {
// app.serverError(w, r, err)
// return
// }
// err = ts.ExecuteTemplate(w, "base", nil)
// if err != nil {
// app.serverError(w, r, err)
// }
}
// snippetView ...
@ -40,7 +53,17 @@ func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Fprintf(w, "It's snippet id: %d", id)
snippet, err := app.snippets.Get(id)
if err != nil || id < 1 {
if errors.Is(err, models.ErrNoRecord) {
app.notFound(w)
} else {
app.serverError(w, r, err)
}
return
}
fmt.Fprintf(w, "%+v", snippet)
}
// snippetCreate ...
@ -52,5 +75,15 @@ func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
return
}
w.Write([]byte("Creating a snippet!"))
title := "0 snail"
content := "0 snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n - Kobayashi Issa"
expires := 7
id, err := app.snippets.Insert(title, content, expires)
if err != nil {
app.serverError(w, r, err)
return
}
http.Redirect(w, r, fmt.Sprintf("/snippet/view?id=%d", id), http.StatusSeeOther)
}

View File

@ -1,32 +1,68 @@
package main
import (
"database/sql"
"flag"
"log/slog"
"net/http"
"regexp"
"os"
"snippetbox.chaosfem.tw/internal/models"
_ "github.com/go-sql-driver/mysql"
)
// for application wide dependencies
type application struct {
logger *slog.Logger
snippets *models.SnippetModel
}
// main it's the snippetbox webapp
func main() {
// configuration
addr := flag.String("addr", ":4000", "HTTP network address")
dsn := flag.String("dsn", "web:dbpass@/snippetbox?parseTime=true", "DB data source name")
logfmt := flag.String("logfmt", "text", "Log output format")
loglevel := flag.String("loglevel", "INFO", "Log level: DEBUG, INFO, WARN, or ERROR")
flag.Parse()
// setup the application
app := &application{
logger: loggerBuilder(logfmt, loglevel),
logger := loggerBuilder(logfmt, loglevel)
db, err := openDB(*dsn, logger)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
app.logger.Info("starting server", slog.String("addr", *addr))
err := http.ListenAndServe(*addr, app.routes())
app.logger.Error(err.Error())
defer db.Close()
// setup the application
app := &application{
logger: logger,
snippets: &models.SnippetModel{DB: db},
}
logger.Info("starting server", slog.String("addr", *addr))
err = http.ListenAndServe(*addr, app.routes())
logger.Error(err.Error())
os.Exit(1)
}
func openDB(dsn string, logger *slog.Logger) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
db.Close()
return nil, err
}
passRegexp := regexp.MustCompile(":\\S+@")
logger.Info("Opened DB connection pool", slog.String("adapter", "mysql"), slog.String("dsn", passRegexp.ReplaceAllString(dsn, ":<REDACTED>@")))
return db, nil
}

View File

@ -8,7 +8,7 @@ CREATE TABLE snippets (
expires DATETIME NOT NULL
);
CREATE USER 'web'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON snippetbox.* TO 'web'@'localhost' IDENTIFIED BY 'dbpass';
CREATE USER 'web';
GRANT SELECT, INSERT, UPDATE, DELETE ON snippetbox.* TO 'web' IDENTIFIED BY 'dbpass';
CREATE INDEX idx_snippets_created ON snippets(created);

View File

@ -0,0 +1,7 @@
package models
import (
"errors"
)
var ErrNoRecord = errors.New("models: no matching record found")

View File

@ -0,0 +1,86 @@
package models
import (
"database/sql"
"errors"
"time"
)
type Snippet struct {
ID int
Title string
Content string
Created time.Time
Expires time.Time
}
type SnippetModel struct {
DB *sql.DB
}
// Insert ...
func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) {
stmt := `INSERT INTO snippets (title, content, created, expires)
VALUES(?, ?, UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? DAY))`
result, err := m.DB.Exec(stmt, title, content, expires)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return int(id), nil
}
// Get ...
func (m *SnippetModel) Get(id int) (Snippet, error) {
stmt := `SELECT id, title, content, created, expires FROM snippets
WHERE expires > UTC_TIMESTAMP() and id = ?`
var s Snippet
err := m.DB.QueryRow(stmt, id).Scan(&s.ID, &s.Title, &s.Content, &s.Created, &s.Expires)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Snippet{}, ErrNoRecord
} else {
return Snippet{}, err
}
}
return s, nil
}
// Latest ...
func (m *SnippetModel) Latest() ([]Snippet, error) {
stmt := `SELECT id, title, content, created, expires FROM snippets
WHERE expires > UTC_TIMESTAMP() ORDER BY id DESC LIMIT 10`
rows, err := m.DB.Query(stmt)
if err != nil {
return nil, err
}
defer rows.Close()
var snippets []Snippet
for rows.Next() {
var s Snippet
err = rows.Scan(&s.ID, &s.Title, &s.Content, &s.Created, &s.Expires)
if err != nil {
return nil, err
}
snippets = append(snippets, s)
}
if err = rows.Err(); err != nil {
return nil, err
}
return snippets, nil
}