iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 13
0
Modern Web

從coding到上線-打造自己的blog系統系列 第 13

Day13 Blog API串接

blog

現在來接上procedure,在database/main.go寫入

// get blog with owner super project and category data
func GetBlog(path string) (*BlogProj, error) {
	blogData := &BlogProj{}
	has, err := db.SQL("call get_blog(?)", path).Get(blogData)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, nil
	}
	return blogData, nil
}

// Update an blog
func CreateBlog(oid, superid, blog, descript, typeid, superUrl string) error {
	return checkAffect(db.Exec("call create_blog(?, ?, ?, ?, ?, ?)", oid, superid, blog, descript, typeid, superUrl))
}

// Update an owner if no need to update uniquename, newuniname should be ""
func UpdateBlog(oid, superid, newsuperid, bid, blog, newblog, descript, originUrl, newsuperUrl string) error {
	return checkResult(db.Exec("call update_blog(?, ?, ?, ?, ?, ?, ?, ?, ?)", oid, superid, newsuperid, bid, blog, newblog, descript, originUrl, newsuperUrl))
}

// delete a blog
func DelBlog(oid, bid, url string) error {
	return checkAffect(db.Exec("call del_blog(?, ?, ?)", oid, bid, url))
}

在serve/main.go我們還需要處理檔案,現在先假設使用者寫完文章會已檔案的形式上傳過來,假設從前端傳來的形式是multipart/form,在gin裡面有專門處理的function可以用,建立common packeage來放一些通用function,創建form.go寫入

package common

import (
	"app/apperr"
	"app/logger"
	"errors"
	"github.com/gin-gonic/gin"
	"mime/multipart"
)

// check form key match or not
func checkParam(param []string, form *multipart.Form) string {
	for _, v := range param {
		if len(form.Value[v]) == 0 {
			return v
		}
	}

	return ""
}

func BindMultipartForm(c *gin.Context, param []string) (*multipart.Form, error) {
	form, err := c.MultipartForm()
	if err != nil {
		log.Warn(c, apperr.ErrWrongArgument, err, "binding error of put multipart form", "binding error of put multipart form")
		return nil, err
	}
	if v := checkParam(param, form); v != "" {
		var errStr = "multi part form miss match key "+v
		log.Warn(c, apperr.ErrWrongArgument, nil, errStr)
		return nil, errors.New(errStr)
	}
	return form, nil
}

解釋:

  • bind資料前先檢查傳來的參數有沒有吻合,如果不穩何就回傳error

create

先來處理寫檔,在util/file/file.go寫入

package file

import (
	"html/template"
	"io"
	"io/ioutil"
	"mime/multipart"
	"os"
	"path/filepath"
)

func Checkdir(dir string) error {
	var err error
	if _, err = os.Stat(dir); os.IsNotExist(err) {
		err = os.Mkdir(dir, 0700)
	}
	return err
}

// SaveUploadedFile uploads the form file to specific dst.
func SaveFile(file *multipart.FileHeader, dir, dst string) error {
	src, err := file.Open()
	if err != nil {
		return err
	}
	defer src.Close()

	srcByte, err := ioutil.ReadAll(src)
	if err != nil {
		return err
	}

	out, err := os.OpenFile(dir+"/"+dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = out.WriteString("{{define \"content\"}}\n")
	if err != nil {
		return err
	}

	_, err = out.Write(srcByte)

	_, err = out.WriteString("{{end}}")
	if err != nil {
		return err
	}

	return err
}

解釋:

  • 在檔案頭尾寫入{{define "content"}}{{end}}作為template

現在來規劃一下檔案的路徑,我們把先建立owner id的資料夾,底下再建立blog id的資料夾,在blog id底下就可以放文件與圖片,先在config/app/app.yaml的serve/main補上

FilePath: path_of_user_file

然後到setting/setting.go的ServerStruct補上

FilePath     string            `yaml:"FilePath,omitempty"`

之後就能讀設定的檔案路徑了

在common package下創建file.go寫入

package common

import (
	"app/apperr"
	"app/log"
	"app/setting"
	"app/util/file"
	"github.com/gin-gonic/gin"
	"mime/multipart"
)

// write file and parse to html
func WriteFormFile(c *gin.Context, form *multipart.Form, fileName string) {
	fileHeader := form.File["content"][0]
	filePath := setting.Servers["main"].FilePath + "/" + form.Value["oid"][0] + "/" + fileName
	// check exist and create
	if err := file.Checkdir(filePath); err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "something error in write file", "something error in create folder")
	}
	if err := file.SaveFile(fileHeader, filePath, fileName+".md"); err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "something error in write file")
		return
	}
}

現在來寫服務,在serve/main.go寫入

var (
	createBlogParam = []string{"oid", "superid", "descript", "blogType"}
	blogType        = map[string]string{"project": "1", "article": "2"}
)

func CreateBlog(c *gin.Context) {
	// check form
	form, err := common.BindMultipartForm(c, createBlogParam)
	if err != nil {
		return
	}

	if form.Value["blogType"][0] != "project" && len(form.File["content"]) == 0 {
		log.Warn(c, apperr.ErrWrongArgument, nil, "multy part form miss match key content")
		return
	}

	// create to database
	superUrl, blog := splitWork(c.Request.URL.Path)
	err = database.CreateBlog(form.Value["oid"][0], form.Value["superid"][0], blog, form.Value["descript"][0], blogType[form.Value["blogType"][0]], strings.Join(superUrl, "/"))
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error. please try again", "database error of update blog")
		return
	}

	// get data
	blogData, err := database.GetBlog(c.Request.URL.Path)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "database error")
		return
	}

	// write file
	if form.Value["blogType"][0] == "article" {
		common.WriteFormFile(c, form, strconv.Itoa(blogData.Bid))
	}

	c.Header("Location", "/.")
	c.Status(http.StatusCreated)
}

解釋:

  • 我們定義了create blog前端應該傳來的資料要有createBlogParam所包含的參數
  • 定義project,article對應資料庫的blogtype id

read

gin的context.HTML沒有提供合併檔案,要將檔案合併需要自己來,在serve/main.go寫入

func GetBlog(c *gin.Context) {
	blogData, err := database.GetBlog(c.Request.URL.Path)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "database error")
		return
	} else if blogData == nil {
		log.Warn(c, apperr.ErrWrongArgument, nil, "parmeter error", "parmeter error")
		return
	}

	meta, err := json.Marshal(blogData)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "parse json error")
		return
	}

	data := gin.H{
		"title":       blogData.Name,
		"meta":        string(meta),
		"description": blogData.Description,
		"owner":       blogData.OUniquename,
		"nickname":    blogData.ONickname,
		"updatetime":  blogData.Updatetime,
	}

	if blogData.Type == 1 {
		data["list"] = true
		err = file.ParseTmpl(c.Writer, data)
	} else {
		data["content"] = true
		//get file
		fileName := strconv.Itoa(blogData.Bid)

		err = file.ParseTmpl(c.Writer, data, setting.Servers["main"].FilePath+"/"+strconv.Itoa(blogData.Oid)+"/"+fileName+"/"+fileName+".md")
	}
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "read file error")
		return
	}

	c.Status(http.StatusOK)
}

解釋:

  • data是我們要傳入到HTML文本的值
  • 區分出project類型的blog,project只要傳資料就好,不用傳檔案

在view/html/component創建blogContainer.html寫入

{{ define "container/blog"}}
    <div>
        <!-- Title -->
        <h1>{{ .title }}</h1>

        <div>
            <!-- Author -->
            <h5>
                <a href="/{{ .owner }}">
                    {{ .nickname }}
                    <span class="text-muted">
                                @{{ .owner }}
                    </span>
                </a>
            </h5>
            <!-- Date/Time -->
            <p>Update on {{ .updatetime }}</p>

            <!-- description -->
            <p class="lead">{{.description}}</p>

            <!-- Post Content -->
            {{if .content}}
                <div id="content">
                    {{template "content"}}
                </div>
            {{end}}
        </div>
    </div>
{{ end }}

解釋:

  • {{if .content}}如果有傳content才會有裡面的片段
  • {{template "content"}}就是blog的檔案

創建blogList.html寫入

{{ define "list/blog" }}
    <div class="blog-list"></div>
    <!-- Blog Post template-->
    <template id="blog-list-tmpl">
        <div>
            <div>
                <a class="blog-href" href="#">
                    <h2 class="blog-list-title"></h2>
                </a>
                <p class="blog-list-description"></p>
            </div>
            <div class="">
                <a href="#" class="owner-href blog-list-owner"></a>
                ‧
                <a href="#" class="blog-list-createtime"></a>
            </div>
        </div>
    </template>
{{ end }}

解釋:

  • template用在前端parse

在view/html/meta/index.html的body內補上

                <!-- Blog Post -->
                {{if not .root}}
                    {{template "container/blog" . }}
                    <hr>
                {{end}}

                <!-- Blog List -->
                {{if .list}}
                    {{ template "list/blog" . }}
                {{end}}

解釋:

  • root page會放各個blog的標題與簡述,不會有單一blog的內容,所以用{{if not .root}}

update

update就把資料跟檔案覆寫上去

var (
	updateBlogParam = append(createBlogParam, "bid", "newsuperid", "newname", "newsuperUrl")
)

func UpdateBlog(c *gin.Context) {
	// check form
	form, err := common.BindMultipartForm(c, updateBlogParam)
	if err != nil {
		return
	}

	// check param update to database
	_, blog := splitWork(c.Request.URL.Path)

	// new super should be -1 if no update super
	// new name should be "" if no update name
	err = database.UpdateBlog(form.Value["oid"][0], form.Value["superid"][0], form.Value["newsuperid"][0],
		form.Value["bid"][0], blog, form.Value["newname"][0], form.Value["descript"][0], c.Request.URL.Path,
		form.Value["newsuperUrl"][0])
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error. please try again", "database error of update blog")
		return
	}

	// write file
	if form.Value["blogType"][0] == "article" && len(form.File["content"]) != 0 {
		common.WriteFormFile(c, form, form.Value["bid"][0])
	}

	c.Header("Location", c.Request.URL.Path)
	c.Status(http.StatusCreated)
}

delete

delete把整個資料夾與資料都刪掉

func DelBlog(c *gin.Context) {
	if err := database.DelBlog(c.PostForm("oid"), c.PostForm("bid"), c.Request.URL.Path); err != nil {
		if err == database.ERR_TASK_FAIL {
			log.Warn(c, apperr.ErrWrongArgument, err, "delete owner fail, please check oid and owner name correct")
		} else {
			log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error. please try again", "database error of delete owner")
		}
		return
	}

	if err := os.RemoveAll(setting.Servers["main"].FilePath + "/" + c.PostForm("oid") + "/" + c.PostForm("bid")); err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "something error in delete file")
		return
	}

	c.Status(http.StatusResetContent)
}

root

在database/scheme.go裡補上

type BlogList struct {
	OUniquename string `json:"ouniquename" xorm:"not null comment('id of User-defined') VARCHAR(50) 'uniquename'"`
	ONickname   string `json:"onickname" xorm:"not null VARCHAR(50) 'nickname'"`
	Blog        `xorm:"extends"`
}

database/main.go補上

func GetRoot(page string) ([]BlogList, error) {
	err = db.SQL("call get_root(?)", page).Find(&blogs)
	if err != nil {
		return nil, err
	} else if len(blogs) == 0 {
		return nil, nil
	}
	return blogs, nil
}

serve/main.go補上

// get root
func GetRoot(c *gin.Context) {
	blogDatas, err := database.GetRoot(c.DefaultQuery("p", "1"))
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "database error of getting root page")
		return
	}

	meta, err := json.Marshal(blogDatas)
	if err != nil {
		log.Warn(c, apperr.ErrPermissionDenied, err, "Sorry, something error", "parse json error")
		return
	}

	// return root page data
	c.HTML(http.StatusOK, "index", gin.H{
		"meta":        string(meta),
		"title":       "DCreater",
		"description": "create your blog",
		"author":      "林彥賓, https://github.com/Yan-Bin-Lin",
		"root":        true,
		"list":        true,
	})
}

解釋:

  • 定義query "p"來代表頁數,默認是第一頁

總結

我們把blog的讀寫搞定了,再處理前端就能看到畫面了

目前的工作環境

.
├── app
│   ├── apperr
│   │   ├── error.go
│   │   └── handle.go
│   ├── common
│   │   └── cookie.go
│   ├── config
│   │   └── app
│   │       ├── app.yaml
│   │       └── error.yaml
│   ├── database
│   │   ├── connect.go
│   │   ├── error.go
│   │   ├── main.go
│   │   └── scheme.go
│   ├── go.mod
│   ├── go.sum
│   ├── log
│   │   ├── logger.go
│   │   └── logging.go
│   ├── main.go
│   ├── middleware
│   │   ├── error.go
│   │   └── log.go
│   ├── router
│   │   ├── host_switch.go
│   │   └── main.go
│   ├── serve
│   │   ├── main.go
│   │   └── main_test.go
│   ├── setting
│   │   └── setting.go
│   ├── util
│   │   ├── debug
│   │   │   ├── stack.go
│   │   │   └── stack_test.go
│   │   └── file
│   │       └── file.go
│   └── view
│       ├── css
│       │   └── style.css
│       ├── html
│       │   ├── component
│       │   │   ├── blogContainer.html
│       │   │   └── blogList.html
│       │   └── meta
│       │       ├── head.html
│       │       └── index.html
│       └── js
├── config
│   └── app
│       ├── app.yaml
│       └── error.yaml
└── database
    └── maindata

上一篇
Day12 Blog CRUD Procedure
下一篇
Day14 帳戶處理
系列文
從coding到上線-打造自己的blog系統30

尚未有邦友留言

立即登入留言