mirror of
https://github.com/eiblog/eiblog.git
synced 2026-02-10 00:22:27 +08:00
refactor: eiblog
This commit is contained in:
7
pkg/README.md
Normal file
7
pkg/README.md
Normal 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
310
pkg/cache/cache.go
vendored
Normal 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
94
pkg/cache/render/render.go
vendored
Normal 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
2
pkg/cache/store/mongodb.go
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package store provides ...
|
||||
package store
|
||||
70
pkg/cache/store/store.go
vendored
Normal file
70
pkg/cache/store/store.go
vendored
Normal 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
173
pkg/config/config.go
Normal 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
80
pkg/core/blog/api.go
Normal 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))
|
||||
}
|
||||
65
pkg/core/blog/docs/docs.go
Normal file
65
pkg/core/blog/docs/docs.go
Normal 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{})
|
||||
}
|
||||
13
pkg/core/blog/docs/swagger.json
Normal file
13
pkg/core/blog/docs/swagger.json
Normal 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": {}
|
||||
}
|
||||
10
pkg/core/blog/docs/swagger.yaml
Normal file
10
pkg/core/blog/docs/swagger.yaml
Normal 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"
|
||||
49
pkg/core/blog/file/file.go
Normal file
49
pkg/core/blog/file/file.go
Normal 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
163
pkg/core/blog/file/timer.go
Normal 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
293
pkg/core/blog/page/page.go
Normal 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")
|
||||
}
|
||||
15
pkg/core/blog/swag/swag.go
Normal file
15
pkg/core/blog/swag/swag.go
Normal 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
296
pkg/internal/disqus.go
Normal 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
248
pkg/internal/es.go
Normal 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
123
pkg/internal/http.go
Normal 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
111
pkg/internal/pinger.go
Normal 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
101
pkg/internal/qiniu.go
Normal 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
55
pkg/mid/language.go
Normal 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
34
pkg/mid/session.go
Normal 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
18
pkg/mid/u.go
Normal 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
19
pkg/model/account.go
Normal 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
23
pkg/model/archive.go
Normal 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
40
pkg/model/article.go
Normal 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
14
pkg/model/blogger.go
Normal 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
27
pkg/model/series.go
Normal 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
4
pkg/proto/Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
.PHONY: protoc
|
||||
|
||||
protoc:
|
||||
@${PWD}/protoc.sh
|
||||
226
pkg/proto/cmd-demo/demo.pb.go
Normal file
226
pkg/proto/cmd-demo/demo.pb.go
Normal 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
|
||||
}
|
||||
19
pkg/proto/cmd-demo/demo.proto
Normal file
19
pkg/proto/cmd-demo/demo.proto
Normal 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) {}
|
||||
}
|
||||
97
pkg/proto/cmd-demo/demo_grpc.pb.go
Normal file
97
pkg/proto/cmd-demo/demo_grpc.pb.go
Normal 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
11
pkg/proto/protoc.sh
Executable 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
|
||||
Reference in New Issue
Block a user