mirror of
https://github.com/eiblog/eiblog.git
synced 2026-02-10 00:22:27 +08:00
refactor: refactor eiblog
This commit is contained in:
584
pkg/cache/cache.go
vendored
584
pkg/cache/cache.go
vendored
@@ -1,584 +0,0 @@
|
||||
// Package cache provides ...
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache/render"
|
||||
"github.com/eiblog/eiblog/pkg/cache/store"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
// Ei eiblog cache
|
||||
Ei *Cache
|
||||
|
||||
// PagesCh regenerate pages chan
|
||||
PagesCh = make(chan string, 2)
|
||||
// PageSeries the page series regenerate flag
|
||||
PageSeries = "series-md"
|
||||
// PageArchive the page archive regenerate flag
|
||||
PageArchive = "archive-md"
|
||||
|
||||
// ArticleStartID article start id
|
||||
ArticleStartID = 11
|
||||
// TrashArticleExp trash article timeout
|
||||
TrashArticleExp = time.Duration(-48) * time.Hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
// init timezone
|
||||
var err error
|
||||
tools.TimeLocation, err = time.LoadLocation(
|
||||
config.Conf.EiBlogApp.General.Timezone)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// init store
|
||||
logrus.Info("store drivers: ", store.Drivers())
|
||||
store, err := store.NewStore(config.Conf.Database.Driver,
|
||||
config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Ei init
|
||||
Ei = &Cache{
|
||||
lock: sync.Mutex{},
|
||||
Store: store,
|
||||
TagArticles: make(map[string]model.SortedArticles),
|
||||
ArticlesMap: make(map[string]*model.Article),
|
||||
}
|
||||
err = Ei.loadOrInit()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go Ei.regeneratePages()
|
||||
go Ei.timerClean()
|
||||
go Ei.timerDisqus()
|
||||
}
|
||||
|
||||
// Cache 整站缓存
|
||||
type Cache struct {
|
||||
lock sync.Mutex
|
||||
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
|
||||
}
|
||||
|
||||
// AddArticle 添加文章
|
||||
func (c *Cache) AddArticle(article *model.Article) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// store
|
||||
err := c.InsertArticle(context.Background(), article, ArticleStartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 是否是草稿
|
||||
if article.IsDraft {
|
||||
return nil
|
||||
}
|
||||
// 正式发布文章
|
||||
c.refreshCache(article, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepArticle 替换文章
|
||||
func (c *Cache) RepArticle(oldArticle, newArticle *model.Article) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.ArticlesMap[newArticle.Slug] = newArticle
|
||||
render.GenerateExcerptMarkdown(newArticle)
|
||||
if newArticle.ID < ArticleStartID {
|
||||
return
|
||||
}
|
||||
if oldArticle != nil { // 移除旧文章
|
||||
c.refreshCache(oldArticle, true)
|
||||
}
|
||||
c.refreshCache(newArticle, false)
|
||||
}
|
||||
|
||||
// DelArticle 删除文章
|
||||
func (c *Cache) DelArticle(id int) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
article, _ := c.FindArticleByID(id)
|
||||
if article == nil {
|
||||
return nil
|
||||
}
|
||||
// set delete
|
||||
err := c.UpdateArticle(context.Background(), id, map[string]interface{}{
|
||||
"deleted_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// drop from tags,series,archives
|
||||
c.refreshCache(article, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSerie 添加专题
|
||||
func (c *Cache) AddSerie(serie *model.Serie) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
err := c.InsertSerie(context.Background(), serie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Series = append(c.Series, serie)
|
||||
PagesCh <- PageSeries
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelSerie 删除专题
|
||||
func (c *Cache) DelSerie(id int) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
for i, serie := range c.Series {
|
||||
if serie.ID == id {
|
||||
if len(serie.Articles) > 0 {
|
||||
return errors.New("请删除该专题下的所有文章")
|
||||
}
|
||||
err := c.RemoveSerie(context.Background(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Series[i] = nil
|
||||
c.Series = append(c.Series[:i], c.Series[i+1:]...)
|
||||
PagesCh <- PageSeries
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageArticleFE 文章翻页
|
||||
func (c *Cache) PageArticleFE(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 >= ArticleStartID {
|
||||
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
|
||||
}
|
||||
|
||||
// PageArticleBE 后台文章分页
|
||||
func (c *Cache) PageArticleBE(se int, kw string, draft, del bool, p,
|
||||
n int) ([]*model.Article, int) {
|
||||
|
||||
search := store.SearchArticles{
|
||||
Page: p,
|
||||
Limit: n,
|
||||
Fields: make(map[string]interface{}),
|
||||
}
|
||||
if draft {
|
||||
search.Fields[store.SearchArticleDraft] = true
|
||||
} else if del {
|
||||
search.Fields[store.SearchArticleTrash] = true
|
||||
} else {
|
||||
search.Fields[store.SearchArticleDraft] = false
|
||||
if se > 0 {
|
||||
search.Fields[store.SearchArticleSerieID] = se
|
||||
}
|
||||
if kw != "" {
|
||||
search.Fields[store.SearchArticleTitle] = kw
|
||||
}
|
||||
}
|
||||
articles, count, err := c.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
max := count / n
|
||||
if count%n > 0 {
|
||||
max++
|
||||
}
|
||||
return articles, max
|
||||
}
|
||||
|
||||
// FindArticleByID 通过ID查找文章
|
||||
func (c *Cache) FindArticleByID(id int) (*model.Article, int) {
|
||||
for i, article := range c.Articles {
|
||||
if article.ID == id {
|
||||
return article, i
|
||||
}
|
||||
}
|
||||
return nil, -1
|
||||
}
|
||||
|
||||
// refreshCache 刷新缓存
|
||||
func (c *Cache) refreshCache(article *model.Article, del bool) {
|
||||
if del {
|
||||
_, idx := c.FindArticleByID(article.ID)
|
||||
|
||||
delete(c.ArticlesMap, article.Slug)
|
||||
c.Articles = append(c.Articles[:idx], c.Articles[idx+1:]...)
|
||||
// 从链表移除
|
||||
c.recalcLinkedList(article, true)
|
||||
// 从tag、serie、archive移除
|
||||
c.redelArticle(article)
|
||||
return
|
||||
}
|
||||
// 添加文章
|
||||
defer render.GenerateExcerptMarkdown(article)
|
||||
|
||||
c.ArticlesMap[article.Slug] = article
|
||||
c.Articles = append([]*model.Article{article}, c.Articles...)
|
||||
sort.Sort(c.Articles)
|
||||
// 从链表添加
|
||||
c.recalcLinkedList(article, false)
|
||||
// 从tag、serie、archive添加
|
||||
c.readdArticle(article, true)
|
||||
}
|
||||
|
||||
// recalcLinkedList 重算文章链表
|
||||
func (c *Cache) recalcLinkedList(article *model.Article, del bool) {
|
||||
// 删除操作
|
||||
if del {
|
||||
if article.Prev == nil && article.Next != nil {
|
||||
article.Next.Prev = nil
|
||||
} else if article.Prev != nil && article.Next == nil {
|
||||
article.Prev.Next = nil
|
||||
} else if article.Prev != nil && article.Next != nil {
|
||||
article.Prev.Next = article.Next
|
||||
article.Next.Prev = article.Prev
|
||||
}
|
||||
return
|
||||
}
|
||||
// 添加操作
|
||||
_, idx := c.FindArticleByID(article.ID)
|
||||
if idx == 0 && c.Articles[idx+1].ID >= ArticleStartID {
|
||||
article.Next = c.Articles[idx+1]
|
||||
c.Articles[idx+1].Prev = article
|
||||
} else if idx > 0 && c.Articles[idx-1].ID >= ArticleStartID {
|
||||
article.Prev = c.Articles[idx-1]
|
||||
if c.Articles[idx-1].Next != nil {
|
||||
article.Next = c.Articles[idx-1].Next
|
||||
c.Articles[idx-1].Next.Prev = article
|
||||
}
|
||||
c.Articles[idx-1].Next = article
|
||||
}
|
||||
}
|
||||
|
||||
// readdArticle 添加文章到tag、series、archive
|
||||
func (c *Cache) readdArticle(article *model.Article, needSort bool) {
|
||||
// tag
|
||||
for _, tag := range article.Tags {
|
||||
c.TagArticles[tag] = append(c.TagArticles[tag], article)
|
||||
if needSort {
|
||||
sort.Sort(c.TagArticles[tag])
|
||||
}
|
||||
}
|
||||
// series
|
||||
for i, serie := range c.Series {
|
||||
if serie.ID != article.SerieID {
|
||||
continue
|
||||
}
|
||||
c.Series[i].Articles = append(c.Series[i].Articles, article)
|
||||
if needSort {
|
||||
sort.Sort(c.Series[i].Articles)
|
||||
PagesCh <- PageSeries // 重建专题
|
||||
}
|
||||
}
|
||||
// archive
|
||||
y, m, _ := article.CreatedAt.Date()
|
||||
for i, archive := range c.Archives {
|
||||
ay, am, _ := archive.Time.Date()
|
||||
if y != ay || m != am {
|
||||
continue
|
||||
}
|
||||
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.CreatedAt,
|
||||
Articles: model.SortedArticles{article},
|
||||
})
|
||||
if needSort { // 重建归档
|
||||
PagesCh <- PageArchive
|
||||
}
|
||||
}
|
||||
|
||||
// redelArticle 从tag、series、archive删除文章
|
||||
func (c *Cache) redelArticle(article *model.Article) {
|
||||
// tag
|
||||
for _, tag := range article.Tags {
|
||||
for i, v := range c.TagArticles[tag] {
|
||||
if v == article {
|
||||
c.TagArticles[tag] = append(c.TagArticles[tag][0:i], c.TagArticles[tag][i+1:]...)
|
||||
if len(c.TagArticles[tag]) == 0 {
|
||||
delete(c.TagArticles, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// serie
|
||||
for i, serie := range c.Series {
|
||||
if serie.ID == article.SerieID {
|
||||
for j, v := range serie.Articles {
|
||||
if v == article {
|
||||
c.Series[i].Articles = append(c.Series[i].Articles[0:j],
|
||||
c.Series[i].Articles[j+1:]...)
|
||||
PagesCh <- PageSeries
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// archive
|
||||
for i, archive := range c.Archives {
|
||||
ay, am, _ := archive.Time.Date()
|
||||
if y, m, _ := article.CreatedAt.Date(); ay == y && am == m {
|
||||
for j, v := range archive.Articles {
|
||||
if v == article {
|
||||
c.Archives[i].Articles = append(c.Archives[i].Articles[0:j],
|
||||
c.Archives[i].Articles[j+1:]...)
|
||||
if len(c.Archives[i].Articles) == 0 {
|
||||
c.Archives = append(c.Archives[:i], c.Archives[i+1:]...)
|
||||
}
|
||||
PagesCh <- PageArchive
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadOrInit 读取数据或初始化
|
||||
func (c *Cache) loadOrInit() error {
|
||||
blogapp := config.Conf.EiBlogApp
|
||||
// blogger
|
||||
blogger := &model.Blogger{
|
||||
BlogName: strings.Title(blogapp.Account.Username),
|
||||
SubTitle: "Rome was not built in one day.",
|
||||
BeiAn: "蜀ICP备xxxxxxxx号-1",
|
||||
BTitle: fmt.Sprintf("%s's Blog", strings.Title(blogapp.Account.Username)),
|
||||
Copyright: `本站使用「<a href="//creativecommons.org/licenses/by/4.0/">署名 4.0 国际</a>」创作共享协议,转载请注明作者及原网址。`,
|
||||
}
|
||||
created, err := c.LoadInsertBlogger(context.Background(), blogger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Blogger = blogger
|
||||
if created { // init articles: about blogroll
|
||||
about := &model.Article{
|
||||
ID: 1, // 固定ID
|
||||
Author: blogapp.Account.Username,
|
||||
Title: "关于",
|
||||
Slug: "about",
|
||||
CreatedAt: time.Time{}.AddDate(0, 0, 1),
|
||||
}
|
||||
err = c.InsertArticle(context.Background(), about, ArticleStartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 推送到 disqus
|
||||
go internal.ThreadCreate(about, blogger.BTitle)
|
||||
blogroll := &model.Article{
|
||||
ID: 2, // 固定ID
|
||||
Author: blogapp.Account.Username,
|
||||
Title: "友情链接",
|
||||
Slug: "blogroll",
|
||||
CreatedAt: time.Time{}.AddDate(0, 0, 7),
|
||||
}
|
||||
err = c.InsertArticle(context.Background(), blogroll, ArticleStartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// account
|
||||
pwd := tools.EncryptPasswd(blogapp.Account.Username,
|
||||
blogapp.Account.Password)
|
||||
|
||||
account := &model.Account{
|
||||
Username: blogapp.Account.Username,
|
||||
Password: pwd,
|
||||
}
|
||||
_, err = c.LoadInsertAccount(context.Background(), account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Account = account
|
||||
// series
|
||||
series, err := c.LoadAllSerie(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Series = series
|
||||
// all articles
|
||||
search := store.SearchArticles{
|
||||
Page: 1,
|
||||
Limit: 9999,
|
||||
Fields: map[string]interface{}{store.SearchArticleDraft: false},
|
||||
}
|
||||
articles, _, err := c.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, v := range articles {
|
||||
// 渲染页面
|
||||
render.GenerateExcerptMarkdown(v)
|
||||
|
||||
c.ArticlesMap[v.Slug] = v
|
||||
// 分析文章
|
||||
if v.ID < ArticleStartID {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
v.Prev = articles[i-1]
|
||||
}
|
||||
if i < len(articles)-1 &&
|
||||
articles[i+1].ID >= ArticleStartID {
|
||||
v.Next = articles[i+1]
|
||||
}
|
||||
c.readdArticle(v, false)
|
||||
}
|
||||
Ei.Articles = articles
|
||||
// 重建专题与归档
|
||||
PagesCh <- PageSeries
|
||||
PagesCh <- PageArchive
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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>\n",
|
||||
article.Title, article.Slug, article.CreatedAt.Format("Jan 02, 2006"))
|
||||
buf.WriteString(str)
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
c.PageSeries = string(render.PageRender(buf.Bytes()))
|
||||
case PageArchive:
|
||||
sort.Sort(c.Archives)
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(c.Blogger.ArchivesSay + "\n")
|
||||
var (
|
||||
currentYear string
|
||||
gt12Month = len(c.Archives) > 12
|
||||
)
|
||||
for _, archive := range c.Archives {
|
||||
t := archive.Time.In(tools.TimeLocation)
|
||||
if gt12Month {
|
||||
year := t.Format("2006 年")
|
||||
if currentYear != year {
|
||||
currentYear = year
|
||||
buf.WriteString(fmt.Sprintf("\n### %s\n\n", t.Format("2006 年")))
|
||||
}
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("\n### %s\n\n", t.Format("2006年1月")))
|
||||
}
|
||||
for i, article := range archive.Articles {
|
||||
createdAt := article.CreatedAt.In(tools.TimeLocation)
|
||||
if i == 0 && gt12Month {
|
||||
str := fmt.Sprintf("* *[%s](/post/%s.html) <span class=\"date\">(%s)</span>*\n",
|
||||
article.Title, article.Slug, createdAt.Format("Jan 02, 2006"))
|
||||
buf.WriteString(str)
|
||||
} else {
|
||||
str := fmt.Sprintf("* [%s](/post/%s.html) <span class=\"date\">(%s)</span>\n",
|
||||
article.Title, article.Slug, createdAt.Format("Jan 02, 2006"))
|
||||
buf.WriteString(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.PageArchives = string(render.PageRender(buf.Bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timerClean 定时清理文章
|
||||
func (c *Cache) timerClean() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
|
||||
for now := range ticker.C {
|
||||
exp := now.Add(TrashArticleExp)
|
||||
err := c.CleanArticles(context.Background(), exp)
|
||||
if err != nil {
|
||||
logrus.Error("cache.timerClean.CleanArticles: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// timerDisqus disqus定时操作
|
||||
func (c *Cache) timerDisqus() {
|
||||
ticker := time.NewTicker(5 * time.Hour)
|
||||
|
||||
for range ticker.C {
|
||||
err := internal.PostsCount(c.ArticlesMap)
|
||||
if err != nil {
|
||||
logrus.Error("cache.timerDisqus.PostsCount: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
pkg/cache/render/render.go
vendored
85
pkg/cache/render/render.go
vendored
@@ -1,85 +0,0 @@
|
||||
// Package render provides ...
|
||||
package render
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"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.EiBlogApp.General.Identifier)
|
||||
// header
|
||||
regHeader = regexp.MustCompile("</nav></div>")
|
||||
)
|
||||
|
||||
// PageRender 渲染markdown
|
||||
func PageRender(md []byte) []byte {
|
||||
renderer := blackfriday.HtmlRenderer(commonHTMLFlags, "", "")
|
||||
return blackfriday.Markdown(md, renderer, commonExtensions)
|
||||
}
|
||||
|
||||
// GenerateExcerptMarkdown 生成预览和描述
|
||||
func GenerateExcerptMarkdown(article *model.Article) {
|
||||
blogapp := config.Conf.EiBlogApp
|
||||
|
||||
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 = tools.IgnoreHTMLTag(prefix)
|
||||
article.Content = article.Content[index:]
|
||||
}
|
||||
|
||||
// 查找目录
|
||||
content := PageRender([]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 = tools.IgnoreHTMLTag(article.Content[:index[0]])
|
||||
return
|
||||
}
|
||||
uc := []rune(article.Content)
|
||||
length := blogapp.General.Length
|
||||
if len(uc) < length {
|
||||
length = len(uc)
|
||||
}
|
||||
article.Excerpt = tools.IgnoreHTMLTag(string(uc[0:length]))
|
||||
}
|
||||
360
pkg/cache/store/mongodb.go
vendored
360
pkg/cache/store/mongodb.go
vendored
@@ -1,360 +0,0 @@
|
||||
// Package store provides ...
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
// example:
|
||||
// driver: mongodb
|
||||
// source: mongodb://localhost:27017
|
||||
|
||||
const (
|
||||
mongoDBName = "eiblog"
|
||||
collectionAccount = "account"
|
||||
collectionArticle = "article"
|
||||
collectionBlogger = "blogger"
|
||||
collectionCounter = "counter"
|
||||
collectionSerie = "serie"
|
||||
|
||||
counterNameSerie = "serie"
|
||||
counterNameArticle = "article"
|
||||
)
|
||||
|
||||
type mongodb struct {
|
||||
*mongo.Client
|
||||
}
|
||||
|
||||
// Init init mongodb client
|
||||
func (db *mongodb) Init(name, source string) (Store, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opts := options.Client().ApplyURI(source)
|
||||
client, err := mongo.Connect(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = client.Ping(ctx, readpref.Primary())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Client = client
|
||||
// create index
|
||||
indexModel := mongo.IndexModel{
|
||||
Keys: bson.D{bson.E{Key: "username", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
}
|
||||
db.Database(mongoDBName).Collection(collectionAccount).
|
||||
Indexes().
|
||||
CreateOne(context.Background(), indexModel)
|
||||
indexModel = mongo.IndexModel{
|
||||
Keys: bson.D{bson.E{Key: "slug", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
}
|
||||
db.Database(mongoDBName).Collection(collectionArticle).
|
||||
Indexes().
|
||||
CreateOne(context.Background(), indexModel)
|
||||
indexModel = mongo.IndexModel{
|
||||
Keys: bson.D{bson.E{Key: "slug", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).SetSparse(true),
|
||||
}
|
||||
db.Database(mongoDBName).Collection(collectionSerie).
|
||||
Indexes().
|
||||
CreateOne(context.Background(), indexModel)
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// LoadInsertBlogger 读取或创建博客
|
||||
func (db *mongodb) LoadInsertBlogger(ctx context.Context,
|
||||
blogger *model.Blogger) (created bool, err error) {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionBlogger)
|
||||
|
||||
filter := bson.M{}
|
||||
result := collection.FindOne(ctx, filter)
|
||||
err = result.Err()
|
||||
if err != nil {
|
||||
if err != mongo.ErrNoDocuments {
|
||||
return
|
||||
}
|
||||
_, err = collection.InsertOne(ctx, blogger)
|
||||
created = true
|
||||
} else {
|
||||
err = result.Decode(blogger)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateBlogger 更新博客
|
||||
func (db *mongodb) UpdateBlogger(ctx context.Context,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionBlogger)
|
||||
|
||||
filter := bson.M{}
|
||||
params := bson.M{}
|
||||
for k, v := range fields {
|
||||
params[k] = v
|
||||
}
|
||||
update := bson.M{"$set": params}
|
||||
_, err := collection.UpdateOne(ctx, filter, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadInsertAccount 读取或创建账户
|
||||
func (db *mongodb) LoadInsertAccount(ctx context.Context,
|
||||
acct *model.Account) (created bool, err error) {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionAccount)
|
||||
|
||||
filter := bson.M{"username": acct.Username}
|
||||
result := collection.FindOne(ctx, filter)
|
||||
err = result.Err()
|
||||
if err != nil {
|
||||
if err != mongo.ErrNoDocuments {
|
||||
return
|
||||
}
|
||||
_, err = collection.InsertOne(ctx, acct)
|
||||
created = true
|
||||
} else {
|
||||
err = result.Decode(acct)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateAccount 更新账户
|
||||
func (db *mongodb) UpdateAccount(ctx context.Context, name string,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionAccount)
|
||||
|
||||
filter := bson.M{"username": name}
|
||||
params := bson.M{}
|
||||
for k, v := range fields {
|
||||
params[k] = v
|
||||
}
|
||||
update := bson.M{"$set": params}
|
||||
_, err := collection.UpdateOne(ctx, filter, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// InsertSerie 创建专题
|
||||
func (db *mongodb) InsertSerie(ctx context.Context, serie *model.Serie) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
|
||||
serie.ID = db.nextValue(ctx, counterNameSerie)
|
||||
_, err := collection.InsertOne(ctx, serie)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveSerie 删除专题
|
||||
func (db *mongodb) RemoveSerie(ctx context.Context, id int) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
_, err := collection.DeleteOne(ctx, filter)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateSerie 更新专题
|
||||
func (db *mongodb) UpdateSerie(ctx context.Context, id int,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
params := bson.M{}
|
||||
for k, v := range fields {
|
||||
params[k] = v
|
||||
}
|
||||
update := bson.M{"$set": params}
|
||||
_, err := collection.UpdateOne(ctx, filter, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAllSerie 查询所有专题
|
||||
func (db *mongodb) LoadAllSerie(ctx context.Context) (model.SortedSeries, error) {
|
||||
collection := db.Database(mongoDBName).Collection(collectionSerie)
|
||||
|
||||
opts := options.Find().SetSort(bson.M{"id": -1})
|
||||
filter := bson.M{}
|
||||
cur, err := collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cur.Close(ctx)
|
||||
|
||||
var series model.SortedSeries
|
||||
for cur.Next(ctx) {
|
||||
obj := model.Serie{}
|
||||
err = cur.Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
series = append(series, &obj)
|
||||
}
|
||||
return series, nil
|
||||
}
|
||||
|
||||
// InsertArticle 创建文章
|
||||
func (db *mongodb) InsertArticle(ctx context.Context, article *model.Article, startID int) error {
|
||||
// 可手动分配ID或者分配ID, 占位至起始id
|
||||
for article.ID == 0 {
|
||||
id := db.nextValue(ctx, counterNameArticle)
|
||||
if id < startID {
|
||||
continue
|
||||
} else {
|
||||
article.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
_, err := collection.InsertOne(ctx, article)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveArticle 硬删除文章
|
||||
func (db *mongodb) RemoveArticle(ctx context.Context, id int) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
_, err := collection.DeleteOne(ctx, filter)
|
||||
return err
|
||||
}
|
||||
|
||||
// CleanArticles 清理回收站文章
|
||||
func (db *mongodb) CleanArticles(ctx context.Context, exp time.Time) error {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
|
||||
// 超过两天自动删除
|
||||
filter := bson.M{"deleted_at": bson.M{"$gt": time.Time{}, "$lt": exp}}
|
||||
_, err := collection.DeleteMany(ctx, filter)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateArticle 更新文章
|
||||
func (db *mongodb) UpdateArticle(ctx context.Context, id int,
|
||||
fields map[string]interface{}) error {
|
||||
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
params := bson.M{}
|
||||
for k, v := range fields {
|
||||
params[k] = v
|
||||
}
|
||||
update := bson.M{"$set": params}
|
||||
_, err := collection.UpdateOne(ctx, filter, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadArticle 查找文章
|
||||
func (db *mongodb) LoadArticle(ctx context.Context, id int) (*model.Article, error) {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{"id": id}
|
||||
result := collection.FindOne(ctx, filter)
|
||||
err := result.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
article := &model.Article{}
|
||||
err = result.Decode(article)
|
||||
return article, err
|
||||
}
|
||||
|
||||
// LoadArticleList 获取文章列表
|
||||
func (db *mongodb) LoadArticleList(ctx context.Context, search SearchArticles) (
|
||||
model.SortedArticles, int, error) {
|
||||
collection := db.Database(mongoDBName).Collection(collectionArticle)
|
||||
|
||||
filter := bson.M{}
|
||||
for k, v := range search.Fields {
|
||||
switch k {
|
||||
case SearchArticleDraft:
|
||||
if ok := v.(bool); ok {
|
||||
filter["is_draft"] = true
|
||||
} else {
|
||||
filter["is_draft"] = false
|
||||
filter["deleted_at"] = bson.M{"$eq": time.Time{}}
|
||||
}
|
||||
case SearchArticleTitle:
|
||||
filter["title"] = bson.M{
|
||||
"$regex": v.(string),
|
||||
"$options": "$i",
|
||||
}
|
||||
case SearchArticleSerieID:
|
||||
filter["serie_id"] = v.(int)
|
||||
case SearchArticleTrash:
|
||||
filter["deleted_at"] = bson.M{"$ne": time.Time{}}
|
||||
}
|
||||
}
|
||||
// search count
|
||||
count, err := collection.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
opts := options.Find().SetLimit(int64(search.Limit)).
|
||||
SetSkip(int64((search.Page - 1) * search.Limit)).
|
||||
SetSort(bson.M{"created_at": -1})
|
||||
cur, err := collection.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer cur.Close(ctx)
|
||||
|
||||
var articles model.SortedArticles
|
||||
for cur.Next(ctx) {
|
||||
obj := model.Article{}
|
||||
err = cur.Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
articles = append(articles, &obj)
|
||||
}
|
||||
sort.Sort(articles)
|
||||
return articles, int(count), nil
|
||||
}
|
||||
|
||||
// DropDatabase drop eiblog database
|
||||
func (db *mongodb) DropDatabase(ctx context.Context) error {
|
||||
return db.Database(mongoDBName).Drop(ctx)
|
||||
}
|
||||
|
||||
// counter counter
|
||||
type counter struct {
|
||||
Name string
|
||||
NextVal int
|
||||
}
|
||||
|
||||
// nextValue counter value
|
||||
func (db *mongodb) nextValue(ctx context.Context, name string) int {
|
||||
collection := db.Database(mongoDBName).Collection(collectionCounter)
|
||||
|
||||
opts := options.FindOneAndUpdate().SetUpsert(true).
|
||||
SetReturnDocument(options.After)
|
||||
filter := bson.M{"name": name}
|
||||
update := bson.M{"$inc": bson.M{"nextval": 1}}
|
||||
|
||||
next := counter{}
|
||||
err := collection.FindOneAndUpdate(ctx, filter, update, opts).Decode(&next)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return next.NextVal
|
||||
}
|
||||
|
||||
// register store
|
||||
func init() {
|
||||
Register("mongodb", &mongodb{})
|
||||
}
|
||||
183
pkg/cache/store/mongodb_test.go
vendored
183
pkg/cache/store/mongodb_test.go
vendored
@@ -1,183 +0,0 @@
|
||||
// Package store provides ...
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
)
|
||||
|
||||
var (
|
||||
store Store
|
||||
acct *model.Account
|
||||
blogger *model.Blogger
|
||||
series *model.Serie
|
||||
article *model.Article
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
store, err = NewStore("mongodb", "mongodb://127.0.0.1:27017")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// account
|
||||
acct = &model.Account{
|
||||
Username: "deepzz",
|
||||
Password: "deepzz",
|
||||
Email: "deepzz@example.com",
|
||||
PhoneN: "12345678900",
|
||||
Address: "address",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
// blogger
|
||||
blogger = &model.Blogger{
|
||||
BlogName: "Deepzz",
|
||||
SubTitle: "不抛弃,不放弃",
|
||||
BeiAn: "beian",
|
||||
BTitle: "Deepzz's Blog",
|
||||
Copyright: "Copyright",
|
||||
}
|
||||
// series
|
||||
series = &model.Serie{
|
||||
Slug: "slug",
|
||||
Name: "series name",
|
||||
Desc: "series desc",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
// article
|
||||
article = &model.Article{
|
||||
Author: "deepzz",
|
||||
Slug: "slug",
|
||||
Title: "title",
|
||||
Count: 0,
|
||||
Content: "### count",
|
||||
SerieID: 0,
|
||||
Tags: nil,
|
||||
IsDraft: false,
|
||||
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInsertAccount(t *testing.T) {
|
||||
ok, err := store.LoadInsertAccount(context.Background(), acct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ok)
|
||||
}
|
||||
|
||||
func TestUpdateAccount(t *testing.T) {
|
||||
err := store.UpdateAccount(context.Background(), "deepzz", map[string]interface{}{
|
||||
"phonn": "09876543211",
|
||||
"loginua": "chrome",
|
||||
"password": "123456",
|
||||
"logintime": time.Now(),
|
||||
"logouttime": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInsertBlogger(t *testing.T) {
|
||||
ok, err := store.LoadInsertBlogger(context.Background(), blogger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ok)
|
||||
}
|
||||
|
||||
func TestUpdateBlogger(t *testing.T) {
|
||||
err := store.UpdateBlogger(context.Background(), map[string]interface{}{
|
||||
"blogname": "blogname",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertSeries(t *testing.T) {
|
||||
err := store.InsertSerie(context.Background(), series)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveSeries(t *testing.T) {
|
||||
err := store.RemoveSerie(context.Background(), 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSeries(t *testing.T) {
|
||||
err := store.UpdateSerie(context.Background(), 2, map[string]interface{}{
|
||||
"desc": "update desc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAllSeries(t *testing.T) {
|
||||
series, err := store.LoadAllSerie(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("load all series: %d", len(series))
|
||||
}
|
||||
|
||||
func TestInsertArticle(t *testing.T) {
|
||||
article.ID = 12
|
||||
err := store.InsertArticle(context.Background(), article, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveArticle(t *testing.T) {
|
||||
err := store.RemoveArticle(context.Background(), 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteArticle(t *testing.T) {
|
||||
err := store.RemoveArticle(context.Background(), 12)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanArticles(t *testing.T) {
|
||||
err := store.CleanArticles(context.Background(), time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateArticle(t *testing.T) {
|
||||
err := store.UpdateArticle(context.Background(), 13, map[string]interface{}{
|
||||
"title": "new title",
|
||||
"updatetime": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAllArticle(t *testing.T) {
|
||||
_, total, err := store.LoadArticleList(context.Background(), SearchArticles{
|
||||
Page: 1,
|
||||
Limit: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("load all articles: %d", total)
|
||||
}
|
||||
206
pkg/cache/store/rdbms.go
vendored
206
pkg/cache/store/rdbms.go
vendored
@@ -1,206 +0,0 @@
|
||||
// Package store provides ...
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
|
||||
"gorm.io/driver/clickhouse"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// example:
|
||||
// driver: mysql
|
||||
// source: user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
//
|
||||
// driver: postgres
|
||||
// source: host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable
|
||||
//
|
||||
// driver: sqlite
|
||||
// source: /path/gorm.db
|
||||
//
|
||||
// driver: sqlserver
|
||||
// source: sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm
|
||||
//
|
||||
// driver: clickhouse
|
||||
// source: tcp://localhost:9000?database=gorm&username=gorm&password=gorm&read_timeout=10&write_timeout=20
|
||||
|
||||
type rdbms struct {
|
||||
*gorm.DB
|
||||
}
|
||||
|
||||
// Init 数据库初始化, 建表, 加索引操作等
|
||||
// name 应该为具体的关系数据库驱动名
|
||||
func (db *rdbms) Init(name, source string) (Store, error) {
|
||||
var (
|
||||
gormDB *gorm.DB
|
||||
err error
|
||||
)
|
||||
switch name {
|
||||
case "mysql":
|
||||
// https://github.com/go-sql-driver/mysql
|
||||
gormDB, err = gorm.Open(mysql.Open(source), &gorm.Config{})
|
||||
case "postgres":
|
||||
// https://github.com/go-gorm/postgres
|
||||
gormDB, err = gorm.Open(postgres.Open(source), &gorm.Config{})
|
||||
case "sqlite":
|
||||
// github.com/mattn/go-sqlite3
|
||||
gormDB, err = gorm.Open(sqlite.Open(source), &gorm.Config{})
|
||||
case "sqlserver":
|
||||
// github.com/denisenkom/go-mssqldb
|
||||
gormDB, err = gorm.Open(sqlserver.Open(source), &gorm.Config{})
|
||||
case "clickhouse":
|
||||
gormDB, err = gorm.Open(clickhouse.Open(source), &gorm.Config{})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// auto migrate
|
||||
gormDB.AutoMigrate(
|
||||
&model.Account{},
|
||||
&model.Blogger{},
|
||||
&model.Article{},
|
||||
&model.Serie{},
|
||||
)
|
||||
db.DB = gormDB
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// LoadInsertBlogger 读取或创建博客
|
||||
func (db *rdbms) LoadInsertBlogger(ctx context.Context, blogger *model.Blogger) (bool, error) {
|
||||
result := db.FirstOrCreate(blogger)
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// UpdateBlogger 更新博客
|
||||
func (db *rdbms) UpdateBlogger(ctx context.Context, fields map[string]interface{}) error {
|
||||
return db.Model(model.Blogger{}).Session(&gorm.Session{AllowGlobalUpdate: true}).
|
||||
Updates(fields).Error
|
||||
}
|
||||
|
||||
// LoadInsertAccount 读取或创建账户
|
||||
func (db *rdbms) LoadInsertAccount(ctx context.Context, acct *model.Account) (bool, error) {
|
||||
result := db.Where("username=?", acct.Username).FirstOrCreate(acct)
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// UpdateAccount 更新账户
|
||||
func (db *rdbms) UpdateAccount(ctx context.Context, name string, fields map[string]interface{}) error {
|
||||
return db.Model(model.Account{}).Where("username=?", name).Updates(fields).Error
|
||||
}
|
||||
|
||||
// InsertSerie 创建专题
|
||||
func (db *rdbms) InsertSerie(ctx context.Context, serie *model.Serie) error {
|
||||
return db.Create(serie).Error
|
||||
}
|
||||
|
||||
// RemoveSerie 删除专题
|
||||
func (db *rdbms) RemoveSerie(ctx context.Context, id int) error {
|
||||
return db.Where("id=?", id).Delete(model.Serie{}).Error
|
||||
}
|
||||
|
||||
// UpdateSerie 更新专题
|
||||
func (db *rdbms) UpdateSerie(ctx context.Context, id int, fields map[string]interface{}) error {
|
||||
return db.Model(model.Serie{}).Where("id=?", id).Updates(fields).Error
|
||||
}
|
||||
|
||||
// LoadAllSerie 读取所有专题
|
||||
func (db *rdbms) LoadAllSerie(ctx context.Context) (model.SortedSeries, error) {
|
||||
var series model.SortedSeries
|
||||
err := db.Order("id DESC").Find(&series).Error
|
||||
return series, err
|
||||
}
|
||||
|
||||
// InsertArticle 创建文章
|
||||
func (db *rdbms) InsertArticle(ctx context.Context, article *model.Article, startID int) error {
|
||||
if article.ID == 0 {
|
||||
// auto generate id
|
||||
var id int
|
||||
err := db.Model(model.Article{}).Select("MAX(id)").Row().Scan(&id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id < startID {
|
||||
id = startID
|
||||
} else {
|
||||
id++
|
||||
}
|
||||
article.ID = id
|
||||
}
|
||||
return db.Create(article).Error
|
||||
}
|
||||
|
||||
// RemoveArticle 硬删除文章
|
||||
func (db *rdbms) RemoveArticle(ctx context.Context, id int) error {
|
||||
return db.Where("id=?", id).Delete(model.Article{}).Error
|
||||
}
|
||||
|
||||
// CleanArticles 清理回收站文章
|
||||
func (db *rdbms) CleanArticles(ctx context.Context, exp time.Time) error {
|
||||
return db.Where("deleted_at > ? AND deleted_at < ?", time.Time{}, exp).Delete(model.Article{}).Error
|
||||
}
|
||||
|
||||
// UpdateArticle 更新文章
|
||||
func (db *rdbms) UpdateArticle(ctx context.Context, id int, fields map[string]interface{}) error {
|
||||
return db.Model(model.Article{}).Where("id=?", id).Updates(fields).Error
|
||||
}
|
||||
|
||||
// LoadArticle 查找文章
|
||||
func (db *rdbms) LoadArticle(ctx context.Context, id int) (*model.Article, error) {
|
||||
article := &model.Article{}
|
||||
err := db.Where("id=?", id).First(article).Error
|
||||
return article, err
|
||||
}
|
||||
|
||||
// LoadArticleList 查找文章列表
|
||||
func (db *rdbms) LoadArticleList(ctx context.Context, search SearchArticles) (model.SortedArticles, int, error) {
|
||||
gormDB := db.Model(model.Article{})
|
||||
for k, v := range search.Fields {
|
||||
switch k {
|
||||
case SearchArticleDraft:
|
||||
if ok := v.(bool); ok {
|
||||
gormDB = gormDB.Where("is_draft=?", true)
|
||||
} else {
|
||||
gormDB = gormDB.Where("is_draft=? AND deleted_at=?", false, time.Time{})
|
||||
}
|
||||
case SearchArticleTitle:
|
||||
gormDB = gormDB.Where("title LIKE ?", "%"+v.(string)+"%")
|
||||
case SearchArticleSerieID:
|
||||
gormDB = gormDB.Where("serie_id=?", v.(int))
|
||||
case SearchArticleTrash:
|
||||
gormDB = gormDB.Where("deleted_at!=?", time.Time{})
|
||||
}
|
||||
}
|
||||
// search count
|
||||
var count int64
|
||||
err := gormDB.Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var articles model.SortedArticles
|
||||
err = gormDB.Limit(search.Limit).
|
||||
Offset((search.Page - 1) * search.Limit).
|
||||
Order("created_at DESC").Find(&articles).Error
|
||||
return articles, int(count), err
|
||||
}
|
||||
|
||||
// DropDatabase drop eiblog database
|
||||
func (db *rdbms) DropDatabase(ctx context.Context) error {
|
||||
return errors.New("can not drop eiblog database in rdbms")
|
||||
}
|
||||
|
||||
// register store
|
||||
func init() {
|
||||
Register("mysql", &rdbms{})
|
||||
Register("postgres", &rdbms{})
|
||||
Register("sqlite", &rdbms{})
|
||||
Register("sqlserver", &rdbms{})
|
||||
Register("clickhouse", &rdbms{})
|
||||
}
|
||||
114
pkg/cache/store/store.go
vendored
114
pkg/cache/store/store.go
vendored
@@ -1,114 +0,0 @@
|
||||
// Package store provides ...
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
)
|
||||
|
||||
var (
|
||||
storeMu sync.RWMutex
|
||||
stores = make(map[string]Driver)
|
||||
)
|
||||
|
||||
// search field
|
||||
const (
|
||||
SearchArticleDraft = "draft"
|
||||
SearchArticleTrash = "trash"
|
||||
SearchArticleTitle = "title"
|
||||
SearchArticleSerieID = "serieid"
|
||||
)
|
||||
|
||||
// SearchArticles 搜索字段
|
||||
type SearchArticles struct {
|
||||
Page int // 第几页/1
|
||||
Limit int // 每页大小
|
||||
Fields map[string]interface{} // 字段:值
|
||||
}
|
||||
|
||||
// Store 存储后端
|
||||
type Store interface {
|
||||
// LoadInsertBlogger 读取或创建博客
|
||||
LoadInsertBlogger(ctx context.Context, blogger *model.Blogger) (bool, error)
|
||||
// UpdateBlogger 更新博客
|
||||
UpdateBlogger(ctx context.Context, fields map[string]interface{}) error
|
||||
|
||||
// LoadInsertAccount 读取或创建账户
|
||||
LoadInsertAccount(ctx context.Context, acct *model.Account) (bool, error)
|
||||
// UpdateAccount 更新账户
|
||||
UpdateAccount(ctx context.Context, name string, fields map[string]interface{}) error
|
||||
|
||||
// InsertSerie 创建专题
|
||||
InsertSerie(ctx context.Context, serie *model.Serie) error
|
||||
// RemoveSerie 删除专题
|
||||
RemoveSerie(ctx context.Context, id int) error
|
||||
// UpdateSerie 更新专题
|
||||
UpdateSerie(ctx context.Context, id int, fields map[string]interface{}) error
|
||||
// LoadAllSerie 读取所有专题
|
||||
LoadAllSerie(ctx context.Context) (model.SortedSeries, error)
|
||||
|
||||
// InsertArticle 创建文章
|
||||
InsertArticle(ctx context.Context, article *model.Article, startID int) error
|
||||
// RemoveArticle 硬删除文章
|
||||
RemoveArticle(ctx context.Context, id int) error
|
||||
// CleanArticles 清理回收站文章
|
||||
CleanArticles(ctx context.Context, exp time.Time) error
|
||||
// UpdateArticle 更新文章
|
||||
UpdateArticle(ctx context.Context, id int, fields map[string]interface{}) error
|
||||
// LoadArticle 查找文章
|
||||
LoadArticle(ctx context.Context, id int) (*model.Article, error)
|
||||
// LoadArticleList 查找文章列表
|
||||
LoadArticleList(ctx context.Context, search SearchArticles) (model.SortedArticles, int, error)
|
||||
|
||||
// 危险操作
|
||||
DropDatabase(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Driver 存储驱动
|
||||
type Driver interface {
|
||||
// Init 数据库初始化, 建表, 加索引操作等
|
||||
Init(name, 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(name, source)
|
||||
}
|
||||
@@ -4,172 +4,51 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
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
|
||||
// RunMode 列表
|
||||
const (
|
||||
RunModeDev RunMode = "dev" // 开发环境
|
||||
RunModeProd RunMode = "pro" // 生产环境
|
||||
)
|
||||
|
||||
// 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"`
|
||||
// RunMode 运行模式
|
||||
type RunMode string
|
||||
|
||||
// IsReleaseMode 是否
|
||||
func (mode RunMode) IsReleaseMode() bool {
|
||||
return mode == RunModeProd
|
||||
}
|
||||
|
||||
// Database sql database
|
||||
type Database struct {
|
||||
Driver string `yaml:"driver"`
|
||||
Source string `yaml:"source"`
|
||||
// IsDevMode 是否时开发模式
|
||||
func (mode RunMode) IsDevMode() bool {
|
||||
return mode == RunModeDev
|
||||
}
|
||||
|
||||
// General common
|
||||
type General struct {
|
||||
PageNum int `yaml:"pagenum"` // 前台每页文章数量
|
||||
PageSize int `yaml:"pagesize"` // 后台每页文章数量
|
||||
StartID int `yaml:"startid"` // 文章启始ID
|
||||
DescPrefix string `yaml:"descprefix"` // 文章描述前缀
|
||||
Identifier string `yaml:"identifier"` // 文章截取标识
|
||||
Length int `yaml:"length"` // 文章预览长度
|
||||
Timezone string `yaml:"timezone"` // 时区
|
||||
// IsRunMode 是否是runmode
|
||||
func (mode RunMode) IsRunMode() bool {
|
||||
return mode == RunModeDev || mode == RunModeProd
|
||||
}
|
||||
|
||||
// Disqus comments
|
||||
type Disqus struct {
|
||||
ShortName string `yaml:"shortname"`
|
||||
PublicKey string `yaml:"publickey"`
|
||||
AccessToken string `yaml:"accesstoken"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
AdSense string `yaml:"adsense"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// EiBlogApp config
|
||||
type EiBlogApp 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"`
|
||||
}
|
||||
|
||||
// BackupApp config
|
||||
type BackupApp struct {
|
||||
Mode
|
||||
|
||||
BackupTo string `yaml:"backupto"`
|
||||
Interval string `yaml:"interval"` // circle backup, default: 7d
|
||||
Validity int `yaml:"validity"` // storage days, default: 60
|
||||
Qiniu Qiniu `yaml:"qiniu"` // qiniu config
|
||||
}
|
||||
|
||||
// Config app config
|
||||
type Config struct {
|
||||
RunMode string `yaml:"runmode"`
|
||||
AppName string `yaml:"appname"`
|
||||
Database Database `yaml:"database"`
|
||||
ESHost string `yaml:"eshost"`
|
||||
EiBlogApp EiBlogApp `yaml:"eiblogapp"`
|
||||
BackupApp BackupApp `yaml:"backupapp"`
|
||||
}
|
||||
|
||||
// load config file
|
||||
func init() {
|
||||
// compatibility linux and windows
|
||||
var err error
|
||||
WorkDir = workDir()
|
||||
path := filepath.Join(WorkDir, "conf", "app.yml")
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
// WalkWorkDir walk work dir
|
||||
func WalkWorkDir() (string, error) {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return "", err
|
||||
}
|
||||
err = yaml.Unmarshal(data, &Conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// read run mode from env
|
||||
Conf.RunMode = ModeDev
|
||||
if runmode := os.Getenv("RUN_MODE"); runmode == ModeProd {
|
||||
Conf.RunMode = runmode
|
||||
}
|
||||
// read env
|
||||
readDBEnv()
|
||||
}
|
||||
// find work dir, try 3 times
|
||||
for gopath != workDir && workDir != "/" {
|
||||
dir := filepath.Join(workDir, "etc")
|
||||
|
||||
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
|
||||
_, err := os.Stat(dir)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
workDir = filepath.Dir(workDir)
|
||||
}
|
||||
return workDir, nil
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// +build !prod
|
||||
|
||||
// Package config provides ...
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// workDir recognize workspace dir
|
||||
var workDir = func() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for wd != "" {
|
||||
name := filepath.Join(wd, "conf")
|
||||
_, err := os.Stat(name)
|
||||
if err != nil {
|
||||
dir, _ := path.Split(wd)
|
||||
wd = path.Clean(dir)
|
||||
continue
|
||||
}
|
||||
return wd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
124
pkg/config/enums.go
Normal file
124
pkg/config/enums.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package config
|
||||
|
||||
// APIMode 应用配置
|
||||
type APIMode struct {
|
||||
// 运行模式,可根据不同模式做相关判断,如日志打印等
|
||||
RunMode RunMode
|
||||
|
||||
// 服务名称,如 eiblog, backup
|
||||
Name string
|
||||
// 监听地址,如 0.0.0.0:9000
|
||||
Listen string
|
||||
// 所属域名,如 deepzz.com
|
||||
Host string
|
||||
// 一般的应用都会有个密钥,可以用这个字段
|
||||
Secret string
|
||||
}
|
||||
|
||||
// Database 数据库配置
|
||||
type Database struct {
|
||||
// 数据库驱动,如 sqlite, mysql, postgres 等
|
||||
Driver string
|
||||
// 数据库连接字符串,如
|
||||
// sqlite:./db.sqlite
|
||||
// mysql:root:123456@tcp(127.0.0.1:3306)/eiblog?charset=utf8mb4&parseTime=True&loc=Local
|
||||
Source string
|
||||
}
|
||||
|
||||
// Disqus 评论配置
|
||||
type Disqus struct {
|
||||
// 短名称,如 deepzz
|
||||
ShortName string
|
||||
// 公共密钥 wdSgxRm9rdGAlLKFcFdToBe3GT4SibmV7Y8EjJQ0r4GWXeKtxpopMAeIeoI2dTEg
|
||||
PublicKey string
|
||||
// 访问令牌, 需自行创建
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
// Twitter 社交配置
|
||||
type Twitter struct {
|
||||
// 卡片, 可选 summary, summary_large_image, player
|
||||
Card string
|
||||
// id号, 如 deepzz02
|
||||
Site string
|
||||
// 图片, 如 st.deepzz.cn/static/img/avatar.jpg
|
||||
Image string
|
||||
// 地址, 如 twitter.com/deepzz02
|
||||
Address string
|
||||
}
|
||||
|
||||
// Google 分析配置
|
||||
type Google struct {
|
||||
// url, 如 https://www.google-analytics.com/g/collect
|
||||
URL string
|
||||
// tid, 如 G-S085VRC5PF
|
||||
Tid string
|
||||
// v, 如 "2"
|
||||
V string
|
||||
// 如果开启广发, 配置 <script async src="xxx" crossorigin="anonymous"></script>
|
||||
AdSense string
|
||||
}
|
||||
|
||||
// Qiniu 对象存储配置
|
||||
type Qiniu struct {
|
||||
// bucket, 如 eiblog
|
||||
Bucket string
|
||||
// domain, 如 st.deepzz.cn
|
||||
Domain string
|
||||
// accesskey, 如 1234567890
|
||||
AccessKey string
|
||||
// secretkey, 如 1234567890
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// FeedRPC 订阅配置
|
||||
type FeedRPC struct {
|
||||
// feedrurl, 如 https://deepzz.superfeedr.com/
|
||||
FeedrURL string
|
||||
// pingrpc, 如 http://ping.baidu.com/ping/RPC2, http://rpc.pingomatic.com/
|
||||
PingRPC []string
|
||||
}
|
||||
|
||||
// General 博客通用配置
|
||||
type General struct {
|
||||
// 前台每页文章数量, 一般配置为 10
|
||||
PageNum int
|
||||
// 后台每页文章数量, 一般配置为 20
|
||||
PageSize int
|
||||
// 文章描述前缀, 一般配置为 Desc:
|
||||
DescPrefix string
|
||||
// 文章截取标识, 一般配置为 <!--more-->
|
||||
Identifier string
|
||||
// 文章预览长度, 一般配置为 400
|
||||
Length int
|
||||
// 时区, 一般配置为 Asia/Shanghai
|
||||
Timezone string
|
||||
}
|
||||
|
||||
// Account 账户配置
|
||||
type Account struct {
|
||||
// *必须配置, 后台登录用户名
|
||||
Username string
|
||||
// *必须配置, 后台登录密码。登录后请后台立即修改
|
||||
Password string
|
||||
// 邮箱, 用于显示
|
||||
Email string
|
||||
// 手机号, 用于显示
|
||||
PhoneNumber string
|
||||
// 地址, 用于显示
|
||||
Address string
|
||||
}
|
||||
|
||||
// Blogger 博客配置, 无需配置,程序默认初始化,可在后台更改
|
||||
type Blogger struct {
|
||||
// 博客名称, 如 deepzz
|
||||
BlogName string
|
||||
// 格言, 如 Rome was not built in one day.
|
||||
SubTitle string
|
||||
// 备案号, 不填则不显示在网站底部, 如 蜀ICP备xxxxxxxx号-1
|
||||
BeiAn string
|
||||
// 标题, 如 deepzz's Blog
|
||||
BTitle string
|
||||
// 版权, 如 本站使用「<a href="//creativecommons.org/licenses/by/4.0/">署名 4.0 国际</a>」创作共享协议,转载请注明作者及原网址。
|
||||
Copyright string // 版权
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// +build prod
|
||||
|
||||
// Package config provides ...
|
||||
package config
|
||||
|
||||
// workDir production use current dir
|
||||
var workDir = func() string { return "" }
|
||||
46
pkg/connector/db/db.go
Normal file
46
pkg/connector/db/db.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
// NewMDB new mongodb
|
||||
// https://docs.mongodb.com/manual/reference/connection-string/
|
||||
// mongodb://db0.example.com,db1.example.com,db2.example.com/dbname?replicaSet=myRepl&w=majority&wtimeoutMS=5000
|
||||
func NewMDB(opts config.Database) (*mongo.Database, error) {
|
||||
if opts.Driver != "mongodb" {
|
||||
return nil, errors.New("db: driver must be mongodb, but " + opts.Driver)
|
||||
}
|
||||
u, err := url.Parse(opts.Source)
|
||||
if err != nil {
|
||||
return nil, errors.New("db: " + err.Error())
|
||||
}
|
||||
database := u.Path
|
||||
if database == "" {
|
||||
return nil, errors.New("db: please specify a database")
|
||||
}
|
||||
database = database[1:]
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
// remove database
|
||||
u.Path = ""
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(u.String()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = client.Ping(ctx, readpref.Primary())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.Database(database), nil
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Package backup provides ...
|
||||
package backup
|
||||
|
||||
// @title APP Demo API
|
||||
// @version 1.0
|
||||
// @description This is a sample server celler server.
|
||||
|
||||
// @BasePath /api
|
||||
@@ -1,80 +0,0 @@
|
||||
// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag
|
||||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
var doc = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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{
|
||||
Version: "1.0",
|
||||
Host: "",
|
||||
BasePath: "/api",
|
||||
Schemes: []string{},
|
||||
Title: "APP Demo API",
|
||||
Description: "This is a sample server celler server.",
|
||||
}
|
||||
|
||||
type s struct{}
|
||||
|
||||
func (s *s) ReadDoc() string {
|
||||
sInfo := SwaggerInfo
|
||||
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
|
||||
|
||||
t, err := template.New("swagger_info").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) string {
|
||||
a, _ := json.Marshal(v)
|
||||
return string(a)
|
||||
},
|
||||
"escape": func(v interface{}) string {
|
||||
// escape tabs
|
||||
str := strings.Replace(v.(string), "\t", "\\t", -1)
|
||||
// replace " with \", and if that results in \\", replace that with \\\"
|
||||
str = strings.Replace(str, "\"", "\\\"", -1)
|
||||
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
|
||||
},
|
||||
}).Parse(doc)
|
||||
if err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
var tpl bytes.Buffer
|
||||
if err := t.Execute(&tpl, sInfo); err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
return tpl.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(swag.Name, &s{})
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is a sample server celler server.",
|
||||
"title": "APP Demo API",
|
||||
"contact": {},
|
||||
"version": "1.0"
|
||||
},
|
||||
"basePath": "/api",
|
||||
"paths": {}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
basePath: /api
|
||||
info:
|
||||
contact: {}
|
||||
description: This is a sample server celler server.
|
||||
title: APP Demo API
|
||||
version: "1.0"
|
||||
paths: {}
|
||||
swagger: "2.0"
|
||||
@@ -1,18 +0,0 @@
|
||||
// Package ping provides ...
|
||||
package ping
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
func RegisterRoutes(group gin.IRoutes) {
|
||||
group.GET("/ping", handlePing)
|
||||
}
|
||||
|
||||
// handlePing ping
|
||||
func handlePing(c *gin.Context) {
|
||||
c.String(http.StatusOK, "it's ok")
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Package swag provides ...
|
||||
package swag
|
||||
|
||||
import (
|
||||
_ "github.com/eiblog/eiblog/pkg/core/backup/docs" // docs
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
func RegisterRoutes(group gin.IRoutes) {
|
||||
group.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Package qiniu provides ...
|
||||
package qiniu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache/store"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
)
|
||||
|
||||
// Storage qiniu storage
|
||||
type Storage struct{}
|
||||
|
||||
// BackupData implements timer.Storage
|
||||
func (s Storage) BackupData(now time.Time) error {
|
||||
switch config.Conf.Database.Driver {
|
||||
case "mongodb":
|
||||
return backupFromMongoDB(now)
|
||||
default:
|
||||
return errors.New("unsupported source backup to qiniu: " +
|
||||
config.Conf.Database.Driver)
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreData implements timer.Storage
|
||||
func (s Storage) RestoreData() error {
|
||||
switch config.Conf.Database.Driver {
|
||||
case "mongodb":
|
||||
return restoreToMongoDB()
|
||||
default:
|
||||
return errors.New("unsupported source restore from qiniu: " +
|
||||
config.Conf.Database.Driver)
|
||||
}
|
||||
}
|
||||
|
||||
func backupFromMongoDB(now time.Time) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*20)
|
||||
defer cancel()
|
||||
|
||||
// dump
|
||||
u, err := url.Parse(config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arg := fmt.Sprintf("mongodump -h %s -d eiblog -o /tmp", u.Host)
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// tar
|
||||
name := fmt.Sprintf("eiblog-%s.tar.gz", now.Format("2006-01-02"))
|
||||
arg = fmt.Sprintf("tar czf /tmp/%s -C /tmp eiblog", name)
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// upload file
|
||||
f, err := os.Open("/tmp/" + name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadParams := internal.UploadParams{
|
||||
Name: filepath.Join("blog", name), // blog/eiblog-xx.tar.gz
|
||||
Size: s.Size(),
|
||||
Data: f,
|
||||
NoCompletePath: true,
|
||||
|
||||
Conf: config.Conf.BackupApp.Qiniu,
|
||||
}
|
||||
_, err = internal.QiniuUpload(uploadParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// after days delete
|
||||
deleteParams := internal.DeleteParams{
|
||||
Name: filepath.Join("blog", name), // blog/eiblog-xx.tar.gz
|
||||
Days: config.Conf.BackupApp.Validity,
|
||||
NoCompletePath: true,
|
||||
|
||||
Conf: config.Conf.BackupApp.Qiniu,
|
||||
}
|
||||
return internal.QiniuDelete(deleteParams)
|
||||
}
|
||||
|
||||
func restoreToMongoDB() error {
|
||||
// backup file
|
||||
params := internal.ContentParams{
|
||||
Prefix: "blog/",
|
||||
|
||||
Conf: config.Conf.BackupApp.Qiniu,
|
||||
}
|
||||
raw, err := internal.QiniuContent(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile("/tmp/eiblog.tar.gz", os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = f.Write(raw)
|
||||
defer f.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*20)
|
||||
defer cancel()
|
||||
// drop database
|
||||
store, err := store.NewStore(config.Conf.Database.Driver,
|
||||
config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = store.DropDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// unarchive
|
||||
arg := fmt.Sprintf("tar xzf /tmp/eiblog.tar.gz -C /tmp")
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// restore
|
||||
u, err := url.Parse(config.Conf.Database.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arg = fmt.Sprintf("mongorestore -h %s -d eiblog /tmp/eiblog", u.Host)
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", arg)
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Package timer provides ...
|
||||
package timer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/backup/timer/qiniu"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Start to backup with ticker
|
||||
func Start(restore bool) (err error) {
|
||||
var storage Storage
|
||||
// backup instance
|
||||
switch config.Conf.BackupApp.BackupTo {
|
||||
case "qiniu":
|
||||
storage = qiniu.Storage{}
|
||||
|
||||
default:
|
||||
return errors.New("timer: unknown backup to driver: " +
|
||||
config.Conf.BackupApp.BackupTo)
|
||||
}
|
||||
if restore {
|
||||
err = storage.RestoreData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Info("timer: RestoreData success")
|
||||
}
|
||||
// parse duration
|
||||
interval, err := ParseDuration(config.Conf.BackupApp.Interval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t := time.NewTicker(interval)
|
||||
for now := range t.C {
|
||||
err = storage.BackupData(now)
|
||||
if err != nil {
|
||||
logrus.Error("timer: Start.BackupData: ", now.Format(time.RFC3339), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseDuration parse string to duration
|
||||
func ParseDuration(d string) (time.Duration, error) {
|
||||
if len(d) == 0 {
|
||||
return 0, errors.New("timer: incorrect duration input")
|
||||
}
|
||||
|
||||
length := len(d)
|
||||
switch d[length-1] {
|
||||
case 's', 'm', 'h':
|
||||
return time.ParseDuration(d)
|
||||
case 'd':
|
||||
di, err := strconv.Atoi(d[:length-1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Duration(di) * time.Hour * 24, nil
|
||||
}
|
||||
|
||||
return 0, errors.New("timer: unsupported duration:" + d)
|
||||
}
|
||||
|
||||
// Storage backup backend
|
||||
type Storage interface {
|
||||
BackupData(now time.Time) error
|
||||
RestoreData() error
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
// Package admin provides ...
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 通知cookie
|
||||
const (
|
||||
NoticeSuccess = "success"
|
||||
NoticeNotice = "notice"
|
||||
NoticeError = "error"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
func RegisterRoutes(e *gin.Engine) {
|
||||
e.POST("/admin/login", handleAcctLogin)
|
||||
}
|
||||
|
||||
// RegisterRoutesAuthz register routes
|
||||
func RegisterRoutesAuthz(group gin.IRoutes) {
|
||||
group.POST("/api/account", handleAPIAccount)
|
||||
group.POST("/api/blog", handleAPIBlogger)
|
||||
group.POST("/api/password", handleAPIPassword)
|
||||
group.POST("/api/post-delete", handleAPIPostDelete)
|
||||
group.POST("/api/post-add", handleAPIPostCreate)
|
||||
group.POST("/api/serie-delete", handleAPISerieDelete)
|
||||
group.POST("/api/serie-add", handleAPISerieCreate)
|
||||
group.POST("/api/serie-sort", handleAPISerieSort)
|
||||
group.POST("/api/draft-delete", handleDraftDelete)
|
||||
group.POST("/api/trash-delete", handleAPITrashDelete)
|
||||
group.POST("/api/trash-recover", handleAPITrashRecover)
|
||||
group.POST("/api/file-upload", handleAPIQiniuUpload)
|
||||
group.POST("/api/file-delete", handleAPIQiniuDelete)
|
||||
}
|
||||
|
||||
// handleAcctLogin 登录接口
|
||||
func handleAcctLogin(c *gin.Context) {
|
||||
user := c.PostForm("user")
|
||||
pwd := c.PostForm("password")
|
||||
// code := c.PostForm("code") // 二次验证
|
||||
if user == "" || pwd == "" {
|
||||
logrus.Warnf("参数错误: %s %s", user, pwd)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
if cache.Ei.Account.Username != user ||
|
||||
cache.Ei.Account.Password != tools.EncryptPasswd(user, pwd) {
|
||||
logrus.Warnf("账号或密码错误 %s, %s", user, pwd)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
// 登录成功
|
||||
eiblog.SetLogin(c, user)
|
||||
|
||||
cache.Ei.Account.LoginIP = c.ClientIP()
|
||||
cache.Ei.Account.LoginAt = time.Now()
|
||||
cache.Ei.UpdateAccount(context.Background(), user, map[string]interface{}{
|
||||
"login_ip": cache.Ei.Account.LoginIP,
|
||||
"login_at": cache.Ei.Account.LoginAt,
|
||||
})
|
||||
c.Redirect(http.StatusFound, "/admin/profile")
|
||||
}
|
||||
|
||||
// handleAPIBlogger 更新博客信息
|
||||
func handleAPIBlogger(c *gin.Context) {
|
||||
bn := c.PostForm("blogName")
|
||||
bt := c.PostForm("bTitle")
|
||||
ba := c.PostForm("beiAn")
|
||||
st := c.PostForm("subTitle")
|
||||
ss := c.PostForm("seriessay")
|
||||
as := c.PostForm("archivessay")
|
||||
if bn == "" || bt == "" {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
|
||||
err := cache.Ei.UpdateBlogger(context.Background(), map[string]interface{}{
|
||||
"blog_name": bn,
|
||||
"b_title": bt,
|
||||
"bei_an": ba,
|
||||
"sub_title": st,
|
||||
"series_say": ss,
|
||||
"archives_say": as,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIBlogger.UpdateBlogger: ", err)
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
cache.Ei.Blogger.BlogName = bn
|
||||
cache.Ei.Blogger.BTitle = bt
|
||||
cache.Ei.Blogger.BeiAn = ba
|
||||
cache.Ei.Blogger.SubTitle = st
|
||||
cache.Ei.Blogger.SeriesSay = ss
|
||||
cache.Ei.Blogger.ArchivesSay = as
|
||||
cache.PagesCh <- cache.PageSeries
|
||||
cache.PagesCh <- cache.PageArchive
|
||||
responseNotice(c, NoticeSuccess, "更新成功", "")
|
||||
}
|
||||
|
||||
// handleAPIAccount 更新账户信息
|
||||
func handleAPIAccount(c *gin.Context) {
|
||||
e := c.PostForm("email")
|
||||
pn := c.PostForm("phoneNumber")
|
||||
ad := c.PostForm("address")
|
||||
if (e != "" && !tools.ValidateEmail(e)) || (pn != "" &&
|
||||
!tools.ValidatePhoneNo(pn)) {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
|
||||
err := cache.Ei.UpdateAccount(context.Background(), cache.Ei.Account.Username,
|
||||
map[string]interface{}{
|
||||
"email": e,
|
||||
"phone_n": pn,
|
||||
"address": ad,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIAccount.UpdateAccount: ", err)
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
cache.Ei.Account.Email = e
|
||||
cache.Ei.Account.PhoneN = pn
|
||||
cache.Ei.Account.Address = ad
|
||||
responseNotice(c, NoticeSuccess, "更新成功", "")
|
||||
}
|
||||
|
||||
// handleAPIPassword 更新密码
|
||||
func handleAPIPassword(c *gin.Context) {
|
||||
od := c.PostForm("old")
|
||||
nw := c.PostForm("new")
|
||||
cf := c.PostForm("confirm")
|
||||
if nw != cf {
|
||||
responseNotice(c, NoticeNotice, "两次密码输入不一致", "")
|
||||
return
|
||||
}
|
||||
if !tools.ValidatePassword(nw) {
|
||||
responseNotice(c, NoticeNotice, "密码格式错误", "")
|
||||
return
|
||||
}
|
||||
if cache.Ei.Account.Password != tools.EncryptPasswd(cache.Ei.Account.Username, od) {
|
||||
responseNotice(c, NoticeNotice, "原始密码不正确", "")
|
||||
return
|
||||
}
|
||||
newPwd := tools.EncryptPasswd(cache.Ei.Account.Username, nw)
|
||||
|
||||
err := cache.Ei.UpdateAccount(context.Background(), cache.Ei.Account.Username,
|
||||
map[string]interface{}{
|
||||
"password": newPwd,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPassword.UpdateAccount: ", err)
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
cache.Ei.Account.Password = newPwd
|
||||
responseNotice(c, NoticeSuccess, "更新成功", "")
|
||||
}
|
||||
|
||||
// handleDraftDelete 删除草稿, 物理删除
|
||||
func handleDraftDelete(c *gin.Context) {
|
||||
for _, v := range c.PostFormArray("mid[]") {
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil || id < 1 {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.RemoveArticle(context.Background(), id)
|
||||
if err != nil {
|
||||
logrus.Error("handleDraftDelete.RemoveArticle: ", err)
|
||||
responseNotice(c, NoticeNotice, "删除失败", "")
|
||||
return
|
||||
}
|
||||
}
|
||||
responseNotice(c, NoticeSuccess, "删除成功", "")
|
||||
}
|
||||
|
||||
// handleAPIPostDelete 删除文章,移入回收箱
|
||||
func handleAPIPostDelete(c *gin.Context) {
|
||||
var ids []int
|
||||
for _, v := range c.PostFormArray("cid[]") {
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil || id < config.Conf.EiBlogApp.General.StartID {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.DelArticle(id)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostDelete.DelArticle: ", err)
|
||||
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
// elasticsearch
|
||||
err := internal.ElasticDelIndex(ids)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostDelete.ElasticDelIndex: ", err)
|
||||
}
|
||||
// TODO disqus delete
|
||||
responseNotice(c, NoticeSuccess, "删除成功", "")
|
||||
}
|
||||
|
||||
// handleAPIPostCreate 创建文章
|
||||
func handleAPIPostCreate(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
do string
|
||||
cid int
|
||||
)
|
||||
defer func() {
|
||||
now := time.Now().In(tools.TimeLocation)
|
||||
switch do {
|
||||
case "auto": // 自动保存
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"fail": 1, "time": now.Format("15:04:05 PM"), "cid": cid})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": 0, "time": now.Format("15:04:05 PM"), "cid": cid})
|
||||
case "save", "publish": // 草稿,发布
|
||||
if err != nil {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
uri := "/admin/manage-draft"
|
||||
if do == "publish" {
|
||||
uri = "/admin/manage-posts"
|
||||
}
|
||||
c.Redirect(http.StatusFound, uri)
|
||||
}
|
||||
}()
|
||||
|
||||
do = c.PostForm("do") // auto or save or publish
|
||||
slug := c.PostForm("slug")
|
||||
title := c.PostForm("title")
|
||||
text := c.PostForm("text")
|
||||
date := parseLocationDate(c.PostForm("date"))
|
||||
serie := c.PostForm("serie")
|
||||
tag := c.PostForm("tags")
|
||||
update := c.PostForm("update")
|
||||
if slug == "" || title == "" || text == "" {
|
||||
err = errors.New("参数错误")
|
||||
return
|
||||
}
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = strings.Split(tag, ",")
|
||||
}
|
||||
serieid, _ := strconv.Atoi(serie)
|
||||
article := &model.Article{
|
||||
Title: title,
|
||||
Content: text,
|
||||
Slug: slug,
|
||||
IsDraft: do != "publish",
|
||||
Author: cache.Ei.Account.Username,
|
||||
SerieID: serieid,
|
||||
Tags: tags,
|
||||
CreatedAt: date,
|
||||
}
|
||||
cid, err = strconv.Atoi(c.PostForm("cid"))
|
||||
// 新文章
|
||||
if err != nil || cid < 1 {
|
||||
err = cache.Ei.AddArticle(article)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostCreate.AddArticle: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
cid = article.ID
|
||||
|
||||
if !article.IsDraft {
|
||||
// disqus
|
||||
internal.ThreadCreate(article, cache.Ei.Blogger.BTitle)
|
||||
// 异步执行,快
|
||||
go func() {
|
||||
// elastic
|
||||
internal.ElasticAddIndex(article)
|
||||
// rss
|
||||
internal.PingFunc(cache.Ei.Blogger.BTitle, slug)
|
||||
}()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 旧文章
|
||||
article.ID = cid
|
||||
artc, _ := cache.Ei.FindArticleByID(article.ID) // cache
|
||||
if artc != nil {
|
||||
article.IsDraft = false
|
||||
article.Count = artc.Count
|
||||
article.UpdatedAt = artc.UpdatedAt
|
||||
}
|
||||
if update == "true" || update == "1" {
|
||||
article.UpdatedAt = time.Now()
|
||||
}
|
||||
// 数据库更新
|
||||
err = cache.Ei.UpdateArticle(context.Background(), article.ID, map[string]interface{}{
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"serie_id": article.SerieID,
|
||||
"is_draft": article.IsDraft,
|
||||
"tags": article.Tags,
|
||||
"updated_at": article.UpdatedAt,
|
||||
"created_at": article.CreatedAt,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIPostCreate.UpdateArticle: ", err)
|
||||
return
|
||||
}
|
||||
if !article.IsDraft {
|
||||
cache.Ei.RepArticle(artc, article)
|
||||
// disqus
|
||||
if artc == nil {
|
||||
internal.ThreadCreate(article, cache.Ei.Blogger.BTitle)
|
||||
}
|
||||
// 异步执行,快
|
||||
go func() {
|
||||
// elastic
|
||||
internal.ElasticAddIndex(article)
|
||||
// rss
|
||||
internal.PingFunc(cache.Ei.Blogger.BTitle, slug)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// handleAPISerieDelete 只能逐一删除,专题下不能有文章
|
||||
func handleAPISerieDelete(c *gin.Context) {
|
||||
for _, v := range c.PostFormArray("mid[]") {
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil || id < 1 {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.DelSerie(id)
|
||||
if err != nil {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
responseNotice(c, NoticeSuccess, "删除成功", "")
|
||||
}
|
||||
|
||||
// handleAPISerieSort 专题排序
|
||||
func handleAPISerieSort(c *gin.Context) {
|
||||
v := c.PostFormArray("mid[]")
|
||||
logrus.Debug(v)
|
||||
}
|
||||
|
||||
// handleAPISerieCreate 添加专题,如果专题有提交 mid 即更新专题
|
||||
func handleAPISerieCreate(c *gin.Context) {
|
||||
name := c.PostForm("name")
|
||||
slug := c.PostForm("slug")
|
||||
desc := c.PostForm("description")
|
||||
if name == "" || slug == "" || desc == "" {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
mid, err := strconv.Atoi(c.PostForm("mid"))
|
||||
if err == nil && mid > 0 {
|
||||
var serie *model.Serie
|
||||
for _, v := range cache.Ei.Series {
|
||||
if v.ID == mid {
|
||||
serie = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if serie == nil {
|
||||
responseNotice(c, NoticeNotice, "专题不存在", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.UpdateSerie(context.Background(), mid, map[string]interface{}{
|
||||
"slug": slug,
|
||||
"name": name,
|
||||
"desc": desc,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPISerieCreate.UpdateSerie: ", err)
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
serie.Slug = slug
|
||||
serie.Name = name
|
||||
serie.Desc = desc
|
||||
cache.PagesCh <- cache.PageSeries
|
||||
} else {
|
||||
err = cache.Ei.AddSerie(&model.Serie{
|
||||
Slug: slug,
|
||||
Name: name,
|
||||
Desc: desc,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Error("handleAPISerieCreate.InsertSerie: ", err)
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
responseNotice(c, NoticeSuccess, "操作成功", "")
|
||||
}
|
||||
|
||||
// handleAPITrashDelete 删除回收箱, 物理删除
|
||||
func handleAPITrashDelete(c *gin.Context) {
|
||||
for _, v := range c.PostFormArray("mid[]") {
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil || id < 1 {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
}
|
||||
err = cache.Ei.RemoveArticle(context.Background(), id)
|
||||
if err != nil {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
responseNotice(c, NoticeSuccess, "删除成功", "")
|
||||
}
|
||||
|
||||
// handleAPITrashRecover 恢复到草稿
|
||||
func handleAPITrashRecover(c *gin.Context) {
|
||||
for _, v := range c.PostFormArray("mid[]") {
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil || id < 1 {
|
||||
responseNotice(c, NoticeNotice, "参数错误", "")
|
||||
return
|
||||
|
||||
}
|
||||
err = cache.Ei.UpdateArticle(context.Background(), id, map[string]interface{}{
|
||||
"deleted_at": time.Time{},
|
||||
"is_draft": true,
|
||||
})
|
||||
if err != nil {
|
||||
responseNotice(c, NoticeNotice, err.Error(), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
responseNotice(c, NoticeSuccess, "恢复成功", "")
|
||||
}
|
||||
|
||||
// handleAPIQiniuUpload 上传文件
|
||||
func handleAPIQiniuUpload(c *gin.Context) {
|
||||
type Size interface {
|
||||
Size() int64
|
||||
}
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIQiniuUpload.FormFile: ", err)
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s, ok := file.(Size)
|
||||
if !ok {
|
||||
logrus.Error("assert failed")
|
||||
c.String(http.StatusBadRequest, "false")
|
||||
return
|
||||
}
|
||||
filename := strings.ToLower(header.Filename)
|
||||
|
||||
params := internal.UploadParams{
|
||||
Name: filename,
|
||||
Size: s.Size(),
|
||||
Data: file,
|
||||
|
||||
Conf: config.Conf.EiBlogApp.Qiniu,
|
||||
}
|
||||
url, err := internal.QiniuUpload(params)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIQiniuUpload.QiniuUpload: ", err)
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
typ := header.Header.Get("Content-Type")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"title": filename,
|
||||
"isImage": typ[:5] == "image",
|
||||
"url": url,
|
||||
"bytes": fmt.Sprintf("%dkb", s.Size()/1000),
|
||||
})
|
||||
}
|
||||
|
||||
// handleAPIQiniuDelete 删除文件
|
||||
func handleAPIQiniuDelete(c *gin.Context) {
|
||||
defer c.String(http.StatusOK, "删掉了吗?鬼知道。。。")
|
||||
|
||||
name := c.PostForm("title")
|
||||
if name == "" {
|
||||
logrus.Error("handleAPIQiniuDelete.PostForm: 参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
params := internal.DeleteParams{
|
||||
Name: name,
|
||||
|
||||
Conf: config.Conf.EiBlogApp.Qiniu,
|
||||
}
|
||||
err := internal.QiniuDelete(params)
|
||||
if err != nil {
|
||||
logrus.Error("handleAPIQiniuDelete.QiniuDelete: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseLocationDate 解析日期
|
||||
func parseLocationDate(date string) time.Time {
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04", date, tools.TimeLocation)
|
||||
if err == nil {
|
||||
return t.UTC()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func responseNotice(c *gin.Context, typ, content, hl string) {
|
||||
if hl != "" {
|
||||
c.SetCookie("notice_highlight", hl, 86400, "/", "", true, false)
|
||||
}
|
||||
c.SetCookie("notice_type", typ, 86400, "/", "", true, false)
|
||||
c.SetCookie("notice", fmt.Sprintf("[\"%s\"]", content), 86400, "/", "", true, false)
|
||||
c.Redirect(http.StatusFound, c.Request.Referer())
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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
|
||||
|
||||
// AuthFilter auth filter
|
||||
func AuthFilter(c *gin.Context) {
|
||||
if !IsLogined(c) {
|
||||
c.Abort()
|
||||
c.Status(http.StatusUnauthorized)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// SetLogin login user
|
||||
func SetLogin(c *gin.Context, username string) {
|
||||
session := sessions.Default(c)
|
||||
session.Set("username", username)
|
||||
session.Save()
|
||||
}
|
||||
|
||||
// SetLogout logout user
|
||||
func SetLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete("username")
|
||||
session.Save()
|
||||
}
|
||||
|
||||
// IsLogined account logined
|
||||
func IsLogined(c *gin.Context) bool {
|
||||
return GetUsername(c) != ""
|
||||
}
|
||||
|
||||
// GetUsername get logined account
|
||||
func GetUsername(c *gin.Context) string {
|
||||
session := sessions.Default(c)
|
||||
username := session.Get("username")
|
||||
if username == nil {
|
||||
return ""
|
||||
}
|
||||
return username.(string)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag
|
||||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
var doc = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"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{
|
||||
Version: "1.0",
|
||||
Host: "",
|
||||
BasePath: "/api",
|
||||
Schemes: []string{},
|
||||
Title: "APP Demo API",
|
||||
Description: "This is a sample server celler server.",
|
||||
}
|
||||
|
||||
type s struct{}
|
||||
|
||||
func (s *s) ReadDoc() string {
|
||||
sInfo := SwaggerInfo
|
||||
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
|
||||
|
||||
t, err := template.New("swagger_info").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) string {
|
||||
a, _ := json.Marshal(v)
|
||||
return string(a)
|
||||
},
|
||||
"escape": func(v interface{}) string {
|
||||
// escape tabs
|
||||
str := strings.Replace(v.(string), "\t", "\\t", -1)
|
||||
// replace " with \", and if that results in \\", replace that with \\\"
|
||||
str = strings.Replace(str, "\"", "\\\"", -1)
|
||||
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
|
||||
},
|
||||
}).Parse(doc)
|
||||
if err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
var tpl bytes.Buffer
|
||||
if err := t.Execute(&tpl, sInfo); err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
return tpl.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(swag.Name, &s{})
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is a sample server celler server.",
|
||||
"title": "APP Demo API",
|
||||
"contact": {},
|
||||
"version": "1.0"
|
||||
},
|
||||
"basePath": "/api",
|
||||
"paths": {}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
basePath: /api
|
||||
info:
|
||||
contact: {}
|
||||
description: This is a sample server celler server.
|
||||
title: APP Demo API
|
||||
version: "1.0"
|
||||
paths: {}
|
||||
swagger: "2.0"
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
// Package file provides ...
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var xmlTmpl *template.Template
|
||||
|
||||
func init() {
|
||||
root := filepath.Join(config.WorkDir, "website", "template", "*.xml")
|
||||
|
||||
var err error
|
||||
xmlTmpl, err = template.New("").Funcs(template.FuncMap{
|
||||
"dateformat": tools.DateFormat,
|
||||
"imgtonormal": tools.ImgToNormal,
|
||||
}).ParseGlob(root)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
generateOpensearch()
|
||||
generateRobots()
|
||||
generateCrossdomain()
|
||||
go timerFeed()
|
||||
go timerSitemap()
|
||||
}
|
||||
|
||||
// timerFeed 定时刷新feed
|
||||
func timerFeed() {
|
||||
tpl := xmlTmpl.Lookup("feedTpl.xml")
|
||||
if tpl == nil {
|
||||
logrus.Info("file: not found: feedTpl.xml")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_, _, articles := cache.Ei.PageArticleFE(1, 20)
|
||||
params := map[string]interface{}{
|
||||
"Title": cache.Ei.Blogger.BTitle,
|
||||
"SubTitle": cache.Ei.Blogger.SubTitle,
|
||||
"Host": config.Conf.EiBlogApp.Host,
|
||||
"FeedrURL": config.Conf.EiBlogApp.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)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
err = tpl.Execute(f, params)
|
||||
if err != nil {
|
||||
logrus.Error("file: timerFeed.Execute: ", err)
|
||||
return
|
||||
}
|
||||
time.AfterFunc(time.Hour*4, timerFeed)
|
||||
}
|
||||
|
||||
// timerSitemap 定时刷新sitemap
|
||||
func timerSitemap() {
|
||||
tpl := xmlTmpl.Lookup("sitemapTpl.xml")
|
||||
if tpl == nil {
|
||||
logrus.Info("file: not found: sitemapTpl.xml")
|
||||
return
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"Articles": cache.Ei.Articles,
|
||||
"Host": config.Conf.EiBlogApp.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)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
err = tpl.Execute(f, params)
|
||||
if err != nil {
|
||||
logrus.Error("file: timerSitemap.Execute: ", err)
|
||||
return
|
||||
}
|
||||
time.AfterFunc(time.Hour*24, timerSitemap)
|
||||
}
|
||||
|
||||
// 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.EiBlogApp.Host,
|
||||
}
|
||||
f, err := os.OpenFile("assets/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.EiBlogApp.Host,
|
||||
}
|
||||
f, err := os.OpenFile("assets/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.EiBlogApp.Host,
|
||||
}
|
||||
f, err := os.OpenFile("assets/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
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// Package page provides ...
|
||||
package page
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
htemplate "html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/cache/store"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/core/eiblog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// baseBEParams 基础参数
|
||||
func baseBEParams(c *gin.Context) gin.H {
|
||||
return gin.H{
|
||||
"Author": cache.Ei.Account.Username,
|
||||
"Qiniu": config.Conf.EiBlogApp.Qiniu,
|
||||
}
|
||||
}
|
||||
|
||||
// handleLoginPage 登录页面
|
||||
func handleLoginPage(c *gin.Context) {
|
||||
logout := c.Query("logout")
|
||||
if logout == "true" {
|
||||
eiblog.SetLogout(c)
|
||||
} else if eiblog.IsLogined(c) {
|
||||
c.Redirect(http.StatusFound, "/admin/profile")
|
||||
return
|
||||
}
|
||||
params := gin.H{"BTitle": cache.Ei.Blogger.BTitle}
|
||||
renderHTMLAdminLayout(c, "login.html", params)
|
||||
}
|
||||
|
||||
// handleAdminProfile 个人配置
|
||||
func handleAdminProfile(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "个人配置 | " + cache.Ei.Blogger.BTitle
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Console"] = true
|
||||
params["Ei"] = cache.Ei
|
||||
renderHTMLAdminLayout(c, "admin-profile", params)
|
||||
}
|
||||
|
||||
// T tag struct
|
||||
type T struct {
|
||||
ID string `json:"id"`
|
||||
Tags string `json:"tags"`
|
||||
}
|
||||
|
||||
// handleAdminPost 写文章页
|
||||
func handleAdminPost(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
id, err := strconv.Atoi(c.Query("cid"))
|
||||
if err == nil && id > 0 {
|
||||
article, _ := cache.Ei.LoadArticle(context.Background(), id)
|
||||
if article != nil {
|
||||
params["Title"] = "编辑文章 | " + cache.Ei.Blogger.BTitle
|
||||
params["Edit"] = article
|
||||
}
|
||||
}
|
||||
if params["Title"] == nil {
|
||||
params["Title"] = "撰写文章 | " + cache.Ei.Blogger.BTitle
|
||||
}
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Domain"] = config.Conf.EiBlogApp.Host
|
||||
params["Series"] = cache.Ei.Series
|
||||
var tags []T
|
||||
for tag := range cache.Ei.TagArticles {
|
||||
tags = append(tags, T{tag, tag})
|
||||
}
|
||||
str, _ := json.Marshal(tags)
|
||||
params["Tags"] = string(str)
|
||||
renderHTMLAdminLayout(c, "admin-post", params)
|
||||
}
|
||||
|
||||
// handleAdminPosts 文章管理页
|
||||
func handleAdminPosts(c *gin.Context) {
|
||||
kw := c.Query("keywords")
|
||||
tmp := c.Query("serie")
|
||||
se, err := strconv.Atoi(tmp)
|
||||
if err != nil || se < 1 {
|
||||
se = 0
|
||||
}
|
||||
pg, err := strconv.Atoi(c.Query("page"))
|
||||
if err != nil || pg < 1 {
|
||||
pg = 1
|
||||
}
|
||||
vals := c.Request.URL.Query()
|
||||
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "文章管理 | " + cache.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["Series"] = cache.Ei.Series
|
||||
params["Serie"] = se
|
||||
params["KW"] = kw
|
||||
var max int
|
||||
params["List"], max = cache.Ei.PageArticleBE(se, kw, false, false,
|
||||
pg, config.Conf.EiBlogApp.General.PageSize)
|
||||
if pg < max {
|
||||
vals.Set("page", fmt.Sprint(pg+1))
|
||||
params["Next"] = vals.Encode()
|
||||
}
|
||||
if pg > 1 {
|
||||
vals.Set("page", fmt.Sprint(pg-1))
|
||||
params["Prev"] = vals.Encode()
|
||||
}
|
||||
params["PP"] = make(map[int]string, max)
|
||||
for i := 0; i < max; i++ {
|
||||
vals.Set("page", fmt.Sprint(i+1))
|
||||
params["PP"].(map[int]string)[i+1] = vals.Encode()
|
||||
}
|
||||
params["Cur"] = pg
|
||||
renderHTMLAdminLayout(c, "admin-posts", params)
|
||||
}
|
||||
|
||||
// handleAdminSeries 专题列表
|
||||
func handleAdminSeries(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "专题管理 | " + cache.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["List"] = cache.Ei.Series
|
||||
renderHTMLAdminLayout(c, "admin-series", params)
|
||||
}
|
||||
|
||||
// handleAdminSerie 编辑专题
|
||||
func handleAdminSerie(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
|
||||
id, err := strconv.Atoi(c.Query("mid"))
|
||||
params["Title"] = "新增专题 | " + cache.Ei.Blogger.BTitle
|
||||
if err == nil && id > 0 {
|
||||
for _, v := range cache.Ei.Series {
|
||||
if v.ID == id {
|
||||
params["Title"] = "编辑专题 | " + cache.Ei.Blogger.BTitle
|
||||
params["Edit"] = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
renderHTMLAdminLayout(c, "admin-serie", params)
|
||||
}
|
||||
|
||||
// handleAdminTags 标签列表
|
||||
func handleAdminTags(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "标签管理 | " + cache.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
params["List"] = cache.Ei.TagArticles
|
||||
renderHTMLAdminLayout(c, "admin-tags", params)
|
||||
}
|
||||
|
||||
// handleDraftDelete 编辑页删除草稿
|
||||
func handleDraftDelete(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Query("cid"))
|
||||
if err != nil || id < 1 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
err = cache.Ei.RemoveArticle(context.Background(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "删除错误"})
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/admin/write-post")
|
||||
}
|
||||
|
||||
// handleAdminDraft 草稿箱页
|
||||
func handleAdminDraft(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
|
||||
params["Title"] = "草稿箱 | " + cache.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
var err error
|
||||
search := store.SearchArticles{
|
||||
Page: 1,
|
||||
Limit: 9999,
|
||||
Fields: map[string]interface{}{store.SearchArticleDraft: true},
|
||||
}
|
||||
params["List"], _, err = cache.Ei.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
logrus.Error("handleDraft.LoadDraftArticles: ", err)
|
||||
c.Status(http.StatusBadRequest)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
renderHTMLAdminLayout(c, "admin-draft", params)
|
||||
}
|
||||
|
||||
// handleAdminTrash 回收箱页
|
||||
func handleAdminTrash(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "回收箱 | " + cache.Ei.Blogger.BTitle
|
||||
params["Manage"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
var err error
|
||||
search := store.SearchArticles{
|
||||
Page: 1,
|
||||
Limit: 9999,
|
||||
Fields: map[string]interface{}{store.SearchArticleTrash: true},
|
||||
}
|
||||
params["List"], _, err = cache.Ei.LoadArticleList(context.Background(), search)
|
||||
if err != nil {
|
||||
logrus.Error("handleTrash.LoadArticleList: ", err)
|
||||
}
|
||||
renderHTMLAdminLayout(c, "admin-trash", params)
|
||||
}
|
||||
|
||||
// handleAdminGeneral 基本设置
|
||||
func handleAdminGeneral(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "基本设置 | " + cache.Ei.Blogger.BTitle
|
||||
params["Setting"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
renderHTMLAdminLayout(c, "admin-general", params)
|
||||
}
|
||||
|
||||
// handleAdminDiscussion 阅读设置
|
||||
func handleAdminDiscussion(c *gin.Context) {
|
||||
params := baseBEParams(c)
|
||||
params["Title"] = "阅读设置 | " + cache.Ei.Blogger.BTitle
|
||||
params["Setting"] = true
|
||||
params["Path"] = c.Request.URL.Path
|
||||
renderHTMLAdminLayout(c, "admin-discussion", params)
|
||||
}
|
||||
|
||||
// renderHTMLAdminLayout 渲染admin页面
|
||||
func renderHTMLAdminLayout(c *gin.Context, name string, data gin.H) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// special page
|
||||
if name == "login.html" {
|
||||
err := htmlTmpl.ExecuteTemplate(c.Writer, name, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
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, "adminLayout.html", data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if c.Writer.Status() == 0 {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
// Package page provides ...
|
||||
package page
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
htemplate "html/template"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/cache"
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/internal"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// baseFEParams 基础参数
|
||||
func baseFEParams(c *gin.Context) gin.H {
|
||||
version := 0
|
||||
|
||||
cookie, err := c.Request.Cookie("v")
|
||||
if err != nil || cookie.Value !=
|
||||
fmt.Sprint(config.Conf.EiBlogApp.StaticVersion) {
|
||||
version = config.Conf.EiBlogApp.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.EiBlogApp.Host,
|
||||
"CopyYear": time.Now().Year(),
|
||||
"Twitter": config.Conf.EiBlogApp.Twitter,
|
||||
"Qiniu": config.Conf.EiBlogApp.Qiniu,
|
||||
"Disqus": config.Conf.EiBlogApp.Disqus,
|
||||
"AdSense": config.Conf.EiBlogApp.Google.AdSense,
|
||||
"Version": version,
|
||||
}
|
||||
}
|
||||
|
||||
// handleNotFound not found page
|
||||
func handleNotFound(c *gin.Context) {
|
||||
params := baseFEParams(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 := baseFEParams(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.PageArticleFE(pn,
|
||||
config.Conf.EiBlogApp.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 := baseFEParams(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.UpdatedAt.IsZero() {
|
||||
params["Days"] = int(time.Now().Sub(article.UpdatedAt).Hours()) / 24
|
||||
} else {
|
||||
params["Days"] = int(time.Now().Sub(article.CreatedAt).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 := baseFEParams(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 := baseFEParams(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 := baseFEParams(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.EiBlogApp.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.EiBlogApp.General.PageNum; num > 0 {
|
||||
vals.Set("start", fmt.Sprint(num))
|
||||
params["Prev"] = vals.Encode()
|
||||
}
|
||||
if num := start + config.Conf.EiBlogApp.General.PageNum; result.Hits.Total >= num {
|
||||
vals.Set("start", fmt.Sprint(num))
|
||||
params["Next"] = vals.Encode()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
params["HotWords"] = config.Conf.EiBlogApp.HotWords
|
||||
}
|
||||
renderHTMLHomeLayout(c, "search", params)
|
||||
}
|
||||
|
||||
// disqusComments 服务端获取评论详细
|
||||
type disqusComments struct {
|
||||
ErrNo int `json:"errno"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
Data struct {
|
||||
Next string `json:"next"`
|
||||
Total int `json:"total"`
|
||||
Comments []commentsDetail `json:"comments"`
|
||||
Thread string `json:"thread"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// handleDisqusList 获取评论列表
|
||||
func handleDisqusList(c *gin.Context) {
|
||||
dcs := &disqusComments{}
|
||||
defer c.JSON(http.StatusOK, dcs)
|
||||
|
||||
slug := c.Param("slug")
|
||||
cursor := c.Query("cursor")
|
||||
artc := cache.Ei.ArticlesMap[slug]
|
||||
if artc != nil {
|
||||
dcs.Data.Thread = artc.Thread
|
||||
}
|
||||
postsList, err := internal.PostsList(artc, cursor)
|
||||
if err != nil {
|
||||
logrus.Error("hadnleDisqusList.PostsList: ", err)
|
||||
dcs.ErrNo = 0
|
||||
dcs.ErrMsg = "系统错误"
|
||||
return
|
||||
}
|
||||
dcs.ErrNo = postsList.Code
|
||||
if postsList.Cursor.HasNext {
|
||||
dcs.Data.Next = postsList.Cursor.Next
|
||||
}
|
||||
dcs.Data.Total = len(postsList.Response)
|
||||
dcs.Data.Comments = make([]commentsDetail, len(postsList.Response))
|
||||
for i, v := range postsList.Response {
|
||||
if dcs.Data.Thread == "" {
|
||||
dcs.Data.Thread = v.Thread
|
||||
}
|
||||
dcs.Data.Comments[i] = commentsDetail{
|
||||
ID: v.ID,
|
||||
Name: v.Author.Name,
|
||||
Parent: v.Parent,
|
||||
URL: v.Author.ProfileURL,
|
||||
Avatar: v.Author.Avatar.Cache,
|
||||
CreatedAtStr: tools.ConvertStr(v.CreatedAt),
|
||||
Message: v.Message,
|
||||
IsDeleted: v.IsDeleted,
|
||||
}
|
||||
}
|
||||
// query thread & update
|
||||
if artc != nil && artc.Thread == "" {
|
||||
if dcs.Data.Thread != "" {
|
||||
artc.Thread = dcs.Data.Thread
|
||||
} else if internal.ThreadDetails(artc) == nil {
|
||||
dcs.Data.Thread = artc.Thread
|
||||
}
|
||||
cache.Ei.UpdateArticle(context.Background(), artc.ID,
|
||||
map[string]interface{}{
|
||||
"thread": artc.Thread,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
"Title": "发表评论 | " + cache.Ei.Blogger.BTitle,
|
||||
"ATitle": article.Title,
|
||||
"Thread": array[1],
|
||||
"Slug": article.Slug,
|
||||
}
|
||||
renderHTMLHomeLayout(c, "disqus.html", params)
|
||||
}
|
||||
|
||||
// 发表评论
|
||||
// [thread:[5279901489] parent:[] identifier:[post-troubleshooting-https]
|
||||
// next:[] author_name:[你好] author_email:[chenqijing2@163.com] message:[fdsfdsf]]
|
||||
type disqusCreate struct {
|
||||
ErrNo int `json:"errno"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
Data commentsDetail `json:"data"`
|
||||
}
|
||||
|
||||
type commentsDetail struct {
|
||||
ID string `json:"id"`
|
||||
Parent int `json:"parent"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatedAtStr string `json:"createdAtStr"`
|
||||
Message string `json:"message"`
|
||||
IsDeleted bool `json:"isDeleted"`
|
||||
}
|
||||
|
||||
// handleDisqusCreate 评论文章
|
||||
func handleDisqusCreate(c *gin.Context) {
|
||||
resp := &disqusCreate{}
|
||||
defer c.JSON(http.StatusOK, resp)
|
||||
|
||||
msg := c.PostForm("message")
|
||||
email := c.PostForm("author_email")
|
||||
name := c.PostForm("author_name")
|
||||
thread := c.PostForm("thread")
|
||||
identifier := c.PostForm("identifier")
|
||||
if msg == "" || email == "" || name == "" || thread == "" || identifier == "" {
|
||||
resp.ErrNo = 1
|
||||
resp.ErrMsg = "参数错误"
|
||||
return
|
||||
}
|
||||
logrus.Infof("email: %s comments: %s", email, thread)
|
||||
|
||||
comment := internal.PostComment{
|
||||
Message: msg,
|
||||
Parent: c.PostForm("parent"),
|
||||
Thread: thread,
|
||||
AuthorEmail: email,
|
||||
AuthorName: name,
|
||||
Identifier: identifier,
|
||||
IPAddress: c.ClientIP(),
|
||||
}
|
||||
postDetail, err := internal.PostCreate(&comment)
|
||||
if err != nil {
|
||||
logrus.Error("handleDisqusCreate.PostCreate: ", err)
|
||||
resp.ErrNo = 1
|
||||
resp.ErrMsg = "提交评论失败,请重试"
|
||||
return
|
||||
}
|
||||
err = internal.PostApprove(postDetail.Response.ID)
|
||||
if err != nil {
|
||||
logrus.Error("handleDisqusCreate.PostApprove: ", err)
|
||||
resp.ErrNo = 1
|
||||
resp.ErrMsg = "提交评论失败,请重试"
|
||||
}
|
||||
resp.ErrNo = 0
|
||||
resp.Data = commentsDetail{
|
||||
ID: postDetail.Response.ID,
|
||||
Name: name,
|
||||
Parent: postDetail.Response.Parent,
|
||||
URL: postDetail.Response.Author.ProfileURL,
|
||||
Avatar: postDetail.Response.Author.Avatar.Cache,
|
||||
CreatedAtStr: tools.ConvertStr(postDetail.Response.CreatedAt),
|
||||
Message: postDetail.Response.Message,
|
||||
IsDeleted: postDetail.Response.IsDeleted,
|
||||
}
|
||||
}
|
||||
|
||||
// handleBeaconPage 服务端推送谷歌统计
|
||||
// https://www.thyngster.com/ga4-measurement-protocol-cheatsheet/
|
||||
func handleBeaconPage(c *gin.Context) {
|
||||
ua := c.Request.UserAgent()
|
||||
|
||||
vals := c.Request.URL.Query()
|
||||
vals.Set("v", config.Conf.EiBlogApp.Google.V)
|
||||
vals.Set("tid", config.Conf.EiBlogApp.Google.Tid)
|
||||
cookie, _ := c.Cookie("u")
|
||||
vals.Set("cid", cookie)
|
||||
|
||||
vals.Set("dl", c.Request.Referer()) // document location
|
||||
vals.Set("en", "page_view") // event name
|
||||
vals.Set("sct", "1") // Session Count
|
||||
vals.Set("seg", "1") // Session Engagment
|
||||
vals.Set("_uip", c.ClientIP()) // user ip
|
||||
vals.Set("_p", fmt.Sprint(201226219+rand.Intn(499999999))) // random page load hash
|
||||
vals.Set("_ee", "1") // external event
|
||||
go func() {
|
||||
url := config.Conf.EiBlogApp.Google.URL + "?" + vals.Encode()
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
logrus.Error("HandleBeaconPage.NewRequest: ", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("User-Agent", ua)
|
||||
req.Header.Set("Sec-Ch-Ua", c.GetHeader("Sec-Ch-Ua"))
|
||||
req.Header.Set("Sec-Ch-Ua-Platform", c.GetHeader("Sec-Ch-Ua-Platform"))
|
||||
req.Header.Set("Sec-Ch-Ua-Mobile", c.GetHeader("Sec-Ch-Ua-Mobile"))
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
logrus.Error("HandleBeaconPage.Do: ", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := io.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) {
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// special page
|
||||
if name == "disqus.html" {
|
||||
err := htmlTmpl.ExecuteTemplate(c.Writer, name, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Package page provides ...
|
||||
package page
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// htmlTmpl html template cache
|
||||
var htmlTmpl *template.Template
|
||||
|
||||
func init() {
|
||||
htmlTmpl = template.New("eiblog").Funcs(tools.TplFuncMap)
|
||||
root := filepath.Join(config.WorkDir, "website")
|
||||
files := tools.ReadDirFiles(root, func(fi fs.DirEntry) bool {
|
||||
name := fi.Name()
|
||||
if name == ".DS_Store" {
|
||||
return true
|
||||
}
|
||||
// should not read template dir
|
||||
if fi.IsDir() && name == "template" {
|
||||
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/post-:slug", handleDisqusList)
|
||||
e.GET("/disqus/form/post-:slug", handleDisqusPage)
|
||||
e.POST("/disqus/create", handleDisqusCreate)
|
||||
e.GET("/beacon.html", handleBeaconPage)
|
||||
|
||||
// login page
|
||||
e.GET("/admin/login", handleLoginPage)
|
||||
}
|
||||
|
||||
// RegisterRoutesAuthz register admin
|
||||
func RegisterRoutesAuthz(group gin.IRoutes) {
|
||||
// console
|
||||
group.GET("/profile", handleAdminProfile)
|
||||
// write
|
||||
group.GET("/write-post", handleAdminPost)
|
||||
group.GET("/draft-delete", handleDraftDelete)
|
||||
// manage
|
||||
group.GET("/manage-posts", handleAdminPosts)
|
||||
group.GET("/manage-series", handleAdminSeries)
|
||||
group.GET("/add-serie", handleAdminSerie)
|
||||
group.GET("/manage-tags", handleAdminTags)
|
||||
group.GET("/manage-draft", handleAdminDraft)
|
||||
group.GET("/manage-trash", handleAdminTrash)
|
||||
group.GET("/options-general", handleAdminGeneral)
|
||||
group.GET("/options-discussion", handleAdminDiscussion)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Package swag provides ...
|
||||
package swag
|
||||
|
||||
import (
|
||||
_ "github.com/eiblog/eiblog/pkg/core/eiblog/docs" // docs
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// RegisterRoutes register routes
|
||||
func RegisterRoutes(group gin.IRoutes) {
|
||||
group.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [2.2.17](https://github.com/eiblog/eiblog/compare/v2.2.16...v2.2.17) (2025-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* backup file auto delete ([0fe849a](https://github.com/eiblog/eiblog/commit/0fe849ae67de36f2d249e3306ac7d098bc057070))
|
||||
@@ -1,139 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
originHost := u.Host
|
||||
// 获取主机IP
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
addrErr := err.(*net.AddrError)
|
||||
if addrErr.Err != "missing port in address" {
|
||||
return nil, err
|
||||
}
|
||||
// set default value
|
||||
host = originHost
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
port = "80"
|
||||
case "https":
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
ips, err := net.LookupHost(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("http: not found ip(%s)", u.Host)
|
||||
}
|
||||
host = net.JoinHostPort(ips[0], port)
|
||||
u.Host = host
|
||||
// 创建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 = originHost
|
||||
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)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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
18
pkg/mid/u.go
@@ -1,18 +0,0 @@
|
||||
// Package mid provides ...
|
||||
package mid
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// UserMiddleware 用户cookie标记
|
||||
func UserMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cookie, err := c.Cookie("u")
|
||||
if err != nil || cookie == "" {
|
||||
u1 := uuid.NewV4().String()
|
||||
c.SetCookie("u", u1, 86400*730, "/", "", true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
88
pkg/middleware/session.go
Normal file
88
pkg/middleware/session.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// UserMiddleware 用户cookie标记
|
||||
func UserMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cookie, err := c.Cookie("u")
|
||||
if err != nil || cookie == "" {
|
||||
u1 := uuid.NewV4().String()
|
||||
c.SetCookie("u", u1, 86400*730, "/", "", true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AuthFilter auth filter
|
||||
func AuthFilter(c *gin.Context) {
|
||||
if !IsLogined(c) {
|
||||
c.Abort()
|
||||
c.Status(http.StatusUnauthorized)
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// SetLogin login user
|
||||
func SetLogin(c *gin.Context, username string) {
|
||||
session := sessions.Default(c)
|
||||
session.Set("username", username)
|
||||
session.Save()
|
||||
}
|
||||
|
||||
// SetLogout logout user
|
||||
func SetLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete("username")
|
||||
session.Save()
|
||||
}
|
||||
|
||||
// IsLogined account logined
|
||||
func IsLogined(c *gin.Context) bool {
|
||||
return GetUsername(c) != ""
|
||||
}
|
||||
|
||||
// GetUsername get logined account
|
||||
func GetUsername(c *gin.Context) string {
|
||||
session := sessions.Default(c)
|
||||
username := session.Get("username")
|
||||
if username == nil {
|
||||
return ""
|
||||
}
|
||||
return username.(string)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Package internal provides ...
|
||||
package internal
|
||||
package disqus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -26,13 +25,19 @@ const (
|
||||
disqusAPIKey = "E8Uh5l5fHZ6gD8U3KycjAIAk46f68Zw7C6eW8WSjZvCLXebZ7p0r1yrYDrLilk2F"
|
||||
)
|
||||
|
||||
func checkDisqusConfig() error {
|
||||
if config.Conf.EiBlogApp.Disqus.ShortName != "" &&
|
||||
config.Conf.EiBlogApp.Disqus.PublicKey != "" &&
|
||||
config.Conf.EiBlogApp.Disqus.AccessToken != "" {
|
||||
return nil
|
||||
// DisqusClient disqus client
|
||||
type DisqusClient struct {
|
||||
Host string
|
||||
|
||||
Conf config.Disqus
|
||||
}
|
||||
|
||||
// NewDisqusClient new disqus client
|
||||
func NewDisqusClient(host string, conf config.Disqus) (*DisqusClient, error) {
|
||||
if conf.ShortName == "" || conf.PublicKey == "" || conf.AccessToken == "" {
|
||||
return nil, errors.New("disqus: config incompleted")
|
||||
}
|
||||
return errors.New("disqus: config incompleted")
|
||||
return &DisqusClient{Host: host, Conf: conf}, nil
|
||||
}
|
||||
|
||||
// postsCountResp 评论数量响应
|
||||
@@ -46,14 +51,10 @@ type postsCountResp struct {
|
||||
}
|
||||
|
||||
// PostsCount 获取文章评论数量
|
||||
func PostsCount(articles map[string]*model.Article) error {
|
||||
if err := checkDisqusConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cli *DisqusClient) PostsCount(articles map[string]*model.Article) error {
|
||||
vals := url.Values{}
|
||||
vals.Set("api_key", config.Conf.EiBlogApp.Disqus.PublicKey)
|
||||
vals.Set("forum", config.Conf.EiBlogApp.Disqus.ShortName)
|
||||
vals.Set("api_key", cli.Conf.PublicKey)
|
||||
vals.Set("forum", cli.Conf.ShortName)
|
||||
// batch get
|
||||
var count, index int
|
||||
for _, article := range articles {
|
||||
@@ -65,7 +66,7 @@ func PostsCount(articles map[string]*model.Article) error {
|
||||
continue
|
||||
}
|
||||
count = 0
|
||||
resp, err := httpGet(apiPostsCount + "?" + vals.Encode())
|
||||
resp, err := http.DefaultClient.Get(apiPostsCount + "?" + vals.Encode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,20 +128,16 @@ type postDetail struct {
|
||||
}
|
||||
|
||||
// PostsList 评论列表
|
||||
func PostsList(article *model.Article, cursor string) (*PostsListResp, error) {
|
||||
if err := checkDisqusConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (cli *DisqusClient) PostsList(article *model.Article, cursor string) (*PostsListResp, error) {
|
||||
vals := url.Values{}
|
||||
vals.Set("api_key", disqusAPIKey)
|
||||
vals.Set("forum", config.Conf.EiBlogApp.Disqus.ShortName)
|
||||
vals.Set("forum", cli.Conf.ShortName)
|
||||
vals.Set("thread:ident", "post-"+article.Slug)
|
||||
vals.Set("cursor", cursor)
|
||||
vals.Set("order", "popular")
|
||||
vals.Set("limit", "50")
|
||||
|
||||
resp, err := httpGet(apiPostsList + "?" + vals.Encode())
|
||||
resp, err := http.DefaultClient.Get(apiPostsList + "?" + vals.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -181,10 +178,7 @@ type PostCreateResp struct {
|
||||
}
|
||||
|
||||
// PostCreate 评论文章
|
||||
func PostCreate(pc *PostComment) (*PostCreateResp, error) {
|
||||
if err := checkDisqusConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (cli *DisqusClient) PostCreate(pc *PostComment) (*PostCreateResp, error) {
|
||||
vals := url.Values{}
|
||||
vals.Set("api_key", disqusAPIKey)
|
||||
vals.Set("message", pc.Message)
|
||||
@@ -194,8 +188,14 @@ func PostCreate(pc *PostComment) (*PostCreateResp, error) {
|
||||
vals.Set("author_name", pc.AuthorName)
|
||||
// vals.Set("state", "approved")
|
||||
|
||||
header := http.Header{"Referer": {"https://disqus.com"}}
|
||||
resp, err := httpPostHeader(apiPostCreate, vals, header)
|
||||
req, err := http.NewRequest(http.MethodPost, apiPostCreate, strings.NewReader(vals.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", "https://disqus.com")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -225,18 +225,19 @@ type approvedResp struct {
|
||||
}
|
||||
|
||||
// PostApprove 批准评论
|
||||
func PostApprove(post string) error {
|
||||
if err := checkDisqusConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cli *DisqusClient) PostApprove(post string) error {
|
||||
vals := url.Values{}
|
||||
vals.Set("api_key", config.Conf.EiBlogApp.Disqus.PublicKey)
|
||||
vals.Set("access_token", config.Conf.EiBlogApp.Disqus.AccessToken)
|
||||
vals.Set("api_key", disqusAPIKey)
|
||||
vals.Set("access_token", cli.Conf.AccessToken)
|
||||
vals.Set("post", post)
|
||||
|
||||
header := http.Header{"Referer": {"https://disqus.com"}}
|
||||
resp, err := httpPostHeader(apiPostApprove, vals, header)
|
||||
req, err := http.NewRequest(http.MethodPost, apiPostApprove, strings.NewReader(vals.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", "https://disqus.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -264,26 +265,28 @@ type threadCreateResp struct {
|
||||
}
|
||||
|
||||
// ThreadCreate 创建thread
|
||||
func ThreadCreate(article *model.Article, btitle string) error {
|
||||
if err := checkDisqusConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cli *DisqusClient) ThreadCreate(article *model.Article, btitle string) error {
|
||||
vals := url.Values{}
|
||||
vals.Set("api_key", config.Conf.EiBlogApp.Disqus.PublicKey)
|
||||
vals.Set("access_token", config.Conf.EiBlogApp.Disqus.AccessToken)
|
||||
vals.Set("forum", config.Conf.EiBlogApp.Disqus.ShortName)
|
||||
vals.Set("api_key", disqusAPIKey)
|
||||
vals.Set("access_token", cli.Conf.AccessToken)
|
||||
vals.Set("forum", cli.Conf.ShortName)
|
||||
vals.Set("title", article.Title+" | "+btitle)
|
||||
vals.Set("identifier", "post-"+article.Slug)
|
||||
|
||||
urlPath := fmt.Sprintf("https://%s/post/%s.html", config.Conf.EiBlogApp.Host, article.Slug)
|
||||
urlPath := fmt.Sprintf("https://%s/post/%s.html", cli.Host, article.Slug)
|
||||
vals.Set("url", urlPath)
|
||||
|
||||
resp, err := httpPost(apiThreadCreate, vals)
|
||||
req, err := http.NewRequest(http.MethodPost, apiThreadCreate, strings.NewReader(vals.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", "https://disqus.com")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -311,18 +314,14 @@ type threadDetailsResp struct {
|
||||
}
|
||||
|
||||
// ThreadDetails thread详细
|
||||
func ThreadDetails(article *model.Article) error {
|
||||
if err := checkDisqusConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cli *DisqusClient) ThreadDetails(article *model.Article) error {
|
||||
vals := url.Values{}
|
||||
vals.Set("api_key", config.Conf.EiBlogApp.Disqus.PublicKey)
|
||||
vals.Set("access_token", config.Conf.EiBlogApp.Disqus.AccessToken)
|
||||
vals.Set("forum", config.Conf.EiBlogApp.Disqus.ShortName)
|
||||
vals.Set("api_key", disqusAPIKey)
|
||||
vals.Set("access_token", cli.Conf.AccessToken)
|
||||
vals.Set("forum", cli.Conf.ShortName)
|
||||
vals.Set("thread:ident", "post-"+article.Slug)
|
||||
|
||||
resp, err := httpGet(apiThreadDetails + "?" + vals.Encode())
|
||||
resp, err := http.DefaultClient.Get(apiThreadDetails + "?" + vals.Encode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Package internal provides ...
|
||||
package internal
|
||||
package es
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eiblog/eiblog/pkg/config"
|
||||
"github.com/eiblog/eiblog/pkg/model"
|
||||
"github.com/eiblog/eiblog/tools"
|
||||
|
||||
@@ -29,30 +27,26 @@ const (
|
||||
ElasticType = "article"
|
||||
)
|
||||
|
||||
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(ElasticIndex, ElasticType, []byte(mappings))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// ESClient es client
|
||||
type ESClient struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func checkESConfig() error {
|
||||
if config.Conf.ESHost == "" {
|
||||
return errors.New("es: elasticsearch not config")
|
||||
// NewESClient new es client
|
||||
func NewESClient(host string) (*ESClient, error) {
|
||||
if host == "" {
|
||||
return nil, errors.New("es: elasticsearch host is empty")
|
||||
}
|
||||
return nil
|
||||
es := &ESClient{Host: host}
|
||||
err := es.createIndexAndMappings(ElasticIndex, ElasticType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return es, nil
|
||||
}
|
||||
|
||||
// ElasticSearch 搜索文章
|
||||
func ElasticSearch(query string, size, from int) (*SearchIndexResult, error) {
|
||||
if err := checkESConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (cli *ESClient) ElasticSearch(query string, size, from int) (*SearchIndexResult, error) {
|
||||
// 分析查询
|
||||
var (
|
||||
regTerm = regexp.MustCompile(`(tag|slug|date):`)
|
||||
@@ -100,17 +94,13 @@ func ElasticSearch(query string, size, from int) (*SearchIndexResult, error) {
|
||||
// 判断是否为空,判断搜索方式
|
||||
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)
|
||||
dsl = strings.ReplaceAll(strings.ReplaceAll(`{"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), "$2", fmt.Sprintf(SearchFilter, strings.Join(filter, ",")))
|
||||
}
|
||||
return indexQueryDSL(ElasticIndex, ElasticType, size, from, []byte(dsl))
|
||||
return cli.indexQueryDSL(ElasticIndex, ElasticType, size, from, []byte(dsl))
|
||||
}
|
||||
|
||||
// ElasticAddIndex 添加或更新索引
|
||||
func ElasticAddIndex(article *model.Article) error {
|
||||
if err := checkESConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cli *ESClient) ElasticAddIndex(article *model.Article) error {
|
||||
img := tools.PickFirstImage(article.Content)
|
||||
mapping := map[string]interface{}{
|
||||
"title": article.Title,
|
||||
@@ -121,20 +111,16 @@ func ElasticAddIndex(article *model.Article) error {
|
||||
"date": article.CreatedAt,
|
||||
}
|
||||
data, _ := json.Marshal(mapping)
|
||||
return indexOrUpdateDocument(ElasticIndex, ElasticType, article.ID, data)
|
||||
return cli.indexOrUpdateDocument(ElasticIndex, ElasticType, article.ID, data)
|
||||
}
|
||||
|
||||
// ElasticDelIndex 删除索引
|
||||
func ElasticDelIndex(ids []int) error {
|
||||
if err := checkESConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cli *ESClient) ElasticDelIndex(ids []int) error {
|
||||
var target []string
|
||||
for _, id := range ids {
|
||||
target = append(target, fmt.Sprint(id))
|
||||
}
|
||||
return deleteIndexDocument(ElasticIndex, ElasticType, target)
|
||||
return cli.deleteIndexDocument(ElasticIndex, ElasticType, target)
|
||||
}
|
||||
|
||||
// indicesCreateResult 索引创建结果
|
||||
@@ -143,9 +129,11 @@ type indicesCreateResult struct {
|
||||
}
|
||||
|
||||
// createIndexAndMappings 创建索引和映射关系
|
||||
func createIndexAndMappings(index, typ string, mappings []byte) error {
|
||||
rawurl := fmt.Sprintf("%s/%s/%s", config.Conf.ESHost, index, typ)
|
||||
resp, err := httpHead(rawurl)
|
||||
func (cli *ESClient) createIndexAndMappings(index, typ string) error {
|
||||
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"}}}}}`, typ)
|
||||
|
||||
rawurl := fmt.Sprintf("%s/%s/%s", cli.Host, index, typ)
|
||||
resp, err := http.DefaultClient.Head(rawurl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -154,8 +142,13 @@ func createIndexAndMappings(index, typ string, mappings []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawurl = fmt.Sprintf("%s/%s", config.Conf.ESHost, index)
|
||||
resp, err = httpPut(rawurl, mappings)
|
||||
rawurl = fmt.Sprintf("%s/%s", cli.Host, index)
|
||||
req, err := http.NewRequest(http.MethodPut, rawurl, bytes.NewReader([]byte(mappings)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,9 +169,15 @@ func createIndexAndMappings(index, typ string, mappings []byte) error {
|
||||
}
|
||||
|
||||
// indexOrUpdateDocument 创建或更新索引
|
||||
func indexOrUpdateDocument(index, typ string, id int, doc []byte) (err error) {
|
||||
rawurl := fmt.Sprintf("%s/%s/%s/%d", config.Conf.ESHost, index, typ, id)
|
||||
resp, err := httpPut(rawurl, doc)
|
||||
func (cli *ESClient) indexOrUpdateDocument(index, typ string, id int, doc []byte) (err error) {
|
||||
rawurl := fmt.Sprintf("%s/%s/%s/%d", cli.Host, index, typ, id)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, rawurl, bytes.NewReader(doc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,7 +204,7 @@ type deleteIndexResult struct {
|
||||
}
|
||||
|
||||
// deleteIndexDocument 删除文档
|
||||
func deleteIndexDocument(index, typ string, ids []string) error {
|
||||
func (cli *ESClient) deleteIndexDocument(index, typ string, ids []string) error {
|
||||
buf := bytes.Buffer{}
|
||||
for _, id := range ids {
|
||||
dd := deleteIndexReq{Index: index, Type: typ, ID: id}
|
||||
@@ -214,8 +213,13 @@ func deleteIndexDocument(index, typ string, ids []string) error {
|
||||
buf.Write(b)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
rawurl := fmt.Sprintf("%s/_bulk", config.Conf.ESHost)
|
||||
resp, err := httpPost(rawurl, buf.Bytes())
|
||||
rawurl := fmt.Sprintf("%s/_bulk", cli.Host)
|
||||
req, err := http.NewRequest(http.MethodPost, rawurl, bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -264,10 +268,10 @@ type SearchIndexResult struct {
|
||||
}
|
||||
|
||||
// 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,
|
||||
func (c *ESClient) indexQueryDSL(index, typ string, size, from int, dsl []byte) (*SearchIndexResult, error) {
|
||||
rawurl := fmt.Sprintf("%s/%s/%s/_search?size=%d&from=%d", c.Host,
|
||||
index, typ, size, from)
|
||||
resp, err := httpPost(rawurl, dsl)
|
||||
resp, err := http.Post(rawurl, "application/json", bytes.NewReader(dsl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Package internal provides ...
|
||||
package internal
|
||||
package pinger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -14,29 +13,50 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// feedrPingFunc http://<your-hub-name>.superfeedr.com/
|
||||
var feedrPingFunc = func(btitle, slug string) error {
|
||||
feedrHost := config.Conf.EiBlogApp.FeedRPC.FeedrURL
|
||||
if feedrHost == "" {
|
||||
return nil
|
||||
}
|
||||
// Pinger pinger
|
||||
type Pinger struct {
|
||||
Host string
|
||||
|
||||
Conf config.FeedRPC
|
||||
}
|
||||
|
||||
// NewPinger new pinger
|
||||
func NewPinger(host string, conf config.FeedRPC) (*Pinger, error) {
|
||||
if conf.FeedrURL == "" {
|
||||
return nil, fmt.Errorf("feedr url is empty")
|
||||
}
|
||||
return &Pinger{Host: host, Conf: conf}, nil
|
||||
}
|
||||
|
||||
// PingFunc ping blog article to SE
|
||||
func (p *Pinger) PingFunc(btitle, slug string) {
|
||||
err := p.feedrPingFunc(btitle, slug)
|
||||
if err != nil {
|
||||
logrus.Error("pinger: PingFunc feedr: ", err)
|
||||
}
|
||||
err = p.rpcPingFunc(btitle, slug)
|
||||
if err != nil {
|
||||
logrus.Error("pinger: PingFunc: rpc: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// feedrPingFunc http://<your-hub-name>.superfeedr.com/
|
||||
func (p *Pinger) feedrPingFunc(btitle, slug string) error {
|
||||
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)
|
||||
vals.Add("hub.url", fmt.Sprintf("https://%s/post/%s.html", p.Host, slug))
|
||||
resp, err := http.DefaultClient.PostForm(p.Conf.FeedrURL, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.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 fmt.Errorf("pinger: status code: %d, %s", resp.StatusCode, string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -61,16 +81,13 @@ type rpcValue struct {
|
||||
}
|
||||
|
||||
// rpcPingFunc ping rpc
|
||||
var rpcPingFunc = func(btitle, slug string) error {
|
||||
if len(config.Conf.EiBlogApp.FeedRPC.PingRPC) == 0 {
|
||||
return nil
|
||||
}
|
||||
func (p *Pinger) rpcPingFunc(btitle, slug string) error {
|
||||
param := rpcPingParam{MethodName: "weblogUpdates.extendedPing"}
|
||||
param.Params.Param = [4]rpcValue{
|
||||
0: rpcValue{Value: btitle},
|
||||
1: rpcValue{Value: "https://" + config.Conf.EiBlogApp.Host},
|
||||
2: rpcValue{Value: fmt.Sprintf("https://%s/post/%s.html", config.Conf.EiBlogApp.Host, slug)},
|
||||
3: rpcValue{Value: "https://" + config.Conf.EiBlogApp.Host + "/rss.html"},
|
||||
0: {Value: btitle},
|
||||
1: {Value: "https://" + p.Host},
|
||||
2: {Value: fmt.Sprintf("https://%s/post/%s.html", p.Host, slug)},
|
||||
3: {Value: "https://" + p.Host + "/rss.html"},
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(xml.Header)
|
||||
@@ -81,13 +98,15 @@ var rpcPingFunc = func(btitle, slug string) error {
|
||||
data := buf.Bytes()
|
||||
header := http.Header{}
|
||||
header.Set("Content-Type", "text/xml")
|
||||
for _, addr := range config.Conf.EiBlogApp.FeedRPC.PingRPC {
|
||||
resp, err := httpPostHeader(addr, data, header)
|
||||
|
||||
for _, addr := range p.Conf.PingRPC {
|
||||
resp, err := http.DefaultClient.Post(addr, "text/xml", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
logrus.Error("rpcPingFunc.httpPostHeader: ", err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logrus.Error("rpcPingFunc.ReadAll: ", err)
|
||||
@@ -99,15 +118,3 @@ var rpcPingFunc = func(btitle, slug string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PingFunc ping blog article to SE
|
||||
func PingFunc(btitle, slug string) {
|
||||
err := feedrPingFunc(btitle, slug)
|
||||
if err != nil {
|
||||
logrus.Error("pinger: PingFunc feedr: ", err)
|
||||
}
|
||||
err = rpcPingFunc(btitle, slug)
|
||||
if err != nil {
|
||||
logrus.Error("pinger: PingFunc: rpc: ", err)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// Package internal provides ...
|
||||
package internal
|
||||
package qiniu
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -15,39 +14,49 @@ import (
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
)
|
||||
|
||||
// QiniuClient qiniu client
|
||||
type QiniuClient struct {
|
||||
Conf config.Qiniu
|
||||
}
|
||||
|
||||
// NewQiniuClient new qiniu client
|
||||
func NewQiniuClient(conf config.Qiniu) (*QiniuClient, error) {
|
||||
if conf.AccessKey == "" ||
|
||||
conf.SecretKey == "" ||
|
||||
conf.Bucket == "" ||
|
||||
conf.Domain == "" {
|
||||
return nil, errors.New("qiniu config error")
|
||||
}
|
||||
return &QiniuClient{Conf: conf}, nil
|
||||
}
|
||||
|
||||
// UploadParams upload params
|
||||
type UploadParams struct {
|
||||
Name string
|
||||
Size int64
|
||||
Data io.Reader
|
||||
NoCompletePath bool
|
||||
|
||||
Conf config.Qiniu
|
||||
}
|
||||
|
||||
// QiniuUpload 上传文件
|
||||
func QiniuUpload(params UploadParams) (string, error) {
|
||||
if params.Conf.AccessKey == "" ||
|
||||
params.Conf.SecretKey == "" {
|
||||
return "", errors.New("qiniu config error")
|
||||
}
|
||||
// Upload 上传文件
|
||||
func (cli *QiniuClient) Upload(params UploadParams) (string, error) {
|
||||
key := params.Name
|
||||
if !params.NoCompletePath {
|
||||
key = filepath.Base(params.Name)
|
||||
}
|
||||
|
||||
mac := qbox.NewMac(params.Conf.AccessKey,
|
||||
params.Conf.SecretKey)
|
||||
mac := qbox.NewMac(cli.Conf.AccessKey,
|
||||
cli.Conf.SecretKey)
|
||||
// 设置上传策略
|
||||
putPolicy := &storage.PutPolicy{
|
||||
Scope: params.Conf.Bucket,
|
||||
Scope: cli.Conf.Bucket,
|
||||
Expires: 3600,
|
||||
InsertOnly: 1,
|
||||
}
|
||||
// 上传token
|
||||
uploadToken := putPolicy.UploadToken(mac)
|
||||
// 上传配置
|
||||
region, err := storage.GetRegion(params.Conf.AccessKey, params.Conf.Bucket)
|
||||
region, err := storage.GetRegion(cli.Conf.AccessKey, cli.Conf.Bucket)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -65,7 +74,7 @@ func QiniuUpload(params UploadParams) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
url := "https://" + params.Conf.Domain + "/" + key
|
||||
url := "https://" + cli.Conf.Domain + "/" + key
|
||||
return url, nil
|
||||
}
|
||||
|
||||
@@ -74,21 +83,19 @@ type DeleteParams struct {
|
||||
Name string
|
||||
Days int
|
||||
NoCompletePath bool
|
||||
|
||||
Conf config.Qiniu
|
||||
}
|
||||
|
||||
// QiniuDelete 删除文件
|
||||
func QiniuDelete(params DeleteParams) error {
|
||||
func (cli *QiniuClient) Delete(params DeleteParams) error {
|
||||
key := params.Name
|
||||
if !params.NoCompletePath {
|
||||
key = completeQiniuKey(params.Name)
|
||||
}
|
||||
|
||||
mac := qbox.NewMac(params.Conf.AccessKey,
|
||||
params.Conf.SecretKey)
|
||||
mac := qbox.NewMac(cli.Conf.AccessKey,
|
||||
cli.Conf.SecretKey)
|
||||
// 上传配置
|
||||
region, err := storage.GetRegion(params.Conf.AccessKey, params.Conf.Bucket)
|
||||
region, err := storage.GetRegion(cli.Conf.AccessKey, cli.Conf.Bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -100,24 +107,22 @@ func QiniuDelete(params DeleteParams) error {
|
||||
bucketManager := storage.NewBucketManager(mac, cfg)
|
||||
// Delete
|
||||
if params.Days > 0 {
|
||||
return bucketManager.DeleteAfterDays(params.Conf.Bucket, key, params.Days)
|
||||
return bucketManager.DeleteAfterDays(cli.Conf.Bucket, key, params.Days)
|
||||
}
|
||||
return bucketManager.Delete(params.Conf.Bucket, key)
|
||||
return bucketManager.Delete(cli.Conf.Bucket, key)
|
||||
}
|
||||
|
||||
// ContentParams list params
|
||||
type ContentParams struct {
|
||||
Prefix string
|
||||
|
||||
Conf config.Qiniu
|
||||
}
|
||||
|
||||
// QiniuContent 获取文件列表
|
||||
func QiniuContent(params ContentParams) ([]byte, error) {
|
||||
mac := qbox.NewMac(params.Conf.AccessKey,
|
||||
params.Conf.SecretKey)
|
||||
// Content 获取文件内容
|
||||
func (cli *QiniuClient) Content(params ContentParams) ([]byte, error) {
|
||||
mac := qbox.NewMac(cli.Conf.AccessKey,
|
||||
cli.Conf.SecretKey)
|
||||
// region
|
||||
region, err := storage.GetRegion(params.Conf.AccessKey, params.Conf.Bucket)
|
||||
region, err := storage.GetRegion(cli.Conf.AccessKey, cli.Conf.Bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,7 +133,7 @@ func QiniuContent(params ContentParams) ([]byte, error) {
|
||||
// manager
|
||||
bucketManager := storage.NewBucketManager(mac, cfg)
|
||||
// list file
|
||||
files, _, _, _, err := bucketManager.ListFiles(params.Conf.Bucket, params.Prefix, "", "", 1)
|
||||
files, _, _, _, err := bucketManager.ListFiles(cli.Conf.Bucket, params.Prefix, "", "", 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,7 +141,7 @@ func QiniuContent(params ContentParams) ([]byte, error) {
|
||||
return nil, errors.New("no file")
|
||||
}
|
||||
deadline := time.Now().Add(time.Second * 60).Unix()
|
||||
url := storage.MakePrivateURLv2(mac, "https://"+params.Conf.Domain, files[0].Key, deadline)
|
||||
url := storage.MakePrivateURLv2(mac, "https://"+cli.Conf.Domain, files[0].Key, deadline)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -151,27 +156,16 @@ func completeQiniuKey(name string) string {
|
||||
ext := filepath.Ext(name)
|
||||
|
||||
switch ext {
|
||||
case ".bmp", ".png", ".jpg",
|
||||
".gif", ".ico", ".jpeg":
|
||||
|
||||
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":
|
||||
|
||||
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":
|
||||
|
||||
case ".txt", ".md", ".ini", ".yaml", ".yml", ".doc", ".ppt", ".pdf":
|
||||
name = "blog/document/" + name
|
||||
case ".zip", ".rar", ".tar",
|
||||
".gz":
|
||||
|
||||
case ".zip", ".rar", ".tar", ".gz":
|
||||
name = "blog/archive/" + name
|
||||
default:
|
||||
name = "blog/other/" + name
|
||||
@@ -1,5 +1,4 @@
|
||||
// Package internal provides ...
|
||||
package internal
|
||||
package qiniu
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -10,6 +9,16 @@ import (
|
||||
)
|
||||
|
||||
func TestQiniuUpload(t *testing.T) {
|
||||
cli, err := NewQiniuClient(config.Qiniu{
|
||||
AccessKey: os.Getenv("QINIU_ACCESSKEY"),
|
||||
SecretKey: os.Getenv("QINIU_SECRETKEY"),
|
||||
Bucket: os.Getenv("QINIU_BUCKET"),
|
||||
Domain: os.Getenv("QINIU_DOMAIN"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("NewQiniuClient error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
f, _ := os.Open("qiniu_test.go")
|
||||
fi, _ := f.Stat()
|
||||
@@ -27,17 +36,11 @@ func TestQiniuUpload(t *testing.T) {
|
||||
Name: "test-" + time.Now().Format("200601021504059999") + ".go",
|
||||
Size: fi.Size(),
|
||||
Data: f,
|
||||
Conf: config.Qiniu{
|
||||
AccessKey: os.Getenv("QINIU_ACCESSKEY"),
|
||||
SecretKey: os.Getenv("QINIU_SECRETKEY"),
|
||||
Bucket: os.Getenv("QINIU_BUCKET"),
|
||||
Domain: "bu.st.deepzz.com",
|
||||
},
|
||||
}}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := QiniuUpload(tt.args.params)
|
||||
got, err := cli.Upload(tt.args.params)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("QiniuUpload() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -48,15 +51,21 @@ func TestQiniuUpload(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQiniuContent(t *testing.T) {
|
||||
params := ContentParams{
|
||||
Conf: config.Qiniu{
|
||||
AccessKey: os.Getenv("QINIU_ACCESSKEY"),
|
||||
SecretKey: os.Getenv("QINIU_SECRETKEY"),
|
||||
Bucket: os.Getenv("QINIU_BUCKET"),
|
||||
Domain: "bu.st.deepzz.com",
|
||||
},
|
||||
cli, err := NewQiniuClient(config.Qiniu{
|
||||
AccessKey: os.Getenv("QINIU_ACCESSKEY"),
|
||||
SecretKey: os.Getenv("QINIU_SECRETKEY"),
|
||||
Bucket: os.Getenv("QINIU_BUCKET"),
|
||||
Domain: os.Getenv("QINIU_DOMAIN"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("NewQiniuClient error = %v", err)
|
||||
return
|
||||
}
|
||||
_, err := QiniuContent(params)
|
||||
|
||||
params := ContentParams{
|
||||
Prefix: "blog/",
|
||||
}
|
||||
_, err = cli.Content(params)
|
||||
if err != nil {
|
||||
t.Errorf("QiniuList error = %v", err)
|
||||
}
|
||||
Reference in New Issue
Block a user