refactor: eiblog

This commit is contained in:
deepzz0
2021-04-26 15:51:16 +08:00
parent bd69c62254
commit 68e01cdf1f
843 changed files with 3606 additions and 1007377 deletions

7
pkg/README.md Normal file
View File

@@ -0,0 +1,7 @@
Library code that's ok to use by external applications (e.g., `/pkg/mypubliclib`). Other projects will import these libraries expecting them to work, so think twice before you put something here :-) Note that the `internal` directory is a better way to ensure your private packages are not importable because it's enforced by Go. The `/pkg` directory is still a good way to explicitly communicate that the code in that directory is safe for use by others. The [`I'll take pkg over internal`](https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/) blog post by Travis Jeffery provides a good overview of the `pkg` and `internal` directories and when it might make sense to use them.
It's also a way to group Go code in one place when your root directory contains lots of non-Go components and directories making it easier to run various Go tools (as mentioned in these talks: [`Best Practices for Industrial Programming`](https://www.youtube.com/watch?v=PTE4VJIdHPg) from GopherCon EU 2018, [GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps](https://www.youtube.com/watch?v=oL6JBUk6tj0) and [GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go](https://www.youtube.com/watch?v=3gQa1LWwuzk)).
See the [`/pkg`](pkg/README.md) directory if you want to see which popular Go repos use this project layout pattern. This is a common layout pattern, but it's not universally accepted and some in the Go community don't recommend it.
It's ok not to use it if your app project is really small and where an extra level of nesting doesn't add much value (unless you really want to :-)). Think about it when it's getting big enough and your root directory gets pretty busy (especially if you have a lot of non-Go app components).

310
pkg/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,310 @@
// Package cache provides ...
package cache
import (
"bytes"
"fmt"
"sort"
"strings"
"time"
"github.com/eiblog/eiblog/v2/pkg/cache/render"
"github.com/eiblog/eiblog/v2/pkg/cache/store"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/eiblog/eiblog/v2/pkg/internal"
"github.com/eiblog/eiblog/v2/pkg/model"
"github.com/eiblog/eiblog/v2/tools"
"github.com/sirupsen/logrus"
)
var (
// Ei eiblog cache
Ei *Cache
// regenerate pages chan
pagesCh = make(chan string, 1)
pageSeries = "series-md"
pageArchive = "archive-md"
)
func init() {
store, err := store.NewStore(config.Conf.Database.Driver,
config.Conf.Database.Source)
if err != nil {
panic(err)
}
// Ei init
Ei = &Cache{
Store: store,
TagArticles: make(map[string]model.SortedArticles),
ArticlesMap: make(map[string]*model.Article),
}
err = Ei.loadAccount()
if err != nil {
panic(err)
}
// blogger
err = Ei.loadBlogger()
if err != nil {
panic(err)
}
// articles
err = Ei.loadArticles()
if err != nil {
panic(err)
}
go Ei.regeneratePages()
go Ei.timerClean()
go Ei.timerDisqus()
}
// Cache 整站缓存
type Cache struct {
store.Store
// load from db
Blogger *model.Blogger
Account *model.Account
Articles model.SortedArticles
// auto generate
PageSeries string // page
Series model.SortedSeries
PageArchives string // page
Archives model.SortedArchives
TagArticles map[string]model.SortedArticles // tagname:articles
ArticlesMap map[string]*model.Article // slug:article
}
// loadBlogger 博客信息
func (c *Cache) loadBlogger() error {
blogapp := config.Conf.BlogApp
blogger := &model.Blogger{
BlogName: blogapp.Blogger.BlogName,
SubTitle: blogapp.Blogger.SubTitle,
BeiAn: blogapp.Blogger.BeiAn,
BTitle: blogapp.Blogger.BTitle,
Copyright: blogapp.Blogger.Copyright,
}
blogger, err := c.LoadOrCreateBlogger(blogger)
if err != nil {
return err
}
c.Blogger = blogger
return nil
}
// loadAccount 账户账户信息
func (c *Cache) loadAccount() error {
blogapp := config.Conf.BlogApp
pwd := tools.EncryptPasswd(blogapp.Account.Password,
blogapp.Account.Password)
account := &model.Account{
Username: blogapp.Account.Username,
Password: pwd,
Email: blogapp.Account.Email,
PhoneN: blogapp.Account.PhoneNumber,
Address: blogapp.Account.Address,
}
account, err := c.LoadOrCreateAccount(account)
if err != nil {
return err
}
c.Account = account
return nil
}
// loadArticles 文章信息
func (c *Cache) loadArticles() error {
articles, err := c.LoadAllArticles()
if err != nil {
return err
}
sort.Sort(model.SortedArticles(articles))
for i, v := range Ei.Articles {
// 渲染页面
render.GenerateExcerptMarkdown(v)
c.ArticlesMap[v.Slug] = v
// 分析文章
if v.ID < config.Conf.BlogApp.General.StartID {
continue
}
if i > 0 {
v.Prev = Ei.Articles[i-1]
}
if Ei.Articles[i+1].ID >= config.Conf.BlogApp.General.StartID {
v.Next = Ei.Articles[i+1]
}
c.rebuildArticle(v, false)
}
// 重建专题与归档
pagesCh <- pageSeries
pagesCh <- pageArchive
return nil
}
// rebuildArticle 重建缓存tag、series、archive
func (c *Cache) rebuildArticle(article *model.Article, needSort bool) {
// tag
tags := strings.Split(article.Tags, ",")
for _, tag := range tags {
c.TagArticles[tag] = append(c.TagArticles[tag], article)
if needSort {
sort.Sort(c.TagArticles[tag])
}
}
// series
for i, series := range c.Series {
if series.ID == article.SerieID {
c.Series[i].Articles = append(c.Series[i].Articles, article)
if needSort {
sort.Sort(c.Series[i].Articles)
pagesCh <- pageSeries // 重建专题
}
}
}
// archive
y, m, _ := article.CreateTime.Date()
for i, archive := range c.Archives {
if ay, am, _ := archive.Time.Date(); y == ay && m == am {
c.Archives[i].Articles = append(c.Archives[i].Articles, article)
}
if needSort {
sort.Sort(c.Archives[i].Articles)
pagesCh <- pageArchive // 重建归档
}
return
}
// 新建归档
c.Archives = append(c.Archives, &model.Archive{
Time: article.CreateTime,
Articles: model.SortedArticles{article},
})
if needSort { // 重建归档
pagesCh <- pageArchive
}
}
// regeneratePages 重新生成series,archive页面
func (c *Cache) regeneratePages() {
for {
switch page := <-pagesCh; page {
case pageSeries:
sort.Sort(c.Series)
buf := bytes.Buffer{}
buf.WriteString(c.Blogger.SeriesSay)
buf.WriteString("\n\n")
for _, series := range c.Series {
buf.WriteString(fmt.Sprintf("### %s{#toc-%d}", series.Name, series.ID))
buf.WriteByte('\n')
buf.WriteString(series.Desc)
buf.WriteString("\n\n")
for _, article := range series.Articles {
//eg. * [标题一](/post/hello-world.html) <span class="date">(Man 02, 2006)</span>
str := fmt.Sprintf(`* [%s](/post/%s.html) <span class="date">(%s)</span>`,
article.Title, article.Slug, article.CreateTime.Format("Jan 02, 2006"))
buf.WriteString(str)
}
buf.WriteString("\n\n")
}
c.PageSeries = string(render.RenderPage(buf.Bytes()))
case pageArchive:
sort.Sort(c.Archives)
buf := bytes.Buffer{}
buf.WriteString(c.Blogger.ArchivesSay + "\n")
var (
currentYear string
gt12Month = len(Ei.Archives) > 12
)
for _, archive := range c.Archives {
if gt12Month {
year := archive.Time.Format("2006 年")
if currentYear != year {
currentYear = year
buf.WriteString(fmt.Sprintf("\n### %s\n\n", archive.Time.Format("2006 年")))
}
} else {
buf.WriteString(fmt.Sprintf("\n### %s\n\n", archive.Time.Format("2006年1月")))
}
for i, article := range archive.Articles {
if i == 0 && gt12Month {
str := fmt.Sprintf(`* *[%s](/post/%s.html) <span class="date">(%s)</span>`,
article.Title, article.Slug, article.CreateTime.Format("Jan 02, 2006"))
buf.WriteString(str)
} else {
str := fmt.Sprintf(`* [%s](/post/%s.html) <span class="date">(%s)</span>`,
article.Title, article.Slug, article.CreateTime.Format("Jan 02, 2006"))
buf.WriteString(str)
}
buf.WriteByte('\n')
}
}
c.PageArchives = string(render.RenderPage(buf.Bytes()))
}
}
}
// timerClean 定时清理文章
func (c *Cache) timerClean() {
dur := time.Duration(config.Conf.BlogApp.General.Clean)
ticker := time.NewTicker(dur * time.Hour)
for range ticker.C {
err := c.CleanArticles()
if err != nil {
logrus.Error("cache.timerClean.CleanArticles: ", err)
}
}
}
// timerDisqus disqus定时操作
func (c *Cache) timerDisqus() {
dur := time.Duration(config.Conf.BlogApp.Disqus.Interval)
ticker := time.NewTicker(dur * time.Hour)
for range ticker.C {
err := internal.PostsCount(c.ArticlesMap)
if err != nil {
logrus.Error("cache.timerDisqus.PostsCount: ", err)
}
}
}
// PageArticles 文章翻页
func (c *Cache) PageArticles(page int, pageSize int) (prev,
next int, articles []*model.Article) {
var l int
for l = len(c.Articles); l > 0; l-- {
if c.Articles[l-1].ID >= config.Conf.BlogApp.General.StartID {
break
}
}
if l == 0 {
return 0, 0, nil
}
m := l / pageSize
if d := l % pageSize; d > 0 {
m++
}
if page > m {
page = m
}
if page > 1 {
prev = page - 1
}
if page < m {
next = page + 1
}
s := (page - 1) * pageSize
e := page * pageSize
if e > l {
e = l
}
articles = c.Articles[s:e]
return
}

94
pkg/cache/render/render.go vendored Normal file
View File

@@ -0,0 +1,94 @@
// Package render provides ...
package render
import (
"regexp"
"strings"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/eiblog/eiblog/v2/pkg/model"
"github.com/eiblog/blackfriday"
)
// blackfriday 配置
const (
commonHtmlFlags = 0 |
blackfriday.HTML_TOC |
blackfriday.HTML_USE_XHTML |
blackfriday.HTML_USE_SMARTYPANTS |
blackfriday.HTML_SMARTYPANTS_FRACTIONS |
blackfriday.HTML_SMARTYPANTS_DASHES |
blackfriday.HTML_SMARTYPANTS_LATEX_DASHES |
blackfriday.HTML_NOFOLLOW_LINKS
commonExtensions = 0 |
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
blackfriday.EXTENSION_TABLES |
blackfriday.EXTENSION_FENCED_CODE |
blackfriday.EXTENSION_AUTOLINK |
blackfriday.EXTENSION_STRIKETHROUGH |
blackfriday.EXTENSION_SPACE_HEADERS |
blackfriday.EXTENSION_HEADER_IDS |
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK |
blackfriday.EXTENSION_DEFINITION_LISTS
)
var (
// 渲染markdown操作和截取摘要操作
regIdentifier = regexp.MustCompile(config.Conf.BlogApp.General.Identifier)
// header
regHeader = regexp.MustCompile("</nav></div>")
)
// IgnoreHtmlTag 去掉 html tag
func IgnoreHtmlTag(src string) string {
// 去除所有尖括号内的HTML代码
re, _ := regexp.Compile(`<[\S\s]+?>`)
src = re.ReplaceAllString(src, "")
// 去除换行符
re, _ = regexp.Compile(`\s+`)
return re.ReplaceAllString(src, "")
}
// RenderPage 渲染markdown
func RenderPage(md []byte) []byte {
renderer := blackfriday.HtmlRenderer(commonHtmlFlags, "", "")
return blackfriday.Markdown(md, renderer, commonExtensions)
}
// GenerateExcerptMarkdown 生成预览和描述
func GenerateExcerptMarkdown(article *model.Article) {
blogapp := config.Conf.BlogApp
if strings.HasPrefix(article.Content, blogapp.General.DescPrefix) {
index := strings.Index(article.Content, "\r\n")
prefix := article.Content[len(blogapp.General.DescPrefix):index]
article.Desc = IgnoreHtmlTag(prefix)
article.Content = article.Content[index:]
}
// 查找目录
content := RenderPage([]byte(article.Content))
index := regHeader.FindIndex(content)
if index != nil {
article.Header = string(content[0:index[1]])
article.Content = string(content[index[1]:])
} else {
article.Content = string(content)
}
// excerpt
index = regIdentifier.FindStringIndex(article.Content)
if index != nil {
article.Excerpt = IgnoreHtmlTag(article.Content[:index[0]])
}
uc := []rune(article.Content)
length := blogapp.General.Length
if len(uc) < length {
length = len(uc)
}
article.Excerpt = IgnoreHtmlTag(string(uc[0:length]))
}

2
pkg/cache/store/mongodb.go vendored Normal file
View File

@@ -0,0 +1,2 @@
// Package store provides ...
package store

70
pkg/cache/store/store.go vendored Normal file
View File

@@ -0,0 +1,70 @@
// Package store provides ...
package store
import (
"fmt"
"sort"
"sync"
"github.com/eiblog/eiblog/v2/pkg/model"
)
var (
storeMu sync.RWMutex
stores = make(map[string]Driver)
)
// Store 存储后端
type Store interface {
LoadOrCreateAccount(acct *model.Account) (*model.Account, error)
LoadOrCreateBlogger(blogger *model.Blogger) (*model.Blogger, error)
LoadAllArticles() ([]*model.Article, error)
UpdateAccount(name string, fields map[string]interface{}) error
UpdateBlogger(fields map[string]interface{}) error
UpdateArticle(article *model.Article) error
CleanArticles() error
}
// Driver 存储驱动
type Driver interface {
Init(source string) (Store, error)
}
// Register 注册驱动
func Register(name string, driver Driver) {
storeMu.Lock()
defer storeMu.Unlock()
if driver == nil {
panic("store: register driver is nil")
}
if _, dup := stores[name]; dup {
panic("store: register called twice for driver " + name)
}
stores[name] = driver
}
// Drivers 获取所有
func Drivers() []string {
storeMu.Lock()
defer storeMu.Unlock()
list := make([]string, 0, len(stores))
for name := range stores {
list = append(list, name)
}
sort.Strings(list)
return list
}
// NewStore 新建存储
func NewStore(name string, source string) (Store, error) {
storeMu.RLock()
driver, ok := stores[name]
storeMu.RUnlock()
if !ok {
return nil, fmt.Errorf("store: unknown driver %q (forgotten import?)", name)
}
return driver.Init(source)
}

173
pkg/config/config.go Normal file
View File

@@ -0,0 +1,173 @@
// Package config provides ...
package config
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
)
var (
// Conf config instance
Conf Config
// ModeDev run mode as development
ModeDev = "dev"
// ModeProd run mode as production
ModeProd = "prod"
// WorkDir workspace dir
WorkDir string
)
// Mode run mode
type Mode struct {
Name string `yaml:"name"`
EnableHTTP bool `yaml:"enablehttp"`
HTTPPort int `yaml:"httpport"`
EnableGRPC bool `yaml:"enablegrpc"`
GRPCPort int `yaml:"grpcport"`
Host string `yaml:"host"`
}
// Database sql database
type Database struct {
Driver string `yaml:"driver"`
Source string `yaml:"source"`
}
// General common
type General struct {
PageNum int `yaml:"pagenum"` // 前台每页文章数量
PageSize int `yaml:"pagesize"` // 后台每页文章数量
StartID int32 `yaml:"startid"` // 文章启始ID
DescPrefix string `yaml:"descprefix"` // 文章描述前缀
Identifier string `yaml:"identifier"` // 文章截取标识
Length int `yaml:"length"` // 文章预览长度
Trash int `yaml:"trash"` // 回收箱文章保留时间
Clean int `yaml:"clean"` // 清理回收箱频率
}
// Disqus comments
type Disqus struct {
ShortName string `yaml:"shortname"`
PublicKey string `yaml:"publickey"`
AccessToken string `yaml:"accesstoken"`
Interval int `yaml:"interval"` // 获取评论数量间隔
}
// Twitter card
type Twitter struct {
Card string `yaml:"card"`
Site string `yaml:"site"`
Image string `yaml:"image"`
Address string `yaml:"address"`
}
// Google analytics
type Google struct {
URL string `yaml:"url"`
Tid string `yaml:"tid"`
V string `yaml:"v"`
T string `yaml:"t"`
}
// Qiniu oss
type Qiniu struct {
Bucket string `yaml:"bucket"`
Domain string `yaml:"domain"`
AccessKey string `yaml:"accesskey"`
SecretKey string `yaml:"secretkey"`
}
// FeedRPC feedr
type FeedRPC struct {
FeedrURL string `yaml:"feedrurl"`
PingRPC []string `yaml:"pingrpc"`
}
// Account info
type Account struct {
Username string `yaml:"username"` // *
Password string `yaml:"password"` // *
Email string `yaml:"email"`
PhoneNumber string `yaml:"phonenumber"`
Address string `yaml:"address"`
}
// Blogger info
type Blogger struct {
BlogName string `yaml:"blogname"`
SubTitle string `yaml:"subtitle"`
BeiAn string `yaml:"beian"`
BTitle string `yaml:"btitle"`
Copyright string `yaml:"copyright"`
}
// BlogApp config
type BlogApp struct {
Mode
StaticVersion int `yaml:"staticversion"`
HotWords []string `yaml:"hotwords"`
General General `yaml:"general"`
Disqus Disqus `yaml:"disqus"`
Google Google `yaml:"google"`
Qiniu Qiniu `yaml:"qiniu"`
Twitter Twitter `yaml:"twitter"`
FeedRPC FeedRPC `yaml:"feedrpc"`
Account Account `yaml:"account"`
Blogger Blogger `yaml:"blogger"`
}
// Config app config
type Config struct {
RunMode string `yaml:"runmode"`
AppName string `yaml:"appname"`
Database Database `yaml:"database"`
ESHost string `yaml:"eshost"`
BlogApp BlogApp `yaml:"blogapp"`
BackupApp Mode `yaml:"backupapp"`
}
// load config file
func init() {
// compatibility linux and windows
var err error
WorkDir, err = os.Getwd()
if err != nil {
panic(err)
}
path := filepath.Join(WorkDir, "conf", "app.yml")
data, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
err = yaml.Unmarshal(data, &Conf)
if err != nil {
panic(err)
}
// read run mode from env
if runmode := os.Getenv("RUN_MODE"); runmode != "" {
if runmode != ModeDev && runmode != ModeProd {
panic("invalid RUN_MODE from env: " + runmode)
}
Conf.RunMode = runmode
}
// read env
readDBEnv()
}
func readDBEnv() {
key := strings.ToUpper(Conf.AppName) + "_DB_DRIVER"
if d := os.Getenv(key); d != "" {
Conf.Database.Driver = d
}
key = strings.ToUpper(Conf.AppName) + "_DB_SOURCE"
if s := os.Getenv(key); s != "" {
Conf.Database.Source = s
}
}

80
pkg/core/blog/api.go Normal file
View File

@@ -0,0 +1,80 @@
// Package eiblog provides ...
package eiblog
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// @title APP Demo API
// @version 1.0
// @description This is a sample server celler server.
// @BasePath /api
// LogStatus log status
type LogStatus int
// user log status
var (
LogStatusOut LogStatus = 0
LogStatusTFA LogStatus = 1
LogStatusIn LogStatus = 2
)
// AuthFilter auth filter
func AuthFilter(c *gin.Context) {
if !IsLogined(c) {
c.Abort()
c.Status(http.StatusUnauthorized)
return
}
c.Next()
}
// SetLogStatus login user
func SetLogStatus(c *gin.Context, uid string, status LogStatus) {
session := sessions.Default(c)
session.Set("uid", uid)
session.Set("status", int(status))
session.Save()
}
// SetLogout logout user
func SetLogout(c *gin.Context) {
session := sessions.Default(c)
session.Set("status", int(LogStatusOut))
session.Save()
}
// IsLogined account logined
func IsLogined(c *gin.Context) bool {
status := GetLogStatus(c)
if status < 0 {
return false
}
return status == LogStatusIn
}
// GetUserID get logined account uuid
func GetUserID(c *gin.Context) string {
session := sessions.Default(c)
uid := session.Get("uid")
if uid == nil {
return ""
}
return uid.(string)
}
// GetLogStatus get account log status
func GetLogStatus(c *gin.Context) LogStatus {
session := sessions.Default(c)
status := session.Get("status")
if status == nil {
return -1
}
return LogStatus(status.(int))
}

View File

@@ -0,0 +1,65 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2021-04-26 15:31:15.52194 +0800 CST m=+0.022347488
package docs
import (
"bytes"
"encoding/json"
"github.com/alecthomas/template"
"github.com/swaggo/swag"
)
var doc = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "This is a sample server celler server.",
"title": "APP Demo API",
"contact": {},
"license": {},
"version": "1.0"
},
"host": "{{.Host}}",
"basePath": "/api",
"paths": {}
}`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{ Schemes: []string{}}
type s struct{}
func (s *s) ReadDoc() string {
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface {}) string {
a, _ := json.Marshal(v)
return string(a)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, SwaggerInfo); err != nil {
return doc
}
return tpl.String()
}
func init() {
swag.Register(swag.Name, &s{})
}

View File

@@ -0,0 +1,13 @@
{
"swagger": "2.0",
"info": {
"description": "This is a sample server celler server.",
"title": "APP Demo API",
"contact": {},
"license": {},
"version": "1.0"
},
"host": "{{.Host}}",
"basePath": "/api",
"paths": {}
}

View File

@@ -0,0 +1,10 @@
basePath: /api
host: '{{.Host}}'
info:
contact: {}
description: This is a sample server celler server.
license: {}
title: APP Demo API
version: "1.0"
paths: {}
swagger: "2.0"

View File

@@ -0,0 +1,49 @@
// Package file provides ...
package file
import (
"net/http"
"github.com/gin-gonic/gin"
)
// RegisterRoutes register routes
func RegisterRoutes(e *gin.Engine) {
e.GET("/rss.html", handleFeed)
e.GET("/feed", handleFeed)
e.GET("/opensearch.xml", handleOpensearch)
e.GET("/sitemap.xml", handleSitemap)
e.GET("/robots.txt", handleRobots)
e.GET("/crossdomain.xml", handleCrossDomain)
e.GET("/favicon.ico", handleFavicon)
}
// handleFeed feed.xml
func handleFeed(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "assets/feed.xml")
}
// handleOpensearch opensearch.xml
func handleOpensearch(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "assets/opensearch.xml")
}
// handleRobots robotx.txt
func handleRobots(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "assets/robots.txt")
}
// handleSitemap sitemap.xml
func handleSitemap(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "assets/sitemap.xml")
}
// handleCrossDomain crossdomain.xml
func handleCrossDomain(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "assets/crossdomain.xml")
}
// handleFavicon favicon.ico
func handleFavicon(c *gin.Context) {
http.ServeFile(c.Writer, c.Request, "assets/favicon.ico")
}

163
pkg/core/blog/file/timer.go Normal file
View File

@@ -0,0 +1,163 @@
// Package file provides ...
package file
import (
"os"
"path/filepath"
"text/template"
"time"
"github.com/eiblog/eiblog/v2/pkg/cache"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/eiblog/utils/tmpl"
"github.com/sirupsen/logrus"
)
var xmlTmpl *template.Template
func init() {
root := filepath.Join(config.WorkDir, "conf", "tpl", "*.xml")
var err error
xmlTmpl, err = template.New("").Funcs(template.FuncMap{
"dateformat": tmpl.DateFormat,
}).ParseGlob(root)
if err != nil {
panic(err)
}
go timerFeed()
go timerSitemap()
}
// timerFeed 定时刷新feed
func timerFeed() {
tpl := xmlTmpl.Lookup("feedTpl.xml")
if tpl == nil {
logrus.Info("file: not found: feedTpl.xml")
return
}
t := time.NewTicker(time.Hour * 4)
for now := range t.C {
_, _, articles := cache.Ei.PageArticles(1, 20)
params := map[string]interface{}{
"Titile": cache.Ei.Blogger.BTitle,
"SubTitle": cache.Ei.Blogger.SubTitle,
"Host": config.Conf.BlogApp.Host,
"FeedrURL": config.Conf.BlogApp.FeedRPC.FeedrURL,
"BuildDate": now.Format(time.RFC1123Z),
"Articles": articles,
}
f, err := os.OpenFile("assets/feed.xml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
logrus.Error("file: timerFeed.OpenFile: ", err)
continue
}
defer f.Close()
err = tpl.Execute(f, params)
if err != nil {
logrus.Error("file: timerFeed.Execute: ", err)
continue
}
}
}
// timerSitemap 定时刷新sitemap
func timerSitemap() {
tpl := xmlTmpl.Lookup("sitemapTpl.xml")
if tpl == nil {
logrus.Info("file: not found: sitemapTpl.xml")
return
}
t := time.NewTicker(time.Hour * 4)
for range t.C {
params := map[string]interface{}{
"Articles": cache.Ei.Articles,
"Host": config.Conf.BlogApp.Host,
}
f, err := os.OpenFile("assets/sitemap.xml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
logrus.Error("file: timerSitemap.OpenFile: ", err)
continue
}
defer f.Close()
err = tpl.Execute(f, params)
if err != nil {
logrus.Error("file: timerSitemap.Execute: ", err)
continue
}
}
}
// generateOpensearch 生成opensearch.xml
func generateOpensearch() {
tpl := xmlTmpl.Lookup("opensearchTpl.xml")
if tpl == nil {
logrus.Info("file: not found: opensearchTpl.xml")
return
}
params := map[string]string{
"BTitle": cache.Ei.Blogger.BTitle,
"SubTitle": cache.Ei.Blogger.SubTitle,
"Host": config.Conf.BlogApp.Host,
}
f, err := os.OpenFile("static/opensearch.xml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
logrus.Error("file: generateOpensearch.OpenFile: ", err)
return
}
defer f.Close()
err = tpl.Execute(f, params)
if err != nil {
logrus.Error("file: generateOpensearch.Execute: ", err)
return
}
}
// generateRobots 生成robots.txt
func generateRobots() {
tpl := xmlTmpl.Lookup("robotsTpl.xml")
if tpl == nil {
logrus.Info("file: not found: robotsTpl.xml")
return
}
params := map[string]string{
"Host": config.Conf.BlogApp.Host,
}
f, err := os.OpenFile("static/robots.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
logrus.Error("file: generateRobots.OpenFile: ", err)
return
}
defer f.Close()
err = tpl.Execute(f, params)
if err != nil {
logrus.Error("file: generateRobots.Execute: ", err)
return
}
}
// generateCrossdomain 生成crossdomain.xml
func generateCrossdomain() {
tpl := xmlTmpl.Lookup("crossdomainTpl.xml")
if tpl == nil {
logrus.Info("file: not found: crossdomainTpl.xml")
return
}
params := map[string]string{
"Host": config.Conf.BlogApp.Host,
}
f, err := os.OpenFile("static/crossdomain.xml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
logrus.Error("file: generateCrossdomain.OpenFile: ", err)
return
}
defer f.Close()
err = tpl.Execute(f, params)
if err != nil {
logrus.Error("file: generateCrossdomain.Execute: ", err)
return
}
}

293
pkg/core/blog/page/page.go Normal file
View File

@@ -0,0 +1,293 @@
// Package page provides ...
package page
import (
"bytes"
"fmt"
htemplate "html/template"
"io/ioutil"
"net/http"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"github.com/eiblog/eiblog/setting"
"github.com/eiblog/eiblog/v2/pkg/cache"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/eiblog/eiblog/v2/pkg/internal"
"github.com/eiblog/eiblog/v2/tools"
"github.com/eiblog/utils/tmpl"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// htmlTmpl html template cache
var htmlTmpl *template.Template
func init() {
htmlTmpl = template.New("eiblog").Funcs(tmpl.TplFuncMap)
root := filepath.Join(config.WorkDir, "website")
files := tools.ReadDirFiles(root, func(name string) bool {
if name == ".DS_Store" {
return true
}
return false
})
_, err := htmlTmpl.ParseFiles(files...)
if err != nil {
panic(err)
}
}
// RegisterRoutes register routes
func RegisterRoutes(e *gin.Engine) {
e.NoRoute(handleNotFound)
e.GET("/", handleHomePage)
e.GET("/post/:slug", handleArticlePage)
e.GET("/series.html", handleSeriesPage)
e.GET("/archives.html", handleArchivePage)
e.GET("/search.html", handleSearchPage)
e.GET("/disqus/form/post-:slug", handleDisqusPage)
e.GET("/beacon.html", handleBeaconPage)
}
// baseParams 基础参数
func baseParams(c *gin.Context) gin.H {
version := 0
cookie, err := c.Request.Cookie("v")
if err != nil || cookie.Value !=
fmt.Sprint(config.Conf.BlogApp.StaticVersion) {
version = config.Conf.BlogApp.StaticVersion
}
return gin.H{
"BlogName": cache.Ei.Blogger.BlogName,
"SubTitle": cache.Ei.Blogger.SubTitle,
"BTitle": cache.Ei.Blogger.BTitle,
"BeiAn": cache.Ei.Blogger.BeiAn,
"Domain": config.Conf.BlogApp.Host,
"CopyYear": time.Now().Year(),
"Twitter": config.Conf.BlogApp.Twitter,
"Qiniu": config.Conf.BlogApp.Qiniu,
"Disqus": config.Conf.BlogApp.Disqus,
"Version": version,
}
}
// handleNotFound not found page
func handleNotFound(c *gin.Context) {
params := baseParams(c)
params["title"] = "Not Found"
params["Description"] = "404 Not Found"
params["Path"] = ""
c.Status(http.StatusNotFound)
renderHTMLHomeLayout(c, "notfound", params)
}
// handleHomePage 首页
func handleHomePage(c *gin.Context) {
params := baseParams(c)
params["title"] = cache.Ei.Blogger.BTitle + " | " + cache.Ei.Blogger.SubTitle
params["Description"] = "博客首页," + cache.Ei.Blogger.SubTitle
params["Path"] = c.Request.URL.Path
params["CurrentPage"] = "blog-home"
pn, err := strconv.Atoi(c.Query("pn"))
if err != nil || pn < 1 {
pn = 1
}
params["Prev"], params["Next"], params["List"] = cache.Ei.PageArticles(pn,
config.Conf.BlogApp.General.PageNum)
renderHTMLHomeLayout(c, "home", params)
}
// handleArticlePage 文章页
func handleArticlePage(c *gin.Context) {
slug := c.Param("slug")
if !strings.HasSuffix(slug, ".html") || cache.Ei.ArticlesMap[slug[:len(slug)-5]] == nil {
handleNotFound(c)
return
}
article := cache.Ei.ArticlesMap[slug[:len(slug)-5]]
params := baseParams(c)
params["Title"] = article.Title + " | " + cache.Ei.Blogger.BTitle
params["Path"] = c.Request.URL.Path
params["CurrentPage"] = "post-" + article.Slug
params["Article"] = article
var name string
switch slug {
case "blogroll.html":
name = "blogroll"
params["Description"] = "友情连接," + cache.Ei.Blogger.SubTitle
case "about.html":
name = "about"
params["Description"] = "关于作者," + cache.Ei.Blogger.SubTitle
default:
params["Description"] = article.Desc + "" + cache.Ei.Blogger.SubTitle
name = "article"
params["Copyright"] = cache.Ei.Blogger.Copyright
if !article.UpdateTime.IsZero() {
params["Days"] = int(time.Now().Sub(article.UpdateTime).Hours()) / 24
} else {
params["Days"] = int(time.Now().Sub(article.CreateTime).Hours()) / 24
}
if article.SerieID > 0 {
for _, series := range cache.Ei.Series {
if series.ID == article.SerieID {
params["Serie"] = series
}
}
}
}
renderHTMLHomeLayout(c, name, params)
}
// handleSeriesPage 专题页
func handleSeriesPage(c *gin.Context) {
params := baseParams(c)
params["Title"] = "专题 | " + cache.Ei.Blogger.BTitle
params["Description"] = "专题列表," + cache.Ei.Blogger.SubTitle
params["Path"] = c.Request.URL.Path
params["CurrentPage"] = "series"
params["Article"] = cache.Ei.PageSeries
renderHTMLHomeLayout(c, "series", params)
}
// handleArchivePage 归档页
func handleArchivePage(c *gin.Context) {
params := baseParams(c)
params["Title"] = "归档 | " + cache.Ei.Blogger.BTitle
params["Description"] = "博客归档," + cache.Ei.Blogger.SubTitle
params["Path"] = c.Request.URL.Path
params["CurrentPage"] = "archives"
params["Article"] = cache.Ei.PageArchives
renderHTMLHomeLayout(c, "archives", params)
}
// handleSearchPage 搜索页
func handleSearchPage(c *gin.Context) {
params := baseParams(c)
params["Title"] = "站内搜索 | " + cache.Ei.Blogger.BTitle
params["Description"] = "站内搜索," + cache.Ei.Blogger.SubTitle
params["Path"] = ""
params["CurrentPage"] = "search-post"
q := strings.TrimSpace(c.Query("q"))
if q != "" {
start, err := strconv.Atoi(c.Query("start"))
if start < 1 || err != nil {
start = 1
}
params["Word"] = q
vals := c.Request.URL.Query()
result, err := internal.ElasticSearch(q, config.Conf.BlogApp.General.PageNum, start-1)
if err != nil {
logrus.Error("HandleSearchPage.ElasticSearch: ", err)
} else {
result.Took /= 1000
for i, v := range result.Hits.Hits {
article := cache.Ei.ArticlesMap[v.Source.Slug]
if len(v.Highlight.Content) == 0 && article != nil {
result.Hits.Hits[i].Highlight.Content = []string{article.Excerpt}
}
}
params["SearchResult"] = result
if num := start - config.Conf.BlogApp.General.PageNum; num > 0 {
vals.Set("start", fmt.Sprint(num))
params["Prev"] = vals.Encode()
}
if num := start + config.Conf.BlogApp.General.PageNum; result.Hits.Total >= num {
vals.Set("start", fmt.Sprint(num))
params["Next"] = vals.Encode()
}
}
} else {
params["HotWords"] = config.Conf.BlogApp.HotWords
}
renderHTMLHomeLayout(c, "search", params)
}
// handleDisqusPage 评论页
func handleDisqusPage(c *gin.Context) {
array := strings.Split(c.Param("slug"), "|")
if len(array) != 4 || array[1] == "" {
c.String(http.StatusOK, "出错啦。。。")
return
}
article := cache.Ei.ArticlesMap[array[0]]
params := gin.H{
"Titile": "发表评论 | " + config.Conf.BlogApp.Blogger.BTitle,
"ATitle": article.Title,
"Thread": array[1],
"Slug": article.Slug,
}
err := htmlTmpl.ExecuteTemplate(c.Writer, "disqus.html", params)
if err != nil {
panic(err)
}
c.Header("Content-Type", "text/html; charset=utf-8")
}
// handleBeaconPage 服务端推送谷歌统计
func handleBeaconPage(c *gin.Context) {
ua := c.Request.UserAgent()
vals := c.Request.URL.Query()
vals.Set("v", setting.Conf.Google.V)
vals.Set("tid", setting.Conf.Google.Tid)
vals.Set("t", setting.Conf.Google.T)
cookie, _ := c.Cookie("u")
vals.Set("cid", cookie)
vals.Set("dl", c.Request.Referer())
vals.Set("uip", c.ClientIP())
go func() {
req, err := http.NewRequest("POST", config.Conf.BlogApp.Google.URL,
strings.NewReader(vals.Encode()))
if err != nil {
logrus.Error("HandleBeaconPage.NewRequest: ", err)
return
}
req.Header.Set("User-Agent", ua)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)
if err != nil {
logrus.Error("HandleBeaconPage.Do: ", err)
return
}
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
logrus.Error("HandleBeaconPage.ReadAll: ", err)
return
}
if res.StatusCode/100 != 2 {
logrus.Error(string(data))
}
}()
c.Status(http.StatusNoContent)
}
// renderHTMLHomeLayout homelayout html
func renderHTMLHomeLayout(c *gin.Context, name string, data gin.H) {
buf := bytes.Buffer{}
err := htmlTmpl.ExecuteTemplate(&buf, name, data)
if err != nil {
panic(err)
}
data["LayoutContent"] = htemplate.HTML(buf.String())
err = htmlTmpl.ExecuteTemplate(c.Writer, "homelayout.html", data)
if err != nil {
panic(err)
}
if c.Writer.Status() == 0 {
c.Status(http.StatusOK)
}
c.Header("Content-Type", "text/html; charset=utf-8")
}

View File

@@ -0,0 +1,15 @@
// Package swag provides ...
package swag
import (
_ "github.com/eiblog/eiblog/v2/pkg/core/blog/docs" // docs
"github.com/gin-gonic/gin"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
)
// RegisterRoutes register routes
func RegisterRoutes(group gin.IRoutes) {
group.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

296
pkg/internal/disqus.go Normal file
View File

@@ -0,0 +1,296 @@
// Package internal provides ...
package internal
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/eiblog/eiblog/v2/pkg/model"
)
// disqus api
const (
apiPostsCount = "https://disqus.com/api/3.0/threads/set.json"
apiPostsList = "https://disqus.com/api/3.0/threads/listPosts.json"
apiPostCreate = "https://disqus.com/api/3.0/posts/create.json"
apiPostApprove = "https://disqus.com/api/3.0/posts/approve.json"
apiThreadCreate = "https://disqus.com/api/3.0/threads/create.json"
)
func checkDisqusConfig() error {
if config.Conf.BlogApp.Disqus.ShortName != "" &&
config.Conf.BlogApp.Disqus.PublicKey != "" &&
config.Conf.BlogApp.Disqus.AccessToken != "" {
return nil
}
return errors.New("disqus: config incompleted")
}
// postsCountResp 评论数量响应
type postsCountResp struct {
Code int
Response []struct {
ID string
Posts int
Identifiers []string
}
}
// PostsCount 获取文章评论数量
func PostsCount(articles map[string]*model.Article) error {
if err := checkDisqusConfig(); err != nil {
return err
}
vals := url.Values{}
vals.Set("api_key", config.Conf.BlogApp.Disqus.PublicKey)
vals.Set("forum", config.Conf.BlogApp.Disqus.ShortName)
// batch get
var count, index int
for _, article := range articles {
if index < len(articles) && count < 50 {
count++
index++
vals.Add("thread:ident", "post-"+article.Slug)
continue
}
count = 0
resp, err := httpGet(apiPostsCount + "?" + vals.Encode())
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
// check http status code
if resp.StatusCode != http.StatusOK {
return errors.New(string(b))
}
result := &postsCountResp{}
err = json.Unmarshal(b, result)
if err != nil {
return err
}
for _, v := range result.Response {
i := strings.Index(v.Identifiers[0], "-")
slug := v.Identifiers[0][i+1:]
if article := articles[slug]; article != nil {
article.Count = v.Posts
article.Thread = v.ID
}
}
}
return nil
}
// postsListResp 获取评论列表
type postsListResp struct {
Cursor struct {
HasNext bool
Next string
}
Code int
Response []postDetail
}
type postDetail struct {
Parent int
ID string
CreatedAt string
Message string
IsDeleted bool
Author struct {
Name string
ProfileUrl string
Avatar struct {
Cache string
}
}
Thread string
}
// PostsList 评论列表
func PostsList(slug, cursor string) (*postsListResp, error) {
if err := checkDisqusConfig(); err != nil {
return nil, err
}
vals := url.Values{}
vals.Set("api_key", config.Conf.BlogApp.Disqus.PublicKey)
vals.Set("forum", config.Conf.BlogApp.Disqus.ShortName)
vals.Set("thread:ident", "post-"+slug)
vals.Set("cursor", cursor)
vals.Set("limit", "50")
resp, err := httpGet(apiPostsList + "?" + vals.Encode())
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(b))
}
result := &postsListResp{}
err = json.Unmarshal(b, result)
if err != nil {
return nil, err
}
return result, nil
}
// PostComment 评论
type PostComment struct {
Message string
Parent string
Thread string
AuthorEmail string
AuthorName string
IpAddress string
Identifier string
UserAgent string
}
type postCreateResp struct {
Code int
Response postDetail
}
// PostCreate 评论文章
func PostCreate(pc *PostComment) (*postCreateResp, error) {
if err := checkDisqusConfig(); err != nil {
return nil, err
}
vals := url.Values{}
vals.Set("api_key", "E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F")
vals.Set("message", pc.Message)
vals.Set("parent", pc.Parent)
vals.Set("thread", pc.Thread)
vals.Set("author_email", pc.AuthorEmail)
vals.Set("author_name", pc.AuthorName)
// vals.Set("state", "approved")
header := http.Header{"Referer": {"https://disqus.com"}}
resp, err := httpPostHeader(apiPostCreate, vals, header)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New(string(b))
}
result := &postCreateResp{}
err = json.Unmarshal(b, result)
if err != nil {
return nil, err
}
return result, nil
}
// approvedResp 批准评论通过
type approvedResp struct {
Code int
Response []struct {
ID string
}
}
// PostApprove 批准评论
func PostApprove(post string) error {
if err := checkDisqusConfig(); err != nil {
return err
}
vals := url.Values{}
vals.Set("api_key", config.Conf.BlogApp.Disqus.PublicKey)
vals.Set("access_token", config.Conf.BlogApp.Disqus.AccessToken)
vals.Set("post", post)
header := http.Header{"Referer": {"https://disqus.com"}}
resp, err := httpPostHeader(apiPostApprove, vals, header)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New(string(b))
}
result := &approvedResp{}
return json.Unmarshal(b, result)
}
// threadCreateResp 创建thread
type threadCreateResp struct {
Code int
Response struct {
ID string
}
}
// ThreadCreate 创建thread
func ThreadCreate(article *model.Article, btitle string) error {
if err := checkDisqusConfig(); err != nil {
return err
}
vals := url.Values{}
vals.Set("api_key", config.Conf.BlogApp.Disqus.PublicKey)
vals.Set("access_token", config.Conf.BlogApp.Disqus.AccessToken)
vals.Set("forum", config.Conf.BlogApp.Disqus.ShortName)
vals.Set("title", article.Title+" | "+btitle)
vals.Set("identifier", "post-"+article.Slug)
urlPath := fmt.Sprintf("https://%s/post/%s.html", config.Conf.BlogApp.Host, article.Slug)
vals.Set("url", urlPath)
resp, err := httpPost(apiThreadCreate, vals)
if err != nil {
return err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.New(string(b))
}
result := &threadCreateResp{}
err = json.Unmarshal(b, result)
if err != nil {
return err
}
article.Thread = result.Response.ID
return nil
}

248
pkg/internal/es.go Normal file
View File

@@ -0,0 +1,248 @@
// Package internal provides ...
package internal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
"time"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/sirupsen/logrus"
)
// search mode
const (
SearchFilter = `"filter":{"bool":{"must":[%s]}}`
SearchTerm = `{"term":{"%s":"%s"}}`
SearchDate = `{"range":{"date":{"gte":"%s","lte": "%s","format": "yyyy-MM-dd||yyyy-MM||yyyy"}}}` // 2016-10||/M
)
func init() {
if checkESConfig() != nil {
return
}
mappings := fmt.Sprintf(`{"mappings":{"%s":{"properties":{"content":{"analyzer":"ik_syno","search_analyzer":"ik_syno","term_vector":"with_positions_offsets","type":"string"},"date":{"index":"not_analyzed","type":"date"},"slug":{"type":"string"},"tag":{"index":"not_analyzed","type":"string"},"title":{"analyzer":"ik_syno","search_analyzer":"ik_syno","term_vector":"with_positions_offsets","type":"string"}}}}}`, "article")
err := createIndexAndMappings("article", "eiblog", []byte(mappings))
if err != nil {
panic(err)
}
}
func checkESConfig() error {
if config.Conf.ESHost == "" {
return errors.New("es: elasticsearch not config")
}
return nil
}
// ElasticSearch 搜索文章
func ElasticSearch(query string, size, from int) (*searchIndexResult, error) {
if err := checkESConfig(); err != nil {
return nil, err
}
// 分析查询
var (
regTerm = regexp.MustCompile(`(tag|slug|date):`)
idxs = regTerm.FindAllStringIndex(query, -1)
length = len(idxs)
str, kw string
filter []string
)
if length == 0 { // 全文搜索
kw = query
}
// 字段搜索,检出,全文搜索
for i, idx := range idxs {
if i == length-1 {
str = query[idx[0]:]
if space := strings.Index(str, " "); space != -1 && space < len(str)-1 {
kw = str[space+1:]
str = str[:space]
}
} else {
str = strings.TrimSpace(query[idx[0]:idxs[i+1][0]])
}
kv := strings.Split(str, ":")
switch kv[0] {
case "slug":
filter = append(filter, fmt.Sprintf(SearchTerm, kv[0], kv[1]))
case "tag":
filter = append(filter, fmt.Sprintf(SearchTerm, kv[0], kv[1]))
case "date":
var date string
switch len(kv[1]) {
case 4:
date = fmt.Sprintf(SearchDate, kv[1], kv[1]+"||/y")
case 7:
date = fmt.Sprintf(SearchDate, kv[1], kv[1]+"||/M")
case 10:
date = fmt.Sprintf(SearchDate, kv[1], kv[1]+"||/d")
default:
break
}
filter = append(filter, date)
}
}
// 判断是否为空,判断搜索方式
dsl := fmt.Sprintf("{"+SearchFilter+"}", strings.Join(filter, ","))
if kw != "" {
dsl = strings.Replace(strings.Replace(`{"highlight":{"fields":{"content":{},"title":{}},"post_tags":["\u003c/b\u003e"],"pre_tags":["\u003cb\u003e"]},"query":{"dis_max":{"queries":[{"match":{"title":{"boost":4,"minimum_should_match":"50%","query":"$1"}}},{"match":{"content":{"boost":4,"minimum_should_match":"75%","query":"$1"}}},{"match":{"tag":{"boost":2,"minimum_should_match":"100%","query":"$1"}}},{"match":{"slug":{"boost":1,"minimum_should_match":"100%","query":"$1"}}}],"tie_breaker":0.3}},$2}`, "$1", kw, -1), "$2", fmt.Sprintf(SearchFilter, strings.Join(filter, ",")), -1)
}
return indexQueryDSL("article", "eiblog", size, from, []byte(dsl))
}
// indicesCreateResult 索引创建结果
type indicesCreateResult struct {
Acknowledged bool `json:"acknowledged"`
}
// createIndexAndMappings 创建索引和映射关系
func createIndexAndMappings(index, typ string, mappings []byte) error {
rawurl := fmt.Sprintf("%s/%s/%s", config.Conf.ESHost, index, typ)
resp, err := httpHead(rawurl)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return nil
}
rawurl = fmt.Sprintf("%s/%s", config.Conf.ESHost, index)
resp, err = httpPut(rawurl, mappings)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
result := indicesCreateResult{}
err = json.Unmarshal(data, &result)
if err != nil {
return err
}
if !result.Acknowledged {
return errors.New(string(data))
}
return nil
}
// indexOrUpdateDocument 创建或更新索引
func indexOrUpdateDocument(index, typ string, id int32, doc []byte) (err error) {
rawurl := fmt.Sprintf("%s/%s/%s/%d", config.Conf.ESHost, index, typ, id)
resp, err := httpPut(rawurl, doc)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
logrus.Debug(string(data))
return nil
}
type deleteIndexReq struct {
Index string `json:"_index"`
Type string `json:"_type"`
ID string `json:"_id"`
}
type deleteIndexResult struct {
Errors bool `json:"errors"`
Iterms []map[string]struct {
Error string `json:"error"`
} `json:"iterms"`
}
// deleteIndexDocument 删除文档
func deleteIndexDocument(index, typ string, ids []string) error {
buf := bytes.Buffer{}
for _, id := range ids {
dd := deleteIndexReq{Index: index, Type: typ, ID: id}
m := map[string]deleteIndexReq{"delete": dd}
b, _ := json.Marshal(m)
buf.Write(b)
buf.WriteByte('\n')
}
rawurl := fmt.Sprintf("%s/_bulk", config.Conf.ESHost)
resp, err := httpPost(rawurl, buf.Bytes())
if err != nil {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
result := deleteIndexResult{}
err = json.Unmarshal(data, &result)
if err != nil {
return err
}
if result.Errors {
for _, iterm := range result.Iterms {
for _, s := range iterm {
if s.Error != "" {
return errors.New(s.Error)
}
}
}
}
return nil
}
// searchIndexResult 查询结果
type searchIndexResult struct {
Took float32 `json:"took"`
Hits struct {
Total int `json:"total"`
Hits []struct {
ID string `json:"_id"`
Source struct {
Slug string `json:"slug"`
Content string `json:"content"`
Date time.Time `json:"date"`
Title string `json:"title"`
Img string `json:"img"`
} `json:"_source"`
Highlight struct {
Title []string `json:"title"`
Content []string `json:"content"`
} `json:"highlight"`
} `json:"hits"`
} `json:"hits"`
}
// indexQueryDSL 语句查询文档
func indexQueryDSL(index, typ string, size, from int, dsl []byte) (*searchIndexResult, error) {
rawurl := fmt.Sprintf("%s/%s/%s/_search?size=%d&from=%d", config.Conf.ESHost,
index, typ, size, from)
resp, err := httpPost(rawurl, dsl)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
result := &searchIndexResult{}
err = json.Unmarshal(data, result)
if err != nil {
return nil, err
}
return result, nil
}

123
pkg/internal/http.go Normal file
View File

@@ -0,0 +1,123 @@
// Package internal provides ...
package internal
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
)
var httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
func newRequest(method, rawurl string, data interface{}) (*http.Request, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
host := u.Host
// 获取主机IP
ips, err := net.LookupHost(u.Host)
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, fmt.Errorf("http: not found ip(%s)", u.Host)
}
u.Host = ips[0]
// 创建HTTP Request
var req *http.Request
switch raw := data.(type) {
case url.Values:
req, err = http.NewRequest(method, u.String(),
strings.NewReader(raw.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
case []byte:
req, err = http.NewRequest(method, u.String(),
bytes.NewReader(raw))
case nil:
req, err = http.NewRequest(method, u.String(), nil)
default:
return nil, fmt.Errorf("http: unsupported data type: %T", data)
}
if err != nil {
return nil, err
}
// 设置Host
req.Host = host
return req, nil
}
// httpHead HTTP HEAD请求
func httpHead(rawurl string) (*http.Response, error) {
req, err := newRequest(http.MethodHead, rawurl, nil)
if err != nil {
return nil, err
}
return httpClient.Do(req)
}
// httpGet HTTP GET请求
func httpGet(rawurl string) (*http.Response, error) {
req, err := newRequest(http.MethodGet, rawurl, nil)
if err != nil {
return nil, err
}
// 发起请求
return httpClient.Do(req)
}
// httpPost HTTP POST请求, 自动识别是否是form
func httpPost(rawurl string, data interface{}) (*http.Response, error) {
req, err := newRequest(http.MethodPost, rawurl, data)
if err != nil {
return nil, err
}
// 发起请求
return httpClient.Do(req)
}
// httpPostHeader HTTP POST请求自定义Header
func httpPostHeader(rawurl string, data interface{},
header http.Header) (*http.Response, error) {
req, err := newRequest(http.MethodPost, rawurl, data)
if err != nil {
return nil, err
}
// set header
req.Header = header
// 发起请求
return httpClient.Do(req)
}
// httpPut HTTP PUT请求
func httpPut(rawurl string, data interface{}) (*http.Response, error) {
req, err := newRequest(http.MethodPut, rawurl, data)
if err != nil {
return nil, err
}
// 发起请求
return httpClient.Do(req)
}

111
pkg/internal/pinger.go Normal file
View File

@@ -0,0 +1,111 @@
// Package internal provides ...
package internal
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/sirupsen/logrus"
)
// feedrPingFunc http://<your-hub-name>.superfeedr.com/
var feedrPingFunc = func(slug string) error {
feedrHost := config.Conf.BlogApp.FeedRPC.FeedrURL
if feedrHost == "" {
return nil
}
vals := url.Values{}
vals.Set("hub.mode", "publish")
vals.Add("hub.url", fmt.Sprintf("https://%s/post/%s.html",
config.Conf.BackupApp.Host, slug))
resp, err := httpPost(feedrHost, vals)
if err != nil {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 204 {
return fmt.Errorf("pinger: status code: %d, %s",
resp.StatusCode, string(data))
}
return nil
}
// rpcPingParam ping to rpc, eg. google baidu
// params:
// BlogName string `xml:"param>value>string"`
// HomePage string `xml:"param>value>string"`
// Article string `xml:"param>value>string"`
// RSS_URL string `xml:"param>value>string"`
type rpcPingParam struct {
XMLName xml.Name `xml:"methodCall"`
MethodName string `xml:"methodName"`
Params struct {
Param [4]rpcValue `xml:"param"`
} `xml:"params"`
}
type rpcValue struct {
Value string `xml:"value>string"`
}
// rpcPingFunc ping rpc
var rpcPingFunc = func(slug string) error {
if len(config.Conf.BlogApp.FeedRPC.PingRPC) == 0 {
return nil
}
param := rpcPingParam{MethodName: "weblogUpdates.extendedPing"}
param.Params.Param = [4]rpcValue{
0: rpcValue{Value: config.Conf.BlogApp.Blogger.BTitle},
1: rpcValue{Value: "https://" + config.Conf.BlogApp.Host},
2: rpcValue{Value: fmt.Sprintf("https://%s/post/%s.html", config.Conf.BlogApp.Host, slug)},
3: rpcValue{Value: "https://" + config.Conf.BlogApp.Host + "/rss.html"},
}
buf := bytes.Buffer{}
buf.WriteString(xml.Header)
enc := xml.NewEncoder(&buf)
if err := enc.Encode(param); err != nil {
return err
}
data := buf.Bytes()
header := http.Header{}
header.Set("Content-Type", "text/xml")
for _, addr := range config.Conf.BlogApp.FeedRPC.PingRPC {
resp, err := httpPostHeader(addr, data, header)
if err != nil {
logrus.Error("rpcPingFunc.httpPostHeader: ", err)
continue
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
logrus.Error("rpcPingFunc.ReadAll: ", err)
continue
}
if resp.StatusCode != 200 {
logrus.Error("rpcPingFunc.failed: ", string(data))
}
}
return nil
}
// PingFunc ping blog article to SE
func PingFunc(slug string) {
err := feedrPingFunc(slug)
if err != nil {
logrus.Error("pinger: PingFunc feedr: ", err)
}
err = rpcPingFunc(slug)
if err != nil {
logrus.Error("pinger: PingFunc: rpc: ", err)
}
}

101
pkg/internal/qiniu.go Normal file
View File

@@ -0,0 +1,101 @@
// Package internal provides ...
package internal
import (
"context"
"errors"
"io"
"path/filepath"
"github.com/eiblog/eiblog/v2/pkg/config"
"github.com/qiniu/api.v7/v7/auth/qbox"
"github.com/qiniu/api.v7/v7/storage"
)
// QiniuUpload 上传文件
func QiniuUpload(name string, size int64, data io.Reader) (string, error) {
if config.Conf.BlogApp.Qiniu.AccessKey == "" ||
config.Conf.BlogApp.Qiniu.SecretKey == "" {
return "", errors.New("qiniu config error")
}
key := completeQiniuKey(name)
mac := qbox.NewMac(config.Conf.BlogApp.Qiniu.AccessKey,
config.Conf.BlogApp.Qiniu.SecretKey)
// 设置上传策略
putPolicy := &storage.PutPolicy{
Scope: config.Conf.BlogApp.Qiniu.Bucket,
Expires: 3600,
InsertOnly: 1,
}
// 上传token
uploadToken := putPolicy.UploadToken(mac)
// 上传配置
cfg := &storage.Config{
Zone: &storage.ZoneHuadong,
UseHTTPS: true,
}
// uploader
uploader := storage.NewFormUploader(cfg)
ret := new(storage.PutRet)
putExtra := &storage.PutExtra{}
err := uploader.Put(context.Background(), ret, uploadToken,
key, data, size, putExtra)
if err != nil {
return "", err
}
url := "https://" + config.Conf.BlogApp.Qiniu.Domain + "/" + key
return url, nil
}
// QiniuDelete 删除文件
func QiniuDelete(name string) error {
key := completeQiniuKey(name)
mac := qbox.NewMac(config.Conf.BlogApp.Qiniu.AccessKey,
config.Conf.BlogApp.Qiniu.SecretKey)
// 上传配置
cfg := &storage.Config{
Zone: &storage.ZoneHuadong,
UseHTTPS: true,
}
// manager
bucketManager := storage.NewBucketManager(mac, cfg)
// Delete
return bucketManager.Delete(config.Conf.BlogApp.Qiniu.Bucket, key)
}
// completeQiniuKey 修复路径
func completeQiniuKey(name string) string {
ext := filepath.Ext(name)
switch ext {
case ".bmp", ".png", ".jpg",
".gif", ".ico", ".jpeg":
name = "blog/img/" + name
case ".mov", ".mp4":
name = "blog/video/" + name
case ".go", ".js", ".css",
".cpp", ".php", ".rb",
".java", ".py", ".sql",
".lua", ".html", ".sh",
".xml", ".cs":
name = "blog/code/" + name
case ".txt", ".md", ".ini",
".yaml", ".yml", ".doc",
".ppt", ".pdf":
name = "blog/document/" + name
case ".zip", ".rar", ".tar",
".gz":
name = "blog/archive/" + name
default:
name = "blog/other/" + name
}
return name
}

55
pkg/mid/language.go Normal file
View File

@@ -0,0 +1,55 @@
// Package mid provides ...
package mid
import (
"strings"
"github.com/gin-gonic/gin"
)
// LangOpts 语言选项
type LangOpts struct {
CookieName string
Default string
Supported []string
}
// isExist language
func (opts LangOpts) isExist(l string) bool {
for _, v := range opts.Supported {
if v == l {
return true
}
}
return false
}
// LangMiddleware set language
func LangMiddleware(opts LangOpts) gin.HandlerFunc {
return func(c *gin.Context) {
lang, err := c.Cookie(opts.CookieName)
// found cookie
if err == nil {
c.Set(opts.CookieName, lang)
return
}
// set cookie
al := strings.ToLower(c.GetHeader("Accept-Language"))
if al != "" {
// choose default if not supported
lang = opts.Default
langs := strings.Split(al, ",")
for _, v := range langs {
if opts.isExist(v) {
lang = v
break
}
}
} else {
lang = opts.Default
}
c.SetCookie(opts.CookieName, lang, 86400*365, "/", "", false, false)
c.Set(opts.CookieName, lang)
}
}

34
pkg/mid/session.go Normal file
View File

@@ -0,0 +1,34 @@
// Package mid provides ...
package mid
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
// SessionOpts 设置选项
type SessionOpts struct {
Name string
Secure bool // required
Secret []byte // required
// redis store
RedisAddr string
RedisPwd string
}
// SessionMiddleware session中间件
func SessionMiddleware(opts SessionOpts) gin.HandlerFunc {
store := cookie.NewStore(opts.Secret)
store.Options(sessions.Options{
MaxAge: 86400 * 30,
Path: "/",
Secure: opts.Secure,
HttpOnly: true,
})
name := "SESSIONID"
if opts.Name != "" {
name = opts.Name
}
return sessions.Sessions(name, store)
}

18
pkg/mid/u.go Normal file
View File

@@ -0,0 +1,18 @@
// Package mid provides ...
package mid
import (
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
)
// UserMiddleware 用户cookie标记
func UserMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
cookie, err := c.Cookie("u")
if err != nil || cookie == "" {
u1 := uuid.Must(uuid.NewV4()).String()
c.SetCookie("u", u1, 86400*730, "/", "", true, true)
}
}
}

19
pkg/model/account.go Normal file
View File

@@ -0,0 +1,19 @@
// Package model provides ...
package model
import "time"
// Account 博客账户
type Account struct {
Username string `gorm:"primaryKey"` // 用户名
Password string `gorm:"not null"` // 密码
Email string `gorm:"not null"` // 邮件地址
PhoneN string `gorm:"not null"` // 手机号
Address string `gorm:"not null"` // 地址信息
LogoutTime time.Time `gorm:"default:null"` // 登出时间
LoginIP string `gorm:"default:null"` // 最近登录IP
LoginUA string `gorm:"default:null"` // 最近登录IP
LoginTime time.Time `gorm:"default:now()"` // 最近登录时间
CreateTime time.Time `gorm:"default:now()"` // 创建时间
}

23
pkg/model/archive.go Normal file
View File

@@ -0,0 +1,23 @@
// Package model provides ...
package model
import "time"
// Archive 归档
type Archive struct {
Time time.Time
Articles SortedArticles `gorm:"-" bson:"-"` // 归档下的文章
}
// SortedArchives 排序后的归档
type SortedArchives []*Archive
// Len 长度
func (s SortedArchives) Len() int { return len(s) }
// Less 比较
func (s SortedArchives) Less(i, j int) bool { return s[i].Time.After(s[j].Time) }
// Swap 交换
func (s SortedArchives) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

40
pkg/model/article.go Normal file
View File

@@ -0,0 +1,40 @@
// Package model provides ...
package model
import "time"
// Article 文章
type Article struct {
ID int32 `gorm:"primaryKey;autoIncrement"` // 自增ID
Author string `gorm:"not null"` // 作者名
Slug string `gorm:"not null;uniqueIndex"` // 文章缩略名
Title string `gorm:"not null"` // 标题
Count int `gorm:"not null"` // 评论数量
Content string `gorm:"not null"` // markdown内容
SerieID int32 `gorm:"not null"` // 专题ID
Tags string `gorm:"not null"` // tag,以逗号隔开
IsDraft bool `gorm:"not null"` // 是否是草稿
DeleteTime time.Time `gorm:"default:null"` // 删除时间
UpdateTime time.Time `gorm:"default:now()"` // 更新时间
CreateTime time.Time `gorm:"default:now()"` // 创建时间
Header string `gorm:"-" bson:"-"` // header
Excerpt string `gorm:"-" bson:"-"` // 预览信息
Desc string `gorm:"-" bson:"-"` // 描述
Thread string `gorm:"-" bson:"-"` // disqus thread
Prev *Article `gorm:"-" bson:"-"` // 上篇文章
Next *Article `gorm:"-" bson:"-"` // 下篇文章
}
// SortedArticles 按时间排序后文章
type SortedArticles []*Article
// Len 长度
func (s SortedArticles) Len() int { return len(s) }
// Less 对比
func (s SortedArticles) Less(i, j int) bool { return s[i].CreateTime.After(s[j].CreateTime) }
// Swap 交换
func (s SortedArticles) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

14
pkg/model/blogger.go Normal file
View File

@@ -0,0 +1,14 @@
// Package model provides ...
package model
// Blogger 博客信息
type Blogger struct {
BlogName string `gorm:"not null"` // 博客名
SubTitle string `gorm:"not null"` // 子标题
BeiAn string `gorm:"not null"` // 备案号
BTitle string `gorm:"not null"` // 底部title
Copyright string `gorm:"not null"` // 版权声明
SeriesSay string `gorm:"not null"` // 专题说明
ArchivesSay string `gorm:"not null"` // 归档说明
}

27
pkg/model/series.go Normal file
View File

@@ -0,0 +1,27 @@
// Package model provides ...
package model
import "time"
// Series 专题
type Series struct {
ID int32 `gorm:"primaryKey;autoIncrement"` // 自增ID
Slug string `gorm:"not null;uniqueIndex"` // 缩略名
Name string `gorm:"not null"` // 专题名
Desc string `gorm:"not null"` // 专题描述
CreateTime time.Time `gorm:"default:now()"` // 创建时间
Articles SortedArticles `gorm:"-" bson:"-"` // 专题下的文章
}
// SortedSeries 排序后专题
type SortedSeries []*Series
// Len 长度
func (s SortedSeries) Len() int { return len(s) }
// Less 比较
func (s SortedSeries) Less(i, j int) bool { return s[i].ID > s[j].ID }
// Swap 交换
func (s SortedSeries) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

4
pkg/proto/Makefile Normal file
View File

@@ -0,0 +1,4 @@
.PHONY: protoc
protoc:
@${PWD}/protoc.sh

View File

@@ -0,0 +1,226 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.13.0
// source: cmd-demo/demo.proto
package cmd_demo
import (
proto "github.com/golang/protobuf/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type UserInfoReq struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
}
func (x *UserInfoReq) Reset() {
*x = UserInfoReq{}
if protoimpl.UnsafeEnabled {
mi := &file_cmd_demo_demo_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UserInfoReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserInfoReq) ProtoMessage() {}
func (x *UserInfoReq) ProtoReflect() protoreflect.Message {
mi := &file_cmd_demo_demo_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserInfoReq.ProtoReflect.Descriptor instead.
func (*UserInfoReq) Descriptor() ([]byte, []int) {
return file_cmd_demo_demo_proto_rawDescGZIP(), []int{0}
}
func (x *UserInfoReq) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
type UserInfoResp struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
}
func (x *UserInfoResp) Reset() {
*x = UserInfoResp{}
if protoimpl.UnsafeEnabled {
mi := &file_cmd_demo_demo_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UserInfoResp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserInfoResp) ProtoMessage() {}
func (x *UserInfoResp) ProtoReflect() protoreflect.Message {
mi := &file_cmd_demo_demo_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserInfoResp.ProtoReflect.Descriptor instead.
func (*UserInfoResp) Descriptor() ([]byte, []int) {
return file_cmd_demo_demo_proto_rawDescGZIP(), []int{1}
}
func (x *UserInfoResp) GetUserId() int64 {
if x != nil {
return x.UserId
}
return 0
}
func (x *UserInfoResp) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
var File_cmd_demo_demo_proto protoreflect.FileDescriptor
var file_cmd_demo_demo_proto_rawDesc = []byte{
0x0a, 0x13, 0x63, 0x6d, 0x64, 0x2d, 0x64, 0x65, 0x6d, 0x6f, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x64, 0x65, 0x6d, 0x6f, 0x22, 0x26, 0x0a, 0x0b, 0x55,
0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
0x72, 0x49, 0x64, 0x22, 0x43, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x65, 0x73, 0x70, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08,
0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x3b, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72,
0x12, 0x33, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x11, 0x2e, 0x64,
0x65, 0x6d, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x1a,
0x12, 0x2e, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x65, 0x73, 0x70, 0x22, 0x00, 0x42, 0x19, 0x5a, 0x17, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63,
0x6d, 0x64, 0x2d, 0x64, 0x65, 0x6d, 0x6f, 0x3b, 0x63, 0x6d, 0x64, 0x5f, 0x64, 0x65, 0x6d, 0x6f,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_cmd_demo_demo_proto_rawDescOnce sync.Once
file_cmd_demo_demo_proto_rawDescData = file_cmd_demo_demo_proto_rawDesc
)
func file_cmd_demo_demo_proto_rawDescGZIP() []byte {
file_cmd_demo_demo_proto_rawDescOnce.Do(func() {
file_cmd_demo_demo_proto_rawDescData = protoimpl.X.CompressGZIP(file_cmd_demo_demo_proto_rawDescData)
})
return file_cmd_demo_demo_proto_rawDescData
}
var file_cmd_demo_demo_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_cmd_demo_demo_proto_goTypes = []interface{}{
(*UserInfoReq)(nil), // 0: demo.UserInfoReq
(*UserInfoResp)(nil), // 1: demo.UserInfoResp
}
var file_cmd_demo_demo_proto_depIdxs = []int32{
0, // 0: demo.User.UserInfo:input_type -> demo.UserInfoReq
1, // 1: demo.User.UserInfo:output_type -> demo.UserInfoResp
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_cmd_demo_demo_proto_init() }
func file_cmd_demo_demo_proto_init() {
if File_cmd_demo_demo_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_cmd_demo_demo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserInfoReq); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_cmd_demo_demo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserInfoResp); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_cmd_demo_demo_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_cmd_demo_demo_proto_goTypes,
DependencyIndexes: file_cmd_demo_demo_proto_depIdxs,
MessageInfos: file_cmd_demo_demo_proto_msgTypes,
}.Build()
File_cmd_demo_demo_proto = out.File
file_cmd_demo_demo_proto_rawDesc = nil
file_cmd_demo_demo_proto_goTypes = nil
file_cmd_demo_demo_proto_depIdxs = nil
}

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
option go_package = "proto/cmd-demo;cmd_demo";
package demo;
message UserInfoReq {
int64 user_id = 1;
}
message UserInfoResp {
int64 user_id = 1;
string username = 2;
}
// User service
service User {
rpc UserInfo(UserInfoReq) returns (UserInfoResp) {}
}

View File

@@ -0,0 +1,97 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package cmd_demo
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion7
// UserClient is the client API for User service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type UserClient interface {
UserInfo(ctx context.Context, in *UserInfoReq, opts ...grpc.CallOption) (*UserInfoResp, error)
}
type userClient struct {
cc grpc.ClientConnInterface
}
func NewUserClient(cc grpc.ClientConnInterface) UserClient {
return &userClient{cc}
}
func (c *userClient) UserInfo(ctx context.Context, in *UserInfoReq, opts ...grpc.CallOption) (*UserInfoResp, error) {
out := new(UserInfoResp)
err := c.cc.Invoke(ctx, "/demo.User/UserInfo", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserServer is the server API for User service.
// All implementations must embed UnimplementedUserServer
// for forward compatibility
type UserServer interface {
UserInfo(context.Context, *UserInfoReq) (*UserInfoResp, error)
mustEmbedUnimplementedUserServer()
}
// UnimplementedUserServer must be embedded to have forward compatible implementations.
type UnimplementedUserServer struct {
}
func (UnimplementedUserServer) UserInfo(context.Context, *UserInfoReq) (*UserInfoResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UserInfo not implemented")
}
func (UnimplementedUserServer) mustEmbedUnimplementedUserServer() {}
// UnsafeUserServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UserServer will
// result in compilation errors.
type UnsafeUserServer interface {
mustEmbedUnimplementedUserServer()
}
func RegisterUserServer(s grpc.ServiceRegistrar, srv UserServer) {
s.RegisterService(&_User_serviceDesc, srv)
}
func _User_UserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UserInfoReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServer).UserInfo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/demo.User/UserInfo",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServer).UserInfo(ctx, req.(*UserInfoReq))
}
return interceptor(ctx, in, info, handler)
}
var _User_serviceDesc = grpc.ServiceDesc{
ServiceName: "demo.User",
HandlerType: (*UserServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "UserInfo",
Handler: _User_UserInfo_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "cmd-demo/demo.proto",
}

11
pkg/proto/protoc.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env sh
set -e
for file in */*.proto; do
if test -f $file; then
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
$file;
fi
done