lets-go:4-x
This commit is contained in:
parent
fd34f7ba42
commit
569f2fc0a9
@ -1,10 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
//"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"snippetbox.chaosfem.tw/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// home ...
|
// home ...
|
||||||
@ -14,22 +17,32 @@ func (app *application) home(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files := []string{
|
snippets, err := app.snippets.Latest()
|
||||||
"./ui/html/base.tmpl",
|
|
||||||
"./ui/html/partials/nav.tmpl",
|
|
||||||
"./ui/html/pages/home.tmpl",
|
|
||||||
}
|
|
||||||
|
|
||||||
ts, err := template.ParseFiles(files...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.serverError(w, r, err)
|
app.serverError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ts.ExecuteTemplate(w, "base", nil)
|
for _, snippet := range snippets {
|
||||||
if err != nil {
|
fmt.Fprintf(w, "%+v", snippet)
|
||||||
app.serverError(w, r, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 ...
|
// snippetView ...
|
||||||
@ -40,7 +53,17 @@ func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 ...
|
// snippetCreate ...
|
||||||
@ -52,5 +75,15 @@ func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"flag"
|
"flag"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"snippetbox.chaosfem.tw/internal/models"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
// for application wide dependencies
|
// for application wide dependencies
|
||||||
type application struct {
|
type application struct {
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
snippets *models.SnippetModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// main it's the snippetbox webapp
|
// main it's the snippetbox webapp
|
||||||
func main() {
|
func main() {
|
||||||
// configuration
|
// configuration
|
||||||
addr := flag.String("addr", ":4000", "HTTP network address")
|
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")
|
logfmt := flag.String("logfmt", "text", "Log output format")
|
||||||
loglevel := flag.String("loglevel", "INFO", "Log level: DEBUG, INFO, WARN, or ERROR")
|
loglevel := flag.String("loglevel", "INFO", "Log level: DEBUG, INFO, WARN, or ERROR")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// setup the application
|
logger := loggerBuilder(logfmt, loglevel)
|
||||||
app := &application{
|
|
||||||
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))
|
defer db.Close()
|
||||||
err := http.ListenAndServe(*addr, app.routes())
|
|
||||||
app.logger.Error(err.Error())
|
// 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)
|
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
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ CREATE TABLE snippets (
|
|||||||
expires DATETIME NOT NULL
|
expires DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE USER 'web'@'localhost';
|
CREATE USER 'web';
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON snippetbox.* TO 'web'@'localhost' IDENTIFIED BY 'dbpass';
|
GRANT SELECT, INSERT, UPDATE, DELETE ON snippetbox.* TO 'web' IDENTIFIED BY 'dbpass';
|
||||||
|
|
||||||
CREATE INDEX idx_snippets_created ON snippets(created);
|
CREATE INDEX idx_snippets_created ON snippets(created);
|
||||||
|
7
snippetbox/internal/models/errors.go
Normal file
7
snippetbox/internal/models/errors.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNoRecord = errors.New("models: no matching record found")
|
86
snippetbox/internal/models/snippets.go
Normal file
86
snippetbox/internal/models/snippets.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user