diff --git a/snippetbox/cmd/web/handlers.go b/snippetbox/cmd/web/handlers.go index ca03a6b..21abc88 100644 --- a/snippetbox/cmd/web/handlers.go +++ b/snippetbox/cmd/web/handlers.go @@ -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) } diff --git a/snippetbox/cmd/web/main.go b/snippetbox/cmd/web/main.go index 44b065d..7ccebcc 100644 --- a/snippetbox/cmd/web/main.go +++ b/snippetbox/cmd/web/main.go @@ -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, ":@"))) + + return db, nil +} diff --git a/snippetbox/db/initdb.d/init.sql b/snippetbox/db/initdb.d/init.sql index 69550c4..8f924f7 100644 --- a/snippetbox/db/initdb.d/init.sql +++ b/snippetbox/db/initdb.d/init.sql @@ -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); diff --git a/snippetbox/internal/models/errors.go b/snippetbox/internal/models/errors.go new file mode 100644 index 0000000..a70c7dc --- /dev/null +++ b/snippetbox/internal/models/errors.go @@ -0,0 +1,7 @@ +package models + +import ( + "errors" +) + +var ErrNoRecord = errors.New("models: no matching record found") diff --git a/snippetbox/internal/models/snippets.go b/snippetbox/internal/models/snippets.go new file mode 100644 index 0000000..52272b0 --- /dev/null +++ b/snippetbox/internal/models/snippets.go @@ -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 +}