new exporter version
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Blog with Go Templates
|
||||
|
||||
This is a blog with templates. It uses the `html/template` package to generate web pages.
|
||||
It uses the Chi router to set up the routes and an SQLite database to store the articles.
|
||||
|
||||
Find the tutorial [here](https://blog.jetbrains.com/go/2022/11/08/build-a-blog-with-go-templates/).
|
||||
Binary file not shown.
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func connect() (*sql.DB, error) {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite3", "./data.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlStmt := `
|
||||
create table if not exists articles (id integer not null primary key autoincrement, title text, content text);
|
||||
`
|
||||
|
||||
_, err = db.Exec(sqlStmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func dbCreateArticle(article *Article) error {
|
||||
query, err := db.Prepare("insert into articles(title,content) values (?,?)")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = query.Exec(article.Title, article.Content)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbGetAllArticles() ([]*Article, error) {
|
||||
query, err := db.Prepare("select id, title, content from articles")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := query.Query()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
articles := make([]*Article, 0)
|
||||
for result.Next() {
|
||||
data := new(Article)
|
||||
err := result.Scan(
|
||||
&data.ID,
|
||||
&data.Title,
|
||||
&data.Content,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
articles = append(articles, data)
|
||||
}
|
||||
|
||||
return articles, nil
|
||||
}
|
||||
|
||||
func dbGetArticle(articleID string) (*Article, error) {
|
||||
query, err := db.Prepare("select id, title, content from articles where id = ?")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := query.QueryRow(articleID)
|
||||
data := new(Article)
|
||||
err = result.Scan(&data.ID, &data.Title, &data.Content)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func dbUpdateArticle(id string, article *Article) error {
|
||||
query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = query.Exec(article.Title, article.Content, id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbDeleteArticle(id string) error {
|
||||
query, err := db.Prepare("delete from articles where id=?")
|
||||
defer query.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = query.Exec(id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module GoBlog
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,221 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var router *chi.Mux
|
||||
var db *sql.DB
|
||||
|
||||
type Article struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content template.HTML `json:"content"`
|
||||
}
|
||||
|
||||
func catch(err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
router = chi.NewRouter()
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
var err error
|
||||
db, err = connect()
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func main() {
|
||||
router = chi.NewRouter()
|
||||
router.Use(middleware.Recoverer)
|
||||
|
||||
var err error
|
||||
db, err = connect()
|
||||
catch(err)
|
||||
|
||||
router.Use(ChangeMethod)
|
||||
router.Get("/", GetAllArticles)
|
||||
router.Post("/upload", UploadHandler) // Add this
|
||||
router.Get("/images/*", ServeImages) // Add this
|
||||
router.Route("/articles", func(r chi.Router) {
|
||||
r.Get("/", NewArticle)
|
||||
r.Post("/", CreateArticle)
|
||||
r.Route("/{articleID}", func(r chi.Router) {
|
||||
r.Use(ArticleCtx)
|
||||
r.Get("/", GetArticle) // GET /articles/1234
|
||||
r.Put("/", UpdateArticle) // PUT /articles/1234
|
||||
r.Delete("/", DeleteArticle) // DELETE /articles/1234
|
||||
r.Get("/edit", EditArticle) // GET /articles/1234/edit
|
||||
})
|
||||
})
|
||||
|
||||
err = http.ListenAndServe(":8005", router)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const MAX_UPLOAD_SIZE = 10 << 20
|
||||
r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
|
||||
if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
|
||||
http.Error(w, "The uploaded file is too big. Please choose an file that's less than 10MB in size", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, fileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
// Create the uploads folder if it doesn't already exist
|
||||
err = os.MkdirAll("./images", os.ModePerm)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new file in the uploads directory
|
||||
filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename))
|
||||
dst, err := os.Create("." + filename)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
|
||||
// Copy the uploaded file to the specified destination
|
||||
_, err = io.Copy(dst, file)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Println(filename)
|
||||
response, _ := json.Marshal(map[string]string{"location": filename})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
func ServeImages(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println(r.URL)
|
||||
fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images")))
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func ChangeMethod(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
switch method := r.PostFormValue("_method"); method {
|
||||
case http.MethodPut:
|
||||
fallthrough
|
||||
case http.MethodPatch:
|
||||
fallthrough
|
||||
case http.MethodDelete:
|
||||
r.Method = method
|
||||
default:
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func ArticleCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
articleID := chi.URLParam(r, "articleID")
|
||||
article, err := dbGetArticle(articleID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "article", article)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func GetAllArticles(w http.ResponseWriter, r *http.Request) {
|
||||
articles, err := dbGetAllArticles()
|
||||
catch(err)
|
||||
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/index.html")
|
||||
err = t.Execute(w, articles)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func NewArticle(w http.ResponseWriter, r *http.Request) {
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/new.html")
|
||||
err := t.Execute(w, nil)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func CreateArticle(w http.ResponseWriter, r *http.Request) {
|
||||
title := r.FormValue("title")
|
||||
content := r.FormValue("content")
|
||||
article := &Article{
|
||||
Title: title,
|
||||
Content: template.HTML(content),
|
||||
}
|
||||
|
||||
err := dbCreateArticle(article)
|
||||
catch(err)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func GetArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/article.html")
|
||||
err := t.Execute(w, article)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func EditArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
|
||||
t, _ := template.ParseFiles("templates/base.html", "templates/edit.html")
|
||||
err := t.Execute(w, article)
|
||||
catch(err)
|
||||
}
|
||||
|
||||
func UpdateArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
|
||||
title := r.FormValue("title")
|
||||
content := r.FormValue("content")
|
||||
newArticle := &Article{
|
||||
Title: title,
|
||||
Content: template.HTML(content),
|
||||
}
|
||||
fmt.Println(newArticle.Content)
|
||||
err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle)
|
||||
catch(err)
|
||||
http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound)
|
||||
}
|
||||
|
||||
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
|
||||
article := r.Context().Value("article").(*Article)
|
||||
err := dbDeleteArticle(strconv.Itoa(article.ID))
|
||||
catch(err)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
{{define "body"}}
|
||||
<h1>{{.Title}} </h1>
|
||||
<div>
|
||||
{{.Content}}
|
||||
</div>
|
||||
<div>
|
||||
<a href="/articles/{{.ID}}/edit">Edit</a>
|
||||
<form action="/articles/{{.ID}}" method="post">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ template "title" . }}</title>
|
||||
{{ template "scripts" }}
|
||||
</head>
|
||||
<body>
|
||||
{{ template "body" . }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{{define "title"}}Create new article{{end}}
|
||||
{{define "scripts"}}
|
||||
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||
<script>
|
||||
tinymce.init({
|
||||
selector: '#mytextarea',
|
||||
plugins: 'image',
|
||||
toolbar: 'undo redo | blocks | image | ' +
|
||||
'bold italic backcolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | help',
|
||||
images_upload_url: "/upload",
|
||||
relative_urls : false,
|
||||
remove_script_host : false,
|
||||
convert_urls : true,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "body"}}
|
||||
<form method="post" action="/articles/{{.ID}}">
|
||||
<input type="text" name="title" value="{{.Title}}">
|
||||
<textarea id="mytextarea" name="content">{{.Content}}</textarea>
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
<button id="submit" type="submit" onclick="submitForm()">Edit</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{{define "title"}}All articles{{end}}
|
||||
{{define "scripts"}}{{end}}
|
||||
{{define "body"}}
|
||||
{{if eq (len .) 0}}
|
||||
Nothing to see here
|
||||
{{end}}
|
||||
{{range .}}
|
||||
<div>
|
||||
<a href="/articles/{{.ID}}">{{.Title}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<p>
|
||||
<a href="/articles">Create new article</a>
|
||||
|
||||
</p>
|
||||
{{end}}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{define "title"}}Create new article{{end}}
|
||||
{{define "scripts"}}
|
||||
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||
<script>
|
||||
tinymce.init({
|
||||
selector: '#mytextarea',
|
||||
plugins: 'image',
|
||||
toolbar: 'undo redo | blocks | image | ' +
|
||||
'bold italic backcolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | help',
|
||||
images_upload_url: "/upload",
|
||||
relative_urls : false,
|
||||
remove_script_host : false,
|
||||
convert_urls : true,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "body"}}
|
||||
<form method="post" action="/articles">
|
||||
<input type="text" name="title" placeholder="Enter the title">
|
||||
<textarea id="mytextarea" name="content"></textarea>
|
||||
<button id="submit" type="submit">Create</button>
|
||||
</form>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user