Repository: go-chinese-site/dreamgo
Branch: master
Commit: 43cec88c782a
Files: 52
Total size: 80.2 KB
Directory structure:
gitextract_maqhqepw/
├── .gitignore
├── .travis.yml
├── Dockerfile
├── Dockerfile_release
├── LICENSE
├── README.md
├── config/
│ └── env.yml
├── dreamgo.sql
├── getpkg.bat
├── getpkg.sh
├── install.bat
├── install.sh
├── run.bat
├── run.sh
├── src/
│ ├── .gitignore
│ ├── config/
│ │ └── config.go
│ ├── datasource/
│ │ ├── ds.go
│ │ ├── github_repo.go
│ │ ├── github_repo_test.go
│ │ ├── mongodb.go
│ │ ├── mysql_repo.go
│ │ └── mysql_repo_test.go
│ ├── dreamgo/
│ │ └── main.go
│ ├── global/
│ │ └── app.go
│ ├── http/
│ │ └── controller/
│ │ ├── about.go
│ │ ├── archive.go
│ │ ├── friends.go
│ │ ├── index.go
│ │ ├── post.go
│ │ ├── routes.go
│ │ ├── static.go
│ │ └── tag.go
│ ├── logger/
│ │ └── log.go
│ ├── model/
│ │ ├── archive.go
│ │ ├── friend.go
│ │ ├── post.go
│ │ └── tag.go
│ ├── route/
│ │ └── mux.go
│ ├── util/
│ │ ├── file.go
│ │ └── util.go
│ ├── vendor/
│ │ └── manifest
│ └── view/
│ └── template.go
├── static/
│ └── css/
│ ├── main.css
│ └── post.css
└── template/
└── theme/
└── default/
├── about.html
├── archives.html
├── friends.html
├── index.html
├── layout.html
├── single.html
├── tag.html
└── tags.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
.vscode/
bin
pkg
log
data
.idea/
================================================
FILE: .travis.yml
================================================
language: go
go:
- 1.8.x
- 1.9.x
- tip
sudo: false
install:
- export GOPATH=$HOME/gopath/src/github.com/go-chinese-site/dreamgo
- export PATH=$PATH:$HOME/gopath/src/github.com/go-chinese-site/dreamgo/bin/
- go get -v github.com/FiloSottile/gvt
script:
- sh getpkg.sh
- sh install.sh
================================================
FILE: Dockerfile
================================================
FROM golang
RUN go get github.com/polaris1119/gvt
RUN ln -sf /go/bin/gvt /usr/local/bin/
ADD . /dreamgo
# install dreamgo
WORKDIR /dreamgo
RUN ./getpkg.sh
RUN ./install.sh
EXPOSE 2017
ENTRYPOINT [ "./bin/dreamgo" ]
================================================
FILE: Dockerfile_release
================================================
FROM golang
RUN mkdir /dreamgo
RUN mkdir /dreamgo/log
ADD ./bin /dreamgo/bin
ADD ./config /dreamgo/config
ADD ./static /dreamgo/static
ADD ./template /dreamgo/template
EXPOSE 2017
WORKDIR /dreamgo
# Define default command.
CMD [ "/dreamgo/bin/dreamgo" ]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Go Chinese Site
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# dreamgo
[](https://travis-ci.org/go-chinese-site/dreamgo)
一个新手学习用的博客系统,用 Go 语言实现自己的梦想。
## 开发计划
### 该博客系统计划采用三种方式实现
1. 不使用框架,直接使用标准库 net/http, branch-std
2. 使用一些基本的路由库,比如 https://github.com/gorilla/mux 或 https://github.com/julienschmidt/httprouter, branch-mux
3. 使用一个 Web 框架,可能考虑使用 Beego,因为国内貌似用这个的比较多,满足广大 gopher 的要求, branch-beego
### 开发时间
2017 年 10 月 1 日开始
## 合作方式
通过 net/http 搭建起来基本的架子后,大家可以以此为基础,加入进来
## 设计说明
1. 数据源(存储),支持三种:
1. 将文章存在 Github Repo 上;
2. 将文章存入 MySQL 中;
3. 将文字存入 MongoDB 中;
2. 支持自定义模板
3. 通过 yaml 做项目配置
## Roadmap
1. master 分支和 branch-std 分支采用 net/http 方式实现。
- 目前已实现了如下功能:
1. 基于 http.ServeMux 的简单封装:route.BlogMux,方便写中间件;
2. 完成基于 github repo 的首页、归档、文章;
3. 完成日志功能,在main.go中已实例化,其他地方调用logger := logger.Instance()即可;
4. tag 列表和 tag 文章列表页
5. 关于页面
- 还未实现的功能:(大家可以认领,提 issue 告知要开发哪个或加入 qq 群沟通 195831198)
1. ~~tag 列表和 tag 文章列表页~~;
2. 友情链接页;
3. ~~关于页面~~;
4. 基于 mysql、mongodb 的存储实现,通过配置切换存储;
5. 管理后台;
2. 使用一些基本的路由库,比如 https://github.com/gorilla/mux 或 https://github.com/julienschmidt/httprouter, branch-mux 还未动工;
3. 使用一个 Web 框架,可能考虑使用 Beego,因为国内貌似用这个的比较多,满足广大 gopher 的要求, branch-beego, 还未动工
## Install
要求:Go 1.8 及以上
**注:如果你是 Windows,请将 `.sh` 的脚本改为 `.bat`**
1. 本项目使用 `gvt` 作为依赖管理工具,通过 `go get github.com/polaris1119/gvt` 安装,并将 gvt 放入 PATH 中;
2. 下载 dreamgo 源码:`git clone https://github.com/go-chinese-site/dreamgo`,比如下载到 ~/dreamgo 中;
3. cd ~/dreamgo,执行 ./getpkg.sh;
4. 执行 ./install.sh
5. 启动 dreamgo:bin/dreamgo 或 执行 ./run.sh
通过浏览器访问:http://localhost:2017

## 如何贡献代码
1. fork,编码,pull request;
2. 通过一次 pull request 后,会把你加入该项目的协作者中,之后就可以直接在该项目中写代码、push 了。
================================================
FILE: config/env.yml
================================================
# listen
listen:
host: 0.0.0.0
port: 2017
setting:
site_name: polaris 的站点
title: Polaris Xu
subtitle: 专注 Go 语言
seo:
keywords: polaris,go,dreamgo
description: 这是我的个人博客
# datasource
datasource:
type: git
url: https://github.com/go-chinese-site/dreamgo-demo
monogdbaddr: 192.168.0.103:27017
monogdbdb: test
mysqlAddr: dreamgo:123456@tcp(127.0.0.1:3306)/dreamgo
# theme
theme: default
================================================
FILE: dreamgo.sql
================================================
-- MySQL dump 10.13 Distrib 5.7.13, for osx10.11 (x86_64)
--
-- Host: 13.229.128.253 Database: dreamgo
-- ------------------------------------------------------
-- Server version 5.7.20
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `article`
--
DROP TABLE IF EXISTS `article`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`title` text NOT NULL COMMENT '标题',
`pub_time` bigint(20) NOT NULL COMMENT '发布时间',
`content` text NOT NULL COMMENT '内容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='文章';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `article`
--
LOCK TABLES `article` WRITE;
/*!40000 ALTER TABLE `article` DISABLE KEYS */;
INSERT INTO `article` VALUES (1,'关于 DreamGo',1506694800,'# 关于 DreamGO\n\nDreamGo 是一个新手学习用的博客系统。计划实现三个版本:\n\n1. 不使用框架,基于标准库 `net/http` 实现;\n2. 使用第三方路由,比如 https://github.com/gorilla/mux 或 https://github.com/julienschmidt/httprouter 实现;\n3. 使用一个 Web 框架,可能考虑使用 Beego,因为国内貌似用这个的比较多,满足广大 gopher 的要求;\n\n数据源计划支持三种方式,尽可能让大家练习使用 Go 语言操作常用的存储:\n\n1. 将文章存在 Github Repo 上;\n2. 将文章存入 MySQL 中;\n3. 将文字存入 MongoDB 中;\n');
/*!40000 ALTER TABLE `article` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `article_tag`
--
DROP TABLE IF EXISTS `article_tag`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `article_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`article_id` bigint(20) NOT NULL COMMENT '文章id',
`tag_id` int(11) NOT NULL COMMENT '标签id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='文章标签';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `article_tag`
--
LOCK TABLES `article_tag` WRITE;
/*!40000 ALTER TABLE `article_tag` DISABLE KEYS */;
INSERT INTO `article_tag` VALUES (1,1,1),(2,1,2),(3,1,3),(4,1,4);
/*!40000 ALTER TABLE `article_tag` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `db_version`
--
DROP TABLE IF EXISTS `db_version`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `db_version` (
`version` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `db_version`
--
LOCK TABLES `db_version` WRITE;
/*!40000 ALTER TABLE `db_version` DISABLE KEYS */;
INSERT INTO `db_version` VALUES (2017101301);
/*!40000 ALTER TABLE `db_version` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `friend_link`
--
DROP TABLE IF EXISTS `friend_link`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `friend_link` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '名称',
`link` varchar(128) NOT NULL DEFAULT '' COMMENT '链接',
`logo` varchar(128) NOT NULL DEFAULT '' COMMENT '图标',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='友情链接';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `friend_link`
--
LOCK TABLES `friend_link` WRITE;
/*!40000 ALTER TABLE `friend_link` DISABLE KEYS */;
INSERT INTO `friend_link` VALUES (1,'Go语言中文网','https://studygolang.com','https://static.studygolang.com/img/favicon.ico');
/*!40000 ALTER TABLE `friend_link` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `tag`
--
DROP TABLE IF EXISTS `tag`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tag` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(128) NOT NULL COMMENT '名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='标签';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `tag`
--
LOCK TABLES `tag` WRITE;
/*!40000 ALTER TABLE `tag` DISABLE KEYS */;
INSERT INTO `tag` VALUES (1,'dreamgo'),(2,'博客系统'),(3,'标准库'),(4,'路由');
/*!40000 ALTER TABLE `tag` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2017-12-18 10:54:12
================================================
FILE: getpkg.bat
================================================
@echo off
setlocal
if exist getpkg.bat goto ok
echo getpkg.bat must be run from its folder
goto end
:ok
set OLDGOPATH=%GOPATH%
set GOPATH=%~dp0
cd src
gvt restore -connections 8 -precaire
cd ..
set GOPATH=%OLDGOPATH%
:end
echo finished
================================================
FILE: getpkg.sh
================================================
#!/usr/bin/env bash
set -e
if [ ! -f getpkg.sh ]; then
echo 'getpkg.sh must be run within its container folder' 1>&2
exit 1
fi
if ! type gvt >/dev/null 2>&1; then
echo >&2 "This script requires the gvt tool."
echo >&2 "You may obtain it with the following command:"
echo >&2 "go get github.com/polaris1119/gvt"
exit 1
fi
OLDGOPATH="$GOPATH"
export GOPATH=`pwd`
cd src
if [ "$1" = "update" ]; then
if [ -d "vendor/github.com" ]; then
gvt update -all
fi
elif [ -f "vendor/manifest" ]; then
gvt restore -connections 8 -precaire
fi
cd ..
export GOPATH="$OLDGOPATH"
echo 'finished'
================================================
FILE: install.bat
================================================
@echo off
setlocal
if exist install.bat goto ok
echo install.bat must be run from its folder
goto end
:ok
set GOBIN=
set OLDGOPATH=%GOPATH%
set GOPATH=%~dp0
if not exist log mkdir log
gofmt -w -s src
go install dreamgo
set GOPATH=%OLDGOPATH%
:end
echo finished
================================================
FILE: install.sh
================================================
#!/usr/bin/env bash
set -e
if [ ! -f install.sh ]; then
echo 'install must be run within its container folder' 1>&2
exit 1
fi
CURDIR=`pwd`
OLDGOPATH="$GOPATH"
export GOPATH="$CURDIR"
export GOBIN=
if [ ! -d log ]; then
mkdir log
fi
gofmt -w -s src
go install dreamgo
export GOPATH="$OLDGOPATH"
echo 'finished'
================================================
FILE: run.bat
================================================
@echo off
setlocal
if exist run.bat goto exec
echo run.bat must be run from its folder
:exec
tasklist /nh|find /i "dreamgo"
if ERRORLEVEL 1 (
goto reload
) else (
goto kill
)
:reload
call install.bat
if exist bin\dreamgo.exe (
echo .........rebuild success.........
goto run
)else (
echo .........the command install fail.........
goto end
)
:run
echo .........the program is starting.......
start /min bin\dreamgo.exe
echo .........started success.........
goto end
:kill
echo .........run kill exist process.........
taskkill /f /im "dreamgo.exe" /t >null 2>&1
goto reload
:end
echo run finished
================================================
FILE: run.sh
================================================
#!/usr/bin/env bash
#set -e
if [ ! -f run.sh ]; then
echo 'install must be run within its container folder' 1>&2
exit 1
fi
ps -ef|grep dreamgo |grep -v grep
if [ $? -ne 0 ];then
source ./install.sh
if [ ! -f bin/dreamgo ];then
echo .........the command install fail.........
exit 1
else
echo .........the program is starting.......
nohup ./bin/dreamgo >/dev/null 2>&1 &
echo .........started success.........
echo finished
fi
else
echo .........run kill exist process.........
ps -ef|grep dreamgo |grep -v grep |awk '{print $2}' |xargs kill -9 >/dev/null 2>&1
source ./install.sh
if [ ! -f bin/dreamgo ];then
echo .........the command install fail.........
exit 1
else
echo .........the program is starting.......
nohup ./bin/dreamgo >/dev/null 2>&1 &
echo .........started success.........
echo start finished
fi
fi
================================================
FILE: src/.gitignore
================================================
vendor/**
!vendor/manifest
================================================
FILE: src/config/config.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package config
import (
"github.com/go-chinese-site/cfg"
)
// YamlConfig stores the config content
var YamlConfig *cfg.YamlConfig
// Parse parses the configFile into YamlConfig
func Parse(configFile string) {
var err error
YamlConfig, err = cfg.ParseYaml(configFile)
if err != nil {
panic(err)
}
}
================================================
FILE: src/datasource/ds.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package datasource
import (
"bytes"
"config"
"model"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/pkg/errors"
"github.com/sourcegraph/syntaxhighlight"
)
// 数据源类型
const (
TypeGit = "git"
TypeMysql = "mysql"
)
// DataSourcer 数据源接口
type DataSourcer interface {
PostList() []*model.Post
PostArchive() []*model.YearArchive
ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string)
FindPost(path string) (*model.Post, error)
TagList() []*model.Tag
FindTag(tagName string) *model.Tag
AboutPost() (*model.Post, error)
UpdateDataSource()
GetFriends() ([]*model.Friend, error)
}
// DefaultDataSourcer 默认数据源
var DefaultDataSourcer DataSourcer
// Init 数据源初始化
func Init() {
dataSourcerType := config.YamlConfig.Get("datasource.type").String()
switch dataSourcerType {
case "git":
DefaultDataSourcer = NewGithub()
case "mongodb":
DefaultDataSourcer = NewMongoDB()
case "mysql":
DefaultDataSourcer = NewMysql(config.YamlConfig.Get("datasource.mysqlAddr").String())
default:
DefaultDataSourcer = NewGithub()
}
go DefaultDataSourcer.UpdateDataSource()
}
func replaceCodeParts(htmlFile []byte) (string, error) {
byteReader := bytes.NewReader(htmlFile)
doc, err := goquery.NewDocumentFromReader(byteReader)
if err != nil {
return "", errors.Wrap(err, "error while parsing html")
}
// find code-parts via css selector and replace them with highlighted versions
doc.Find("code[class*=\"language-\"]").Each(func(i int, s *goquery.Selection) {
oldCode := s.Text()
formatted, _ := syntaxhighlight.AsHTML([]byte(oldCode))
s.SetHtml(string(formatted))
})
new, err := doc.Html()
if err != nil {
return "", errors.Wrap(err, "error while generating html")
}
// replace unnecessarily added html tags
new = strings.Replace(new, "<html><head></head><body>", "", 1)
new = strings.Replace(new, "</body></html>", "", 1)
return new, nil
}
================================================
FILE: src/datasource/github_repo.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package datasource
import (
"config"
"global"
"io/ioutil"
"log"
"model"
"net/http"
"os"
"os/exec"
"regexp"
"sort"
"strings"
"time"
"util"
"github.com/pkg/errors"
"github.com/robfig/cron"
"github.com/russross/blackfriday"
yaml "gopkg.in/yaml.v2"
)
const (
// PostDir 文章存放目录
PostDir = "data/post/"
// IndexFile 首页数据文件
IndexFile = "index.yaml"
// ArchiveFile 归档数据文件
ArchiveFile = "archive.yaml"
// TagsFile 标签数据文件
TagsFile = "tags.yaml"
// FriendFile 友情链接数据文件
FriendFile = "friends.yaml"
)
// GithubRepo git数据源结构体
type GithubRepo struct{}
// NewGithub 创建git数据源实例,相当于构造方法
func NewGithub() *GithubRepo {
return &GithubRepo{}
}
// PostList 读取文章列表
func (self GithubRepo) PostList() []*model.Post {
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + IndexFile)
if err != nil {
return nil
}
posts := make([]*model.Post, 0)
err = yaml.Unmarshal(in, &posts)
if err != nil {
return nil
}
return posts
}
// PostArchive 读取归档列表
func (self GithubRepo) PostArchive() []*model.YearArchive {
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + ArchiveFile)
if err != nil {
return nil
}
yearArchives := make([]*model.YearArchive, 0)
err = yaml.Unmarshal(in, &yearArchives)
if err != nil {
return nil
}
return yearArchives
}
// ServeMarkdown 处理查看 Markdown 请求
func (self GithubRepo) ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string) {
http.ServeFile(w, r, global.App.ProjectRoot+PostDir+util.Filename(filename)+"/post.md")
}
var titleReg = regexp.MustCompile(`^#\s(.+)`)
// FindPost 根据路径查找文章
func (self GithubRepo) FindPost(path string) (*model.Post, error) {
postDir := global.App.ProjectRoot + PostDir + path
post, err := self.genOnePost(postDir, path)
if err == nil {
post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))
}
return post, err
}
// Pull 使用 git pull origin master 命令从远程仓库更新文章
func (self GithubRepo) Pull(gitRepoDir string) error {
cmdName := "git"
pullArgs := []string{"pull", "origin", "master"}
cmd := exec.Command(cmdName, pullArgs...)
cmd.Dir = gitRepoDir
if err := cmd.Run(); err != nil {
log.Printf("error pulling master at %s: %v", gitRepoDir, err)
return err
}
return nil
}
// GenIndexYaml 生成首页数据文件index.yaml
func (self GithubRepo) GenIndexYaml() {
posts := self.fetchPosts()
// 首页最多显示20篇文章
length := 20
if len(posts) < length {
length = len(posts)
}
buf, err := yaml.Marshal(posts[:length])
if err != nil {
log.Printf("gen index yaml error:%v\n", err)
return
}
indexYaml := global.App.ProjectRoot + PostDir + IndexFile
ioutil.WriteFile(indexYaml, buf, 0777)
}
// GenArchiveYaml 生成归档数据文件archive.yaml
func (self GithubRepo) GenArchiveYaml() {
posts := self.fetchPosts()
yearArchiveMap := make(map[int]*model.YearArchive)
for _, post := range posts {
post.Content = ""
year := post.PostTime.Year()
month := int(post.PostTime.Month())
if yearArchive, ok := yearArchiveMap[year]; ok {
monthExists := false
for _, monthArchive := range yearArchive.MonthArchives {
if monthArchive.Month == month {
monthArchive.Posts = append(monthArchive.Posts, post)
monthExists = true
break
}
}
if !monthExists {
yearArchive.MonthArchives = append(yearArchive.MonthArchives, &model.MonthArchive{
Month: month,
Posts: []*model.Post{post},
})
}
} else {
monthArchive := &model.MonthArchive{
Month: month,
Posts: []*model.Post{post},
}
yearArchive = &model.YearArchive{
Year: year,
MonthArchives: []*model.MonthArchive{monthArchive},
}
yearArchiveMap[year] = yearArchive
}
}
yearArchives := make([]*model.YearArchive, 0, len(yearArchiveMap))
for _, yearArchive := range yearArchiveMap {
yearArchives = append(yearArchives, yearArchive)
}
sort.Slice(yearArchives, func(i, j int) bool {
return yearArchives[i].Year > yearArchives[j].Year
})
buf, err := yaml.Marshal(yearArchives)
if err != nil {
log.Printf("gen archives yaml error:%v\n", err)
return
}
archiveYaml := global.App.ProjectRoot + PostDir + ArchiveFile
ioutil.WriteFile(archiveYaml, buf, 0777)
}
// GenTagsYaml 生成标签数据文件tags.yaml
func (self GithubRepo) GenTagsYaml() {
allPosts := self.fetchPosts()
tagMap := make(map[string][]*model.Post)
// 遍历所有文章对象,分析出标签数据
for _, post := range allPosts {
post.Content = ""
for _, tag := range post.Tags {
posts, ok := tagMap[tag]
if !ok {
posts = make([]*model.Post, 0)
}
posts = append(posts, post)
tagMap[tag] = posts
}
}
// 组装标签列表
tags := make([]*model.Tag, 0)
for tag, posts := range tagMap {
sort.Slice(posts, func(i, j int) bool {
return posts[i].PubTime > posts[j].PubTime
})
tags = append(tags, &model.Tag{Name: tag, Posts: posts})
}
// 按文件数量倒序排序
sort.Slice(tags, func(i, j int) bool {
return len(tags[i].Posts) > len(tags[j].Posts)
})
buf, err := yaml.Marshal(tags)
if err != nil {
log.Printf("gen tags yaml error:%v\n", err)
return
}
tagsYaml := global.App.ProjectRoot + PostDir + TagsFile
ioutil.WriteFile(tagsYaml, buf, 0777)
}
// fetchPosts 读取所有文章数据,遍历目录,解析每个目录中的meta.yaml和post.md
func (self GithubRepo) fetchPosts() []*model.Post {
var (
posts = make([]*model.Post, 0, 31)
post *model.Post
err error
)
// 遍历 data/post 下的目录
postDir := global.App.ProjectRoot + PostDir
names := util.ScanDir(postDir)
for _, name := range names {
if util.IsFile(postDir + name) {
continue
}
if name == ".git" {
continue
}
post, err = self.genOnePost(postDir+name, name)
if err != nil {
continue
}
pos := strings.Index(post.Content, `<!--more-->`)
if pos > 0 {
post.Content = post.Content[:pos]
}
posts = append(posts, post)
}
// 按发布时间倒序排序
sort.Slice(posts, func(i, j int) bool {
return posts[i].PubTime > posts[j].PubTime
})
return posts
}
// genOnePost 解析meta.yaml和post.md文件生成model.Post对象
func (self GithubRepo) genOnePost(postDir, path string) (*model.Post, error) {
// 从post.md中读取文章内容
markdown, err := ioutil.ReadFile(postDir + "/post.md")
if err != nil {
return nil, errors.Wrap(err, "read post.md error")
}
// 从meta.yml文件读取文章信息
var meta = &model.Meta{}
metaBytes, err := ioutil.ReadFile(postDir + "/meta.yml")
if err == nil {
err = yaml.Unmarshal(metaBytes, meta)
if err != nil {
return nil, errors.Wrap(err, "yaml unmarshal meta.yml error")
}
meta.PostTime = self.parsePubTime(meta.PubTime)
} else {
meta.Path = path + ".html"
fileInfo, _ := os.Stat(postDir + "/post.md")
meta.PostTime = fileInfo.ModTime()
meta.PubTime = meta.PostTime.Format("2006-01-02 15:04")
matches := titleReg.FindStringSubmatch(string(markdown))
if len(matches) > 2 {
meta.Title = matches[1]
} else {
meta.Title = path
}
}
post := &model.Post{
Content: string(markdown),
Meta: meta,
}
return post, nil
}
// parsePubTime 解析发布时间
func (self GithubRepo) parsePubTime(pubTime string) time.Time {
layouts := []string{
"2006-01-02 15:04:05",
"2006-01-02 15:04",
"2006年01月02 15:04:05",
"2006年01月02 15:04",
}
for _, layout := range layouts {
t, err := time.ParseInLocation(layout, pubTime, time.Local)
if err != nil {
continue
}
return t
}
return time.Now()
}
// TagList 读取标签列表
func (self GithubRepo) TagList() []*model.Tag {
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + TagsFile)
if err != nil {
return nil
}
tags := make([]*model.Tag, 0)
err = yaml.Unmarshal(in, &tags)
if err != nil {
return nil
}
return tags
}
// FindTag 通过标签名查找标签
func (self GithubRepo) FindTag(tagName string) *model.Tag {
tags := self.TagList()
for _, tag := range tags {
if tag.Name == tagName {
return tag
}
}
return nil
}
// AboutPost 获取关于页
func (self GithubRepo) AboutPost() (*model.Post, error) {
// 从 about.md 中读取关于内容
postDir := global.App.ProjectRoot + PostDir
markdown, err := ioutil.ReadFile(postDir + "/about.md")
if err != nil {
return nil, errors.Wrap(err, "read about.md error")
}
// 关于页不需要 meta.yml
var meta = &model.Meta{}
post := &model.Post{
Content: string(markdown),
Meta: meta,
}
return post, nil
}
// UpdateDataSource 更新数据
func (self GithubRepo) UpdateDataSource() {
// 检查文章目录(data/post/)是否存在,不存在则克隆远程仓库
gitRepoDir := global.App.ProjectRoot + PostDir
if !util.Exist(gitRepoDir) {
if err := os.MkdirAll(gitRepoDir, os.ModePerm); err != nil {
panic(err)
}
self.cloneRepo(gitRepoDir)
}
gitFolder := gitRepoDir + ".git"
for {
if util.Exist(gitFolder) {
break
}
self.cloneRepo(gitRepoDir)
}
// 解析仓库文件,生成首页、归档、标签数据
self.GenIndexYaml()
self.GenArchiveYaml()
self.GenTagsYaml()
// 定时每天自动更新仓库,并生成首页、归档、标签数据
c := cron.New()
c.AddFunc("@daily", func() {
self.Pull(gitRepoDir)
self.GenIndexYaml()
self.GenArchiveYaml()
self.GenTagsYaml()
})
c.Start()
}
// 使用git clone命令克隆文章仓库
func (self GithubRepo) cloneRepo(gitRepoDir string) {
cmdName := "git"
pullArgs := []string{"clone", config.YamlConfig.Get("datasource.url").String(), "."}
cmd := exec.Command(cmdName, pullArgs...)
cmd.Dir = gitRepoDir
if err := cmd.Run(); err != nil {
log.Printf("error clone master at %s: %v", gitRepoDir, err)
return
}
}
// GetFriends 友情链接
func (self GithubRepo) GetFriends() ([]*model.Friend, error) {
// 从friends.yaml 中读取友情链接内容
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + FriendFile)
if err != nil {
return nil, errors.Wrap(err, "read friends.yaml error")
}
friends := make([]*model.Friend, 0)
err = yaml.Unmarshal(in, &friends)
if err != nil {
return nil, errors.Wrap(err, "Unmarshal friends.yaml error")
}
return friends, nil
}
================================================
FILE: src/datasource/github_repo_test.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package datasource_test
import (
"datasource"
"global"
"os"
"strings"
"testing"
)
// DefaultGithub git数据源结构体实例
var DefaultGithub *datasource.GithubRepo
func setup() {
cwd, _ := os.Getwd()
pos := strings.LastIndex(cwd, "src")
global.App.ProjectRoot = cwd[:pos]
DefaultGithub = datasource.NewGithub()
}
func TestGenIndexYaml(t *testing.T) {
setup()
DefaultGithub.GenIndexYaml()
}
func TestGenArchiveYaml(t *testing.T) {
setup()
DefaultGithub.GenArchiveYaml()
}
func TestGenTagsYaml(t *testing.T) {
setup()
DefaultGithub.GenTagsYaml()
}
================================================
FILE: src/datasource/mongodb.go
================================================
package datasource
import (
"config"
"log"
"model"
"net/http"
"sort"
"time"
"github.com/russross/blackfriday"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
// MongoDB 数据源结构体
type MongoDB struct {
session *mgo.Session
addr string
db string
}
// NewMongoDB 创建MongoDB数据源实例,相当于构造方法
func NewMongoDB() *MongoDB {
addr := config.YamlConfig.Get("datasource.monogdbaddr").String()
db := config.YamlConfig.Get("datasource.monogdbdb").String()
if len(addr) <= 0 || len(addr) <= 0 {
log.Fatalf("get mongodb addr or db failed addr [%s] db[%s]\n", addr, db)
}
mongoDBDialInfo := &mgo.DialInfo{
Addrs: []string{addr},
Timeout: 10 * time.Second,
PoolLimit: 4096,
}
session, err := mgo.DialWithInfo(mongoDBDialInfo)
if err != nil {
log.Fatalf("dial mongodb failed err:%s\n", err)
}
session.SetMode(mgo.Monotonic, true)
return &MongoDB{session: session, addr: addr, db: db}
}
func (self *MongoDB) sessionclone() *mgo.Session {
if self.session == nil {
var err error
mongoDBDialInfo := &mgo.DialInfo{
Addrs: []string{self.addr},
Timeout: 10 * time.Second,
PoolLimit: 4096,
}
self.session, err = mgo.DialWithInfo(mongoDBDialInfo)
if err != nil {
log.Fatalf("err:%s", err)
}
self.session.SetMode(mgo.Monotonic, true)
}
return self.session.Clone()
}
// PostList 读取文章列表
func (self MongoDB) PostList() []*model.Post {
s := self.sessionclone()
defer s.Close()
posts := make([]*model.Post, 0)
c := s.DB(self.db).C("index")
// 根据meta.pubtime 逆序排序并 取出20个
err := c.Find(nil).Sort("-meta.pubtime").Limit(20).All(&posts)
if err != nil {
log.Printf("get list failed from mongodb err: %s\n", err)
return nil
}
return posts
}
// PostArchive 归档
func (self MongoDB) PostArchive() []*model.YearArchive {
// 目前先从mongodb中将所有的文章都取出来 在进行处理
s := self.sessionclone()
defer s.Close()
posts := make([]*model.Post, 0)
c := s.DB(self.db).C("index")
// 根据meta.pubtime 逆序排序并 取出20个
err := c.Find(nil).Sort("-meta.pubtime").All(&posts)
if err != nil {
log.Printf("get list failed from mongodb err: %s\n", err)
return nil
}
yearArchiveMap := make(map[int]*model.YearArchive)
for _, post := range posts {
post.Content = ""
year := post.PostTime.Year()
month := int(post.PostTime.Month())
if yearArchive, ok := yearArchiveMap[year]; ok {
monthExists := false
for _, monthArchive := range yearArchive.MonthArchives {
if monthArchive.Month == month {
monthArchive.Posts = append(monthArchive.Posts, post)
monthExists = true
break
}
}
if !monthExists {
yearArchive.MonthArchives = append(yearArchive.MonthArchives, &model.MonthArchive{
Month: month,
Posts: []*model.Post{post},
})
}
} else {
monthArchive := &model.MonthArchive{
Month: month,
Posts: []*model.Post{post},
}
yearArchive = &model.YearArchive{
Year: year,
MonthArchives: []*model.MonthArchive{monthArchive},
}
yearArchiveMap[year] = yearArchive
}
}
yearArchives := make([]*model.YearArchive, 0, len(yearArchiveMap))
for _, yearArchive := range yearArchiveMap {
yearArchives = append(yearArchives, yearArchive)
}
sort.Slice(yearArchives, func(i, j int) bool {
return yearArchives[i].Year > yearArchives[j].Year
})
return yearArchives
}
// ServeMarkdown 处理Markdown
func (self MongoDB) ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string) {
//TODO
// http.ServeFile(w, r, global.App.ProjectRoot+PostDir+util.Filename(filename)+"/post.md")
}
// FindPost 根据路径查找文章
func (self MongoDB) FindPost(path string) (*model.Post, error) {
var post *model.Post
s := self.sessionclone()
defer s.Close()
c := s.DB(self.db).C("index")
err := c.Find(bson.M{"meta.path": path + ".html"}).One(&post)
if err != nil {
log.Printf("Find post failed from mongodb err:%s\n", err)
return post, err
}
post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))
return post, err
}
// TagList 标签列表
func (self MongoDB) TagList() []*model.Tag {
// 目前先从mongodb中将所有的文章都取出来 在进行处理
s := self.sessionclone()
defer s.Close()
allPosts := make([]*model.Post, 0)
c := s.DB(self.db).C("index")
// 根据meta.pubtime 逆序排序并 取出20个
err := c.Find(nil).Sort("-meta.pubtime").All(&allPosts)
if err != nil {
log.Printf("get list failed from mongodb err: %s\n", err)
return nil
}
tagMap := make(map[string][]*model.Post)
//遍历所有文章对象,分析出标签数据
for _, post := range allPosts {
post.Content = ""
for _, tag := range post.Tags {
posts, ok := tagMap[tag]
if !ok {
posts = make([]*model.Post, 0)
}
posts = append(posts, post)
tagMap[tag] = posts
}
}
//组装标签列表
tags := make([]*model.Tag, 0)
for tag, posts := range tagMap {
sort.Slice(posts, func(i, j int) bool {
return posts[i].PubTime > posts[j].PubTime
})
tags = append(tags, &model.Tag{Name: tag, Posts: posts})
}
//按文件数量倒序排序
sort.Slice(tags, func(i, j int) bool {
return len(tags[i].Posts) > len(tags[j].Posts)
})
return tags
}
// FindTag 查找标签
func (self MongoDB) FindTag(tagName string) *model.Tag {
tags := self.TagList()
for _, tag := range tags {
if tag.Name == tagName {
return tag
}
}
return nil
}
// AboutPost 关于
func (self MongoDB) AboutPost() (*model.Post, error) {
var meta = &model.Meta{}
post := &model.Post{
Content: string(""),
Meta: meta,
}
return post, nil
}
// UpdateDataSource 更新数据
func (self MongoDB) UpdateDataSource() {
}
// GetFriends 友情链接
func (self MongoDB) GetFriends() ([]*model.Friend, error) {
var friends = []*model.Friend{
{Name: "go语言中文网", Link: "https://studygolang.com"},
}
return friends, nil
}
================================================
FILE: src/datasource/mysql_repo.go
================================================
package datasource
import (
"database/sql"
"fmt"
"global"
"io/ioutil"
"log"
"model"
"net/http"
"os"
"sort"
"strconv"
"time"
"util"
_ "github.com/go-sql-driver/mysql" // data source
"github.com/pkg/errors"
"github.com/robfig/cron"
"github.com/russross/blackfriday"
"gopkg.in/yaml.v2"
)
// MysqlRepo mysql 数据源结构体
type MysqlRepo struct {
db *sql.DB
selectTag *sql.Stmt
selectArticleById *sql.Stmt
selectArticleIndex *sql.Stmt
selectArticleTagsById *sql.Stmt
selectArticleArchives *sql.Stmt
selectArticlesByTag *sql.Stmt
selectFriends *sql.Stmt
}
type articleInfo struct {
Id int64 `json:"id"`
Title string `json:"title"`
PubTime int64 `json:"pub_time"`
Content string `json:"content"`
}
type tagInfo struct {
Id int64 `json:"id"`
Name string `json:"name"`
}
type friendInfo struct {
Id int64 `json:"id"`
Name string `json:"name"`
Link string `json:"link"`
Logo string `json:"logo"`
}
// NewMysql 创建mysql数据源实例,相当于构造方法
func NewMysql(dbParams string) *MysqlRepo {
db, err := sql.Open("mysql", dbParams)
if err != nil {
log.Fatalf("Couldn't connect to database: %s", err)
}
return &MysqlRepo{
db: db,
selectTag: prepare(db, "SELECT * FROM `tag`"),
selectArticleById: prepare(db, "SELECT * FROM `article` WHERE `id`= ?"),
selectArticleIndex: prepare(db, "SELECT * FROM `article` ORDER BY `pub_time` DESC LIMIT 20"),
selectArticleTagsById: prepare(db, "SELECT t.`name` FROM `article_tag` at LEFT JOIN `tag` t ON at.`tag_id`=t.`id` WHERE `article_id`= ?"),
selectArticleArchives: prepare(db, "SELECT `id`,`title`,`pub_time` FROM `article`"),
selectArticlesByTag: prepare(db, "SELECT a.`id`,a.`title`,a.`pub_time` FROM `article` a LEFT JOIN `article_tag` at ON a.`id`=at.`article_id` WHERE at.`tag_id`=?"),
selectFriends: prepare(db, "SELECT * FROM `friend_link`"),
}
}
func prepare(db *sql.DB, sql string) *sql.Stmt {
stmt, err := db.Prepare(sql)
if err != nil {
log.Fatalf("Prepare SQL '%s' failed: %s", sql, err)
}
return stmt
}
// PostList 读取文章列表
func (self *MysqlRepo) PostList() []*model.Post {
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + IndexFile)
if err != nil {
return nil
}
posts := make([]*model.Post, 0)
err = yaml.Unmarshal(in, &posts)
if err != nil {
return nil
}
return posts
}
// PostArchive 读取归档列表
func (self *MysqlRepo) PostArchive() []*model.YearArchive {
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + ArchiveFile)
if err != nil {
return nil
}
yearArchives := make([]*model.YearArchive, 0)
err = yaml.Unmarshal(in, &yearArchives)
if err != nil {
return nil
}
return yearArchives
}
// ServeMarkdown 处理查看 Markdown 请求
func (self *MysqlRepo) ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string) {
http.ServeFile(w, r, global.App.ProjectRoot+PostDir+util.Filename(filename)+"/post.md")
}
// FindPost 根据路径查找文章
func (self *MysqlRepo) FindPost(path string) (*model.Post, error) {
id, err := strconv.Atoi(path)
if err != nil {
log.Printf("Invalid path :%s\n", err)
return nil, fmt.Errorf("文章不存在")
}
row := self.selectArticleById.QueryRow(id)
info := articleInfo{}
row.Scan(&info.Id, &info.Title, &info.PubTime, &info.Content)
post := self.genOnePost(info)
rows, err := self.selectArticleTagsById.Query(info.Id)
if err != nil {
log.Printf("Query article tags error:%s", err)
return nil, fmt.Errorf("文章不存在")
}
var tags []string
for rows.Next() {
var tagName string
err = rows.Scan(&tagName)
if err != nil {
log.Printf("Scan tag error: %s\n", err)
}
tags = append(tags, tagName)
}
post.Tags = tags
post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))
return post, err
}
// TagList 读取标签列表
func (self *MysqlRepo) TagList() []*model.Tag {
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + TagsFile)
if err != nil {
return nil
}
tags := make([]*model.Tag, 0)
err = yaml.Unmarshal(in, &tags)
if err != nil {
return nil
}
return tags
}
// FindTag 通过标签名查找标签
func (self *MysqlRepo) FindTag(tagName string) *model.Tag {
tags := self.TagList()
for _, tag := range tags {
if tag.Name == tagName {
return tag
}
}
return nil
}
// AboutPost 获取关于页
func (self *MysqlRepo) AboutPost() (*model.Post, error) {
// 从 about.md 中读取关于内容
postDir := global.App.ProjectRoot + PostDir
markdown, err := ioutil.ReadFile(postDir + "/about.md")
if err != nil {
return nil, errors.Wrap(err, "read about.md error")
}
// 关于页不需要 meta.yml
var meta = &model.Meta{}
post := &model.Post{
Content: string(markdown),
Meta: meta,
}
return post, nil
}
// GenIndexYaml 生成首页数据文件index.yaml
func (self *MysqlRepo) GenIndexYaml() {
// 首页最多显示20篇文章
var posts []*model.Post
rows, err := self.selectArticleIndex.Query()
if err != nil {
log.Fatalf("query index error:%s", err)
}
for rows.Next() {
info := articleInfo{}
err = rows.Scan(&info.Id, &info.Title, &info.PubTime, &info.Content)
if err != nil {
log.Println("scan error", err)
}
// post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))
posts = append(posts, self.genOnePost(info))
}
buf, err := yaml.Marshal(posts)
if err != nil {
log.Printf("gen index yaml error: %v\n", err)
return
}
indexYaml := global.App.ProjectRoot + PostDir + IndexFile
ioutil.WriteFile(indexYaml, buf, 0777)
}
func (self *MysqlRepo) parsePubTime(pubTime int64) string {
t := time.Unix(pubTime, 0).In(time.Local)
return t.Format("2006-01-02 15:04:05")
}
// GenArchiveYaml 生成归档数据文件archive.yaml
func (self *MysqlRepo) GenArchiveYaml() {
var posts []*model.Post
rows, err := self.selectArticleArchives.Query()
for rows.Next() {
info := articleInfo{}
err = rows.Scan(&info.Id, &info.Title, &info.PubTime)
if err != nil {
log.Println("query error", err)
}
posts = append(posts, self.genOnePost(info))
}
yearArchiveMap := make(map[int]*model.YearArchive)
for _, post := range posts {
year := post.PostTime.Year()
month := int(post.PostTime.Month())
if yearArchive, ok := yearArchiveMap[year]; ok {
monthExists := false
for _, monthArchive := range yearArchive.MonthArchives {
if monthArchive.Month == month {
monthArchive.Posts = append(monthArchive.Posts, post)
monthExists = true
break
}
}
if !monthExists {
yearArchive.MonthArchives = append(yearArchive.MonthArchives, &model.MonthArchive{
Month: month,
Posts: []*model.Post{post},
})
}
} else {
monthArchive := &model.MonthArchive{
Month: month,
Posts: []*model.Post{post},
}
yearArchive = &model.YearArchive{
Year: year,
MonthArchives: []*model.MonthArchive{monthArchive},
}
yearArchiveMap[year] = yearArchive
}
}
yearArchives := make([]*model.YearArchive, 0, len(yearArchiveMap))
for _, yearArchive := range yearArchiveMap {
yearArchives = append(yearArchives, yearArchive)
}
sort.Slice(yearArchives, func(i, j int) bool {
return yearArchives[i].Year > yearArchives[j].Year
})
buf, err := yaml.Marshal(yearArchives)
if err != nil {
log.Printf("gen archives yaml error:%v\n", err)
return
}
archiveYaml := global.App.ProjectRoot + PostDir + ArchiveFile
ioutil.WriteFile(archiveYaml, buf, 0777)
}
// GenTagsYaml 生成标签数据文件tags.yaml
func (self *MysqlRepo) GenTagsYaml() {
tagMap := make(map[string][]*model.Post)
tagRows, err := self.selectTag.Query()
if err != nil {
log.Fatalf("query tag error:%s", err)
}
for tagRows.Next() {
info := tagInfo{}
err = tagRows.Scan(&info.Id, &info.Name)
if err != nil {
log.Println("scan error", err)
}
articleRows, err := self.selectArticlesByTag.Query(info.Id)
if err != nil {
log.Fatalf("query tag articles error:%s", err)
}
for articleRows.Next() {
article := articleInfo{}
err = articleRows.Scan(&article.Id, &article.Title, &article.PubTime)
if err != nil {
log.Println("query error", err)
}
tagMap[info.Name] = append(tagMap[info.Name], self.genOnePost(article))
}
}
// 组装标签列表
tags := make([]*model.Tag, 0)
for tag, posts := range tagMap {
sort.Slice(posts, func(i, j int) bool {
return posts[i].PubTime > posts[j].PubTime
})
tags = append(tags, &model.Tag{Name: tag, Posts: posts})
}
// 按文件数量倒序排序
sort.Slice(tags, func(i, j int) bool {
return len(tags[i].Posts) > len(tags[j].Posts)
})
buf, err := yaml.Marshal(tags)
if err != nil {
log.Printf("gen tags yaml error:%v\n", err)
return
}
tagsYaml := global.App.ProjectRoot + PostDir + TagsFile
ioutil.WriteFile(tagsYaml, buf, 0777)
}
// genOnePost 组装一个post
func (self *MysqlRepo) genOnePost(info articleInfo) *model.Post {
return &model.Post{
Content: info.Content,
Meta: &model.Meta{
Title: info.Title,
Path: fmt.Sprintf("%d.html", info.Id),
PubTime: self.parsePubTime(info.PubTime),
PostTime: time.Unix(info.PubTime, 0).In(time.Local),
},
}
}
// GenFriendsYaml 生成友情链接数据文件friends.yaml
func (self *MysqlRepo) GenFriendsYaml() {
rows, err := self.selectFriends.Query()
if err != nil {
log.Fatalf("query friend error:%s", err)
}
var friends []*model.Friend
for rows.Next() {
info := friendInfo{}
err = rows.Scan(&info.Id, &info.Name, &info.Link, &info.Logo)
if err != nil {
log.Println("scan error", err)
}
// post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))
friends = append(friends, &model.Friend{Name: info.Name, Link: info.Link, Logo: info.Logo})
}
buf, err := yaml.Marshal(friends)
if err != nil {
log.Printf("gen friends yaml error:%v\n", err)
return
}
friendsYaml := global.App.ProjectRoot + PostDir + FriendFile
ioutil.WriteFile(friendsYaml, buf, 0777)
}
// UpdateDataSource 更新mysql数据
func (self *MysqlRepo) UpdateDataSource() {
// 检查文章目录(data/post/)是否存在,不存在则连接mysql生成
mysqlRepoDir := global.App.ProjectRoot + PostDir
if !util.Exist(mysqlRepoDir) {
if err := os.MkdirAll(mysqlRepoDir, os.ModePerm); err != nil {
panic(err)
}
}
// 解析仓库文件,生成首页、归档、标签数据
self.GenIndexYaml()
self.GenArchiveYaml()
self.GenTagsYaml()
self.GenFriendsYaml()
// 定时每天自动更新仓库,并生成首页、归档、标签数据
c := cron.New()
c.AddFunc("@daily", func() {
self.GenIndexYaml()
self.GenArchiveYaml()
self.GenTagsYaml()
self.GenFriendsYaml()
})
c.Start()
}
// GetFriends 友情链接
func (self *MysqlRepo) GetFriends() ([]*model.Friend, error) {
// 从friends.yaml 中读取友情链接内容
in, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + FriendFile)
if err != nil {
return nil, errors.Wrap(err, "read friends.yaml error")
}
friends := make([]*model.Friend, 0)
err = yaml.Unmarshal(in, &friends)
if err != nil {
return nil, errors.Wrap(err, "Unmarshal friends.yaml error")
}
return friends, nil
}
================================================
FILE: src/datasource/mysql_repo_test.go
================================================
package datasource_test
import (
"datasource"
"global"
"os"
"strings"
"testing"
)
var DefaultMysql *datasource.MysqlRepo
func Init() {
cwd, _ := os.Getwd()
pos := strings.LastIndex(cwd, "src")
global.App.ProjectRoot = cwd[:pos]
DefaultMysql = datasource.NewMysql("dreamgo:123456@tcp(127.0.0.1:3306)/dreamgo")
}
func TestGenMysqlIndexYaml(t *testing.T) {
Init()
DefaultMysql.GenIndexYaml()
}
func TestGenMysqlArchiveYaml(t *testing.T) {
Init()
DefaultMysql.GenArchiveYaml()
}
func TestGenMysqlTagsYaml(t *testing.T) {
Init()
DefaultMysql.GenTagsYaml()
}
================================================
FILE: src/dreamgo/main.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package main
import (
"config"
"datasource"
"flag"
"global"
"http/controller"
"log"
"logger"
"math/rand"
"net/http"
"route"
"strings"
"time"
)
var configFile string
func init() {
rand.Seed(time.Now().Unix())
flag.StringVar(&configFile, "config", "config/env.yml", "The config file. Default is $ProjectRoot/config/env.yml")
}
func main() {
// 日志
logger := logger.Init("dreamgo")
logger.Info("main ... ")
// 解析命令行参数
flag.Parse()
// 初始化程序路径
global.App.InitPath()
if strings.HasPrefix(configFile, "/") { //以/开头为绝对路径,直接解析
config.Parse(configFile)
} else { // 相对路径,以程序根目录为基础解析
config.Parse(global.App.ProjectRoot + configFile)
}
datasource.Init()
// 设置模板目录,默认为default
global.App.SetTemplateDir(config.YamlConfig.MustValue("theme", "default"))
// 从配置文件中获取监听IP和端口
global.App.Host = config.YamlConfig.Get("listen.host").String()
global.App.Port = config.YamlConfig.Get("listen.port").String()
addr := global.App.Host + ":" + global.App.Port
// 注册路由
controller.RegisterRoutes()
// 启动监听,使用封装的 route.DefaultBlogMux 处理http请求
log.Fatal(http.ListenAndServe(addr, route.DefaultBlogMux))
}
================================================
FILE: src/global/app.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
// global 全局信息
package global
import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
)
// Build 构建信息,从 git 仓库获取
var Build string
type app struct {
Name string
Build string
Version string
BuildDate time.Time
ProjectRoot string
TemplateDir string
Copyright string
LaunchTime time.Time
Host string
Port string
locker sync.Mutex
}
// App is the App Info
var App = &app{}
var showVersion = flag.Bool("version", false, "Print version of this binary")
func init() {
App.Name = os.Args[0]
App.Version = "V1.0.0"
App.Build = Build
App.LaunchTime = time.Now()
// 查找可执行程序的路径
binaryPath, err := exec.LookPath(os.Args[0])
if err != nil {
panic(err)
}
// 获取可执行程序的绝对路径
binaryPath, err = filepath.Abs(binaryPath)
if err != nil {
panic(err)
}
// 获取可执行程序的文件信息
fileInfo, err := os.Stat(binaryPath)
if err != nil {
panic(err)
}
// 构建时间为可执行程序的修改时间
App.BuildDate = fileInfo.ModTime()
App.Copyright = fmt.Sprintf("%d", time.Now().Year())
}
// InitPath 初始化相关路径,包括项目根目录、模板目录
func (this *app) InitPath() {
App.setProjectRoot()
App.SetTemplateDir("default")
}
// Uptime calculates the duration of lauching
func (this *app) Uptime() time.Duration {
this.locker.Lock()
defer this.locker.Unlock()
return time.Now().Sub(this.LaunchTime)
}
func (this *app) setProjectRoot() {
curFilename := os.Args[0]
binaryPath, err := exec.LookPath(curFilename)
if err != nil {
panic(err)
}
binaryPath, err = filepath.Abs(binaryPath)
if err != nil {
panic(err)
}
projectRoot := filepath.Dir(filepath.Dir(binaryPath))
this.ProjectRoot = projectRoot + "/"
}
// SetTemplateDir 设置模板目录
func (this *app) SetTemplateDir(theme string) {
this.TemplateDir = this.ProjectRoot + "template/theme/" + theme + "/"
}
// PrintVersion prints current version info
func PrintVersion(w io.Writer) {
if !flag.Parsed() {
flag.Parse()
}
if showVersion == nil || !*showVersion {
return
}
fmt.Fprintf(w, "Binary: %s\n", App.Name)
fmt.Fprintf(w, "Version: %s\n", App.Version)
fmt.Fprintf(w, "Build: %s\n", App.Build)
fmt.Fprintf(w, "Compile date: %s\n", App.BuildDate.Format("2006-01-02 15:04:05"))
os.Exit(0)
}
================================================
FILE: src/http/controller/about.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: tk103331 tk103331@gmail.com
package controller
import (
"datasource"
"logger"
"net/http"
"route"
"view"
)
type AboutController struct{}
func (self AboutController) RegisterRoutes() {
route.HandleFunc("/about", self.Detail)
}
func (AboutController) Detail(w http.ResponseWriter, r *http.Request) {
about, err := datasource.DefaultDataSourcer.AboutPost()
if err == nil {
view.Render(w, r, "about.html", map[string]interface{}{"about": about})
} else {
logger.Instance().Error("get about.md error " + err.Error())
http.NotFound(w, r)
}
}
================================================
FILE: src/http/controller/archive.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package controller
import (
"datasource"
"net/http"
"route"
"view"
)
type ArchiveController struct{}
// RegisterRoute 注册路由
func (self ArchiveController) RegisterRoute() {
route.HandleFunc("/archives", self.List)
}
// List 处理归档列表请求
func (ArchiveController) List(w http.ResponseWriter, r *http.Request) {
// 从数据源查询归档列表
yearArchives := datasource.DefaultDataSourcer.PostArchive()
// 渲染模板archives.html,并传入数据
view.Render(w, r, "archives.html", map[string]interface{}{"archives": yearArchives})
}
================================================
FILE: src/http/controller/friends.go
================================================
package controller
import (
"datasource"
"logger"
"net/http"
"route"
"view"
)
type FriendsController struct{}
func (self FriendsController) RegisterRoutes() {
route.HandleFunc("/friends", self.Detail)
}
func (FriendsController) Detail(w http.ResponseWriter, r *http.Request) {
friends, err := datasource.DefaultDataSourcer.GetFriends()
if err == nil {
view.Render(w, r, "friends.html", map[string]interface{}{"friends": friends})
} else {
logger.Instance().Error("get friends.yaml error " + err.Error())
http.NotFound(w, r)
}
}
================================================
FILE: src/http/controller/index.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package controller
import (
"datasource"
"net/http"
"route"
"view"
)
var defaults = map[string]bool{
"/": true,
"/index.html": true,
"/index.htm": true,
}
// IndexController 首页 controller
type IndexController struct{}
// RegisterRoute 注册路由
func (self IndexController) RegisterRoute() {
route.HandleFunc("/", self.Home)
}
// Home 首页
func (IndexController) Home(w http.ResponseWriter, r *http.Request) {
if _, ok := defaults[r.RequestURI]; !ok {
http.NotFound(w, r)
return
}
posts := datasource.DefaultDataSourcer.PostList()
view.Render(w, r, "index.html", map[string]interface{}{"posts": posts})
}
================================================
FILE: src/http/controller/post.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package controller
import (
"net/http"
"path/filepath"
"route"
"strings"
"datasource"
"util"
"view"
)
type PostController struct{}
// RegisterRoute register route
func (self PostController) RegisterRoute() {
route.HandleFunc("/post/", self.Detail)
}
// Detail 处理文件详情请求
func (PostController) Detail(w http.ResponseWriter, r *http.Request) {
// 获取文章文件名,即文章的路径
filename := filepath.Base(r.RequestURI)
if strings.HasSuffix(filename, ".md") {
// 处理markdown
datasource.DefaultDataSourcer.ServeMarkdown(w, r, filename)
} else if strings.HasSuffix(filename, ".html") {
// 根据路径查找文件
post, err := datasource.DefaultDataSourcer.FindPost(util.Filename(filename))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 渲染模板single.html,并传入数据
view.Render(w, r, "single.html", map[string]interface{}{
"post": post,
})
} else {
// 返回404
http.NotFound(w, r)
}
}
================================================
FILE: src/http/controller/routes.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package controller
// RegisterRoutes 注册路由
func RegisterRoutes() {
new(PostController).RegisterRoute() // 注册文章相关路由
new(ArchiveController).RegisterRoute() // 注册归档相关路由
new(IndexController).RegisterRoute() // 注册首页相关路由
new(TagController).RegisterRoute() // 注册标签相关路由
new(AboutController).RegisterRoutes() // 注册关于页面路由
new(StaticController).RegisterRoutes() // 注册静态文件路由
new(FriendsController).RegisterRoutes() // 注册友联相关路由
}
================================================
FILE: src/http/controller/static.go
================================================
package controller
import (
"global"
"net/http"
"route"
"strings"
)
// 静态文件控制器
type StaticController struct{}
func (self StaticController) RegisterRoutes() {
route.HandleFunc("/static/", self.Default)
}
// Default 以/static/开头的URL为静态文件,使用 http.FileServer 直接处理
func (StaticController) Default(w http.ResponseWriter, r *http.Request) {
reqURI := r.RequestURI
//以/结尾的URL,直接返回404
if strings.HasSuffix(reqURI, "/") {
http.NotFound(w, r)
} else {
fileHandler := http.StripPrefix("/static/", http.FileServer(http.Dir(global.App.ProjectRoot+"/static")))
fileHandler.ServeHTTP(w, r)
}
}
================================================
FILE: src/http/controller/tag.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package controller
import (
"datasource"
"net/http"
"net/url"
"path/filepath"
"route"
"sort"
"view"
)
type TagController struct{}
// RegisterRoute 注册路由
func (self TagController) RegisterRoute() {
route.HandleFunc("/tag/", self.Detail)
route.HandleFunc("/tags", self.List)
}
// Detail 处理标签详情请求
func (TagController) Detail(w http.ResponseWriter, r *http.Request) {
// 从URL中获取标签名
reqUrl, _ := url.ParseRequestURI(r.RequestURI)
tagName := filepath.Base(reqUrl.Path)
// 根据标签名查询标签
tag := datasource.DefaultDataSourcer.FindTag(tagName)
if tag != nil {
// 渲染模板tag.html,并传入数据
view.Render(w, r, "tag.html", map[string]interface{}{"tag": tag})
} else {
// 返回404
http.NotFound(w, r)
}
}
// List 处理标签列表请求
func (TagController) List(w http.ResponseWriter, r *http.Request) {
// 从数据源获取标签列表
tags := datasource.DefaultDataSourcer.TagList()
// 按文章数量倒序排序
sort.Slice(tags, func(i, j int) bool {
return len(tags[i].Posts) > len(tags[j].Posts)
})
// 渲染模板tags.html,并传入数据
view.Render(w, r, "tags.html", map[string]interface{}{"tags": tags})
}
================================================
FILE: src/logger/log.go
================================================
package logger
import (
"bytes"
"log"
"os"
"path"
"time"
"global"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var instance *zap.Logger
// Instance 唯一实例
func Instance() *zap.Logger {
return instance
}
// Init作用初始化,srvName 生成的日志文件夹名字
func Init(srvName string) *zap.Logger {
instance = NewLogger(srvName)
return instance
}
// NewLogger 新建日志
func NewLogger(srvName string) *zap.Logger {
directory := global.App.ProjectRoot
if len(directory) == 0 {
directory = path.Join("", "log", srvName)
} else {
directory = path.Join(directory, "log", srvName)
}
writers := []zapcore.WriteSyncer{newRollingFile(directory)}
writers = append(writers, os.Stdout)
logger, _ := newZapLogger(true, zapcore.NewMultiWriteSyncer(writers...))
zap.RedirectStdLog(logger)
/*updateLogLevel( serviceName, dyn, isProduction)
go func() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
updateLogLevel( serviceName, dyn, isProduction)
}
}()*/
return logger
}
/*func updateLogLevel(serviceName string, dyn *zap.AtomicLevel, isProduction bool) {
originLevelString := "info"
if !isProduction {
originLevelString = "debug"
}
levelConf := make(map[string]map[string]string)
newLevelString, ok := levelConf[serviceName]["127.0.0.1"]
if !ok {
newLevelString, ok = levelConf[serviceName]["*"]
if !ok {
newLevelString = originLevelString
}
}
if !ok {
newLevelString, ok = levelConf[serviceName]["*"]
if !ok {
newLevelString = originLevelString
}
}
newLevel := new(zapcore.Level)
if err := newLevel.Set(newLevelString); err != nil {
newLevel.Set(originLevelString)
}
if dyn.Level() != *newLevel {
log.Println("修改日志等级: ", dyn.Level().String(), "=>", newLevel.String())
dyn.SetLevel(*newLevel)
}
}*/
func newRollingFile(directory string) zapcore.WriteSyncer {
if err := os.MkdirAll(directory, 0766); err != nil {
log.Println("failed create log directory:", directory, ":", err)
return nil
}
return newLumberjackWriteSyncer(&lumberjack.Logger{
Filename: path.Join(directory, "output.log"),
MaxSize: 100, //megabytes
MaxAge: 7, //days
LocalTime: true,
Compress: false,
})
}
func newZapLogger(isProduction bool, output zapcore.WriteSyncer) (*zap.Logger, *zap.AtomicLevel) {
encCfg := zapcore.EncoderConfig{
TimeKey: "@timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeDuration: zapcore.NanosDurationEncoder,
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
},
}
var encoder zapcore.Encoder
dyn := zap.NewAtomicLevel()
if isProduction {
dyn.SetLevel(zap.InfoLevel)
encCfg.EncodeLevel = zapcore.LowercaseLevelEncoder
encoder = zapcore.NewConsoleEncoder(encCfg) // zapcore.NewJSONEncoder(encCfg)
} else {
dyn.SetLevel(zap.DebugLevel)
encCfg.EncodeLevel = zapcore.LowercaseColorLevelEncoder
encoder = zapcore.NewConsoleEncoder(encCfg)
}
return zap.New(zapcore.NewCore(encoder, output, dyn), zap.AddCaller()), &dyn
}
type lumberjackWriteSyncer struct {
*lumberjack.Logger
buf *bytes.Buffer
logChan chan []byte
closeChan chan interface{}
maxSize int
}
func newLumberjackWriteSyncer(l *lumberjack.Logger) *lumberjackWriteSyncer {
ws := &lumberjackWriteSyncer{
Logger: l,
buf: bytes.NewBuffer([]byte{}),
logChan: make(chan []byte, 5000),
closeChan: make(chan interface{}),
maxSize: 1024,
}
go ws.run()
return ws
}
func (l *lumberjackWriteSyncer) run() {
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
if l.buf.Len() > 0 {
l.sync()
}
case bs := <-l.logChan:
_, err := l.buf.Write(bs)
if err != nil {
continue
}
if l.buf.Len() > l.maxSize {
l.sync()
}
case <-l.closeChan:
l.sync()
return
}
}
}
func (l *lumberjackWriteSyncer) Stop() {
close(l.closeChan)
}
func (l *lumberjackWriteSyncer) Write(bs []byte) (int, error) {
b := make([]byte, len(bs))
for i, c := range bs {
b[i] = c
}
l.logChan <- b
return 0, nil
}
func (l *lumberjackWriteSyncer) Sync() error {
return nil
}
func (l *lumberjackWriteSyncer) sync() error {
defer l.buf.Reset()
_, err := l.Logger.Write(l.buf.Bytes())
if err != nil {
return err
}
return nil
}
================================================
FILE: src/model/archive.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package model
// YearArchive 归档
type YearArchive struct {
Year int `yaml:"year"`
MonthArchives []*MonthArchive `yaml:"month_archive"`
}
type MonthArchive struct {
Month int `yaml:"month"`
Posts []*Post `yaml:"posts"`
}
================================================
FILE: src/model/friend.go
================================================
package model
type Friend struct {
Name string `yaml:"name"`
Link string `yaml:"link"`
Logo string `yaml:"logo"`
}
================================================
FILE: src/model/post.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package model
import "time"
// 文章
type Post struct {
Content string `yaml:"content"`
*Meta
}
type Meta struct {
Title string `yaml:"title"`
Path string `yaml:"path"`
PubTime string `yaml:"pub_time"`
Tags []string `yaml:"tags"`
PostTime time.Time `yaml:"post_time"`
}
================================================
FILE: src/model/tag.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: tk103331 tk103331@gmail.com
package model
// 标签
type Tag struct {
Name string `yaml:"name"`
Posts []*Post `yaml:"posts"`
}
================================================
FILE: src/route/mux.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package route
import (
"context"
"net/http"
"time"
)
func HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
DefaultBlogMux.HandleFunc(pattern, handler)
}
// BlogMux 路由处理器,扩展http.ServeMux
type BlogMux struct {
*http.ServeMux
}
// DefaultBlogMux 默认路由处理器
var DefaultBlogMux = NewBlogMux()
func NewBlogMux() *BlogMux {
return &BlogMux{ServeMux: http.DefaultServeMux}
}
// ServeHTTP 路由分发方法,封装 http.DefaultServeMux.ServeHTTP()
func (this *BlogMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 创建上下文,并写入start_time
ctx := context.WithValue(r.Context(), "start_time", time.Now())
// 使用上下文
r = r.WithContext(ctx)
// 调用http.DefaultServeMux的路由分发方法
this.ServeMux.ServeHTTP(w, r)
}
================================================
FILE: src/util/file.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package util
import (
"os"
"strings"
)
// Exist 检查文件或目录是否存在
// 如果由 filename 指定的文件或目录存在则返回 true,否则返回 false
func Exist(filename string) bool {
_, err := os.Stat(filename)
return err == nil || os.IsExist(err)
}
// ScanDir 列出指定路径中的文件和目录
// 如果目录不存在,则返回空 slice
func ScanDir(directory string) []string {
file, err := os.Open(directory)
if err != nil {
return []string{}
}
names, err := file.Readdirnames(-1)
if err != nil {
return []string{}
}
return names
}
// IsDir 判断给定文件名是否是一个目录
// 如果文件名存在并且为目录则返回 true。如果 filename 是一个相对路径,则按照当前工作目录检查其相对路径。
func IsDir(filename string) bool {
return isFileOrDir(filename, true)
}
// IsFile 判断给定文件名是否为一个正常的文件
// 如果文件存在且为正常的文件则返回 true
func IsFile(filename string) bool {
return isFileOrDir(filename, false)
}
// Filename returns the filename except ext
func Filename(file string) string {
if file == "" {
return ""
}
pos := strings.LastIndex(file, ".")
return file[:pos]
}
// 判断是文件还是目录,根据decideDir为true表示判断是否为目录;否则判断是否为文件
func isFileOrDir(filename string, decideDir bool) bool {
fileInfo, err := os.Stat(filename)
if err != nil {
return false
}
isDir := fileInfo.IsDir()
if decideDir {
return isDir
}
return !isDir
}
================================================
FILE: src/util/util.go
================================================
package util
import (
"errors"
"reflect"
)
// Contain 判断obj是否在target中,target支持的类型arrary,slice,map
func Contain(obj interface{}, target interface{}) (bool, error) {
targetValue := reflect.ValueOf(target)
switch reflect.TypeOf(target).Kind() {
case reflect.Slice, reflect.Array:
for i := 0; i < targetValue.Len(); i++ {
if targetValue.Index(i).Interface() == obj {
return true, nil
}
}
case reflect.Map:
if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {
return true, nil
}
}
return false, errors.New("not in array")
}
================================================
FILE: src/vendor/manifest
================================================
{
"version": 0,
"dependencies": [
{
"importpath": "github.com/PuerkitoBio/goquery",
"repository": "https://github.com/PuerkitoBio/goquery",
"revision": "1a71cc719d0d5b9e4f14ae072bef08b576b0bab5",
"branch": "master"
},
{
"importpath": "github.com/andybalholm/cascadia",
"repository": "https://github.com/andybalholm/cascadia",
"revision": "349dd0209470eabd9514242c688c403c0926d266",
"branch": "master"
},
{
"importpath": "github.com/go-chinese-site/cfg",
"repository": "https://github.com/go-chinese-site/cfg",
"revision": "844c2049bca3b3f99bae93339d7c5f417ed0f3ef",
"branch": "master"
},
{
"importpath": "github.com/go-sql-driver/mysql",
"repository": "https://github.com/go-sql-driver/mysql",
"revision": "fade21009797158e7b79e04c340118a9220c6f9e",
"branch": "master"
},
{
"importpath": "github.com/pkg/errors",
"repository": "https://github.com/pkg/errors",
"revision": "f15c970de5b76fac0b59abb32d62c17cc7bed265",
"branch": "master"
},
{
"importpath": "github.com/robfig/cron",
"repository": "https://github.com/robfig/cron",
"revision": "736158dc09e10f1911ca3a1e1b01f11b566ce5db",
"branch": "master"
},
{
"importpath": "github.com/russross/blackfriday",
"repository": "https://github.com/russross/blackfriday",
"revision": "6d1ef893fcb01b4f50cb6e57ed7df3e2e627b6b2",
"branch": "master"
},
{
"importpath": "github.com/sourcegraph/annotate",
"repository": "https://github.com/sourcegraph/annotate",
"revision": "f4cad6c6324d3f584e1743d8b3e0e017a5f3a636",
"branch": "master"
},
{
"importpath": "github.com/sourcegraph/syntaxhighlight",
"repository": "https://github.com/sourcegraph/syntaxhighlight",
"revision": "bd320f5d308e1a3c4314c678d8227a0d72574ae7",
"branch": "master"
},
{
"importpath": "go.uber.org/atomic",
"repository": "https://github.com/uber-go/atomic",
"revision": "54f72d32435d760d5604f17a82e2435b28dc4ba5",
"branch": "master"
},
{
"importpath": "go.uber.org/multierr",
"repository": "https://github.com/uber-go/multierr",
"revision": "fb7d312c2c04c34f0ad621048bbb953b168f9ff6",
"branch": "master"
},
{
"importpath": "go.uber.org/zap",
"repository": "https://github.com/uber-go/zap",
"revision": "35aad584952c3e7020db7b839f6b102de6271f89",
"branch": "master"
},
{
"importpath": "golang.org/x/net/html",
"repository": "https://github.com/golang/net",
"revision": "0a9397675ba34b2845f758fe3cd68828369c6517",
"branch": "master",
"path": "/html"
},
{
"importpath": "gopkg.in/mgo.v2",
"repository": "https://gopkg.in/mgo.v2",
"revision": "3f83fa5005286a7fe593b055f0d7771a7dce4655",
"branch": "v2"
},
{
"importpath": "gopkg.in/natefinch/lumberjack.v2",
"repository": "https://gopkg.in/natefinch/lumberjack.v2",
"revision": "a96e63847dc3c67d17befa69c303767e2f84e54f",
"branch": "master"
},
{
"importpath": "gopkg.in/yaml.v2",
"repository": "https://gopkg.in/yaml.v2",
"revision": "eb3733d160e74a9c7e442f435eb3bea458e1d19f",
"branch": "v2"
}
]
}
================================================
FILE: src/view/template.go
================================================
// Copyright 2017 The StudyGolang Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// https://studygolang.com
// Author: polaris polaris@studygolang.com
package view
import (
"config"
"html/template"
"net/http"
"time"
"global"
)
// funcMap is the customize template functions
var funcMap = template.FuncMap{
"noescape": func(s string) template.HTML {
return template.HTML(s)
},
"formatTime": func(t time.Time, layout string) string {
return t.Format(layout)
},
}
// Render 渲染模板并输出
func Render(w http.ResponseWriter, r *http.Request, htmlFile string, data map[string]interface{}) {
if data == nil {
data = make(map[string]interface{})
}
data["app"] = global.App
data["site_name"] = config.YamlConfig.Get("setting.site_name").String()
data["title"] = config.YamlConfig.Get("setting.title").String()
data["subtitle"] = config.YamlConfig.Get("setting.subtitle").String()
// 加载布局模板layout.html
tpl, err := template.New("layout.html").Funcs(funcMap).
ParseFiles(global.App.TemplateDir+"layout.html", global.App.TemplateDir+htmlFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 加载seo关键词和描述
if seoTpl := tpl.Lookup("seo"); seoTpl == nil {
seoKeywords := config.YamlConfig.Get("seo.keywords").String()
seoDescription := config.YamlConfig.Get("seo.description").String()
tpl.Parse(`{{define "seo"}}
<meta name="keywords" content="` + seoKeywords + `">
<meta name="description" content="` + seoDescription + `">
{{end}}`)
}
startTime := r.Context().Value("start_time").(time.Time)
data["response_time"] = time.Since(startTime)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
// 渲染模板,并输出到w
err = tpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
================================================
FILE: static/css/main.css
================================================
*{margin:0;padding:0}
html,body{height:100%}
body{background:#ddd;color:#666;font-size:14px;font-family:"-apple-system","Open Sans","HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,sans-serif}
::selection,::-moz-selection,::-webkit-selection{background-color:#2479CC;color:#eee}
h1{font-size:2em}
h3{font-size:1.3em}
h4{font-size:1.1em}
a:link, a:visited, a:active {color: #076dd0; text-decoration: none; }
a:hover {text-decoration: underline; }
article{padding:30px 0;position:relative;overflow: hidden;}
.container{max-width:1600px;min-height:100%;position:relative}
.global-tips{display:none}
.left-col{background-color:#007b8b;background-image:url(/static/image/left-bg.png);background-size:cover;height:100%;position:fixed;width:270px}
.mid-col{background:#fff;left:0;margin-left:270px;min-height:100%;position:absolute;right:0}
article .post-footer{color:#555;float:right;font-size:.8em;line-height:2;position:relative;text-align:right;width:auto}
article .entry-content{font-size:16px;font-family:Arial,'Hiragino Sans GB',冬青黑,'Microsoft YaHei',微软雅黑,SimSun,宋体,Helvetica,Tahoma,'Arial sans-serif';-webkit-font-smoothing:antialiased;line-height:1.8;word-wrap:break-word}
article .entry-content{color:#444;border-bottom:1px solid #ddd;}
article h1.title{color:#333;font-size:2em;font-weight:300;line-height:35px;margin-bottom:25px}article .entry-content p{margin-top:15px}
article h1.title a{color:#333;transition:color .3s}
.mid-col .mid-col-container{padding:0 70px 0 40px;}
article .meta .date,article .meta .comment,article .meta .tags{position:relative}
article .entry-content a:hover{text-decoration:underline}
article h1.title a:hover{color:#076dd0}
#header{border-bottom:none;height:auto;line-height:30px;margin-left:50px;padding:30px 0;width:100%}
#main-nav{margin-left:0}
#main-nav,#sub-nav{float:none;margin-top:15px}#sub-nav{position:relative}
#content{margin:0 auto;width:100%}
#footer{border-top:1px solid #ddd;font-size:.9em;line-height:2.2;padding:15px 70px 15px 40px;text-align:center;width:auto}
#header a{color:#fff;text-shadow:0 1px #666;transition:color .3s}
#header h1{float:none;font-weight:300;font-size:30px}
#main-nav ul li{display:block;margin-left:0;position:relative}
#header .subtitle{color:#fff}
#sub-nav .social{margin-bottom:10px}
#footer .beian{color:#666}
#header a:hover{color:#ccc}
#header .profilepic a{background-image:url("/static/image/avatar.jpg");background-size:100% 100%;border-radius:50%;display:block;height:160px;margin:15px 0 20px -10px;width:160px}
#sub-nav .social a{background-size:20px 20px;background-position:center center;background-repeat:no-repeat;border-radius:50%;display:inline-block;height:28px;margin:0 6px 15px;opacity:.75;transition:opacity .3s;vertical-align:middle;width:28px}
#sub-nav .social a:hover{opacity:1;}
#sub-nav .social a:first-of-type{margin-left:0}
#sub-nav .social a:last-of-type{margin-right:0}
@media screen and (max-width:1024px){
article{padding-bottom:15px}
.left-col{width:210px}
.mid-col{margin-left:210px}
.mid-col .mid-col-container{padding:0 20px}
#header{margin-left:30px}
#footer{padding:15px 20px}
article h1.title,article .entry-content{margin-left:0}
}
@media screen and (max-width:640px){
#header{margin-left:0;padding:20px 0;text-align:center}
#main-nav{margin-top:10px}
#main-nav ul li{display:inline;margin:0 10px;text-align:center}
#header .profilepic a{height:56px;left:12px;margin:0;position:absolute;top:12px;width:56px}
#sub-nav .social,#sub-nav .social a{margin-bottom:0}
article{padding:20px 0}
.left-col{background-image:none;position:relative;width:100%}
.mid-col{float:none;margin-left:0;width:100%}
article .meta{margin-bottom:10px;position:static;width:auto}
.mid-col .mid-col-container{padding:0 10px}
.mid-col article .meta{float:none;overflow:hidden}
article .meta .date,article .meta .comment,article .meta .tags{display:inline;margin-right:5px;padding-left:0}
article .meta .date{margin-right:30px}#footer{padding:15px 10px}
#sub-nav .social a{opacity:1}
#content #toc-container,#content #toc{float:none}
#content #toc{margin:0;max-width:100%}
}
================================================
FILE: static/css/post.css
================================================
article input.runcode,article button{-webkit-appearance:none;background:#12b0e6;border:none;border-radius:0;box-shadow:inset 0 -5px 20px rgba(0,0,0,.1);color:#fff;cursor:pointer;font-size:14px;line-height:1;margin-top:10px;padding:0.625em .5em}
article button{margin-top:0}
article input.runcode:hover,article input.runcode:focus,article input.runcode:active,article button:hover,article button:focus,article button:active{background:#f6ad08}
article strong{font-weight:700}
article em{font-style:italic}
article blockquote{background-color:#f8f8f8;border-left:5px solid #2479CC;margin-top:10px;overflow:hidden;padding:15px 20px}
article code{background-color:#eee;border-radius:5px;font-family:Consolas,Monaco,'Andale Mono',monospace;font-size:80%;margin:0 2px;padding:4px 5px;vertical-align:middle}
article pre{background-color:#f8f8f8;border-left:5px solid #ccc;color:#5d6a6a;font-size:14px;line-height:1.6;overflow:hidden;padding:0.6em;position:relative;white-space:pre-wrap;word-break:break-word;word-wrap:break-word}
article img{border:1px solid #ccc;display:block;margin:10px 0 5px;max-width:100%;padding:0}
article table{border:0;border-collapse:collapse;border-spacing:0}
article pre code{background-color:transparent;border-radius:0 0 0 0;border:0;display:block;font-size:100%;margin:0;padding:0;position:relative}
article table th,article table td{border:0}
article table th{border-bottom:2px solid #848484;padding:6px 20px;text-align:left}
article table td{border-bottom:1px solid #d0d0d0;padding:6px 20px}
article .copyright-info{font-size:.8em}
article .expire-tips{background-color:#ffffc0;border:1px solid #e2e2e2;border-left:5px solid #fff000;color:#333;font-size:15px;padding:5px 10px}
article .post-info,article .entry-content .date{font-size:14px}
article img.loaded{height:auto!important}
article .entry-content blockquote,article .entry-content ul,article .entry-content ol,article .entry-content dl,article .entry-content table,article .entry-content iframe,article .entry-content h1,article .entry-content h2,article .entry-content h3,article .entry-content h4,article .entry-content h5,article .entry-content h6,article .entry-content pre{margin-top:15px}
article pre b.name{color:#eee;font-family:"Consolas","Liberation Mono",Courier,monospace;font-size:60px;line-height:1;pointer-events:none;position:absolute;right:10px;top:10px}
article .entry-content .date{color:#666}
article .entry-content ul ul,article .entry-content ul ol,article .entry-content ul dl,article .entry-content ol ul,article .entry-content ol ol,article .entry-content ol dl,article .entry-content dl ul,article .entry-content dl ol,article .entry-content dl dl,article .entry-content blockquote > p:first-of-type{margin-top:0}
.total_thread{line-height:1.6}
.page-navi{line-height:20px;overflow:hidden;padding:20px 0;position:relative;width:100%}
article.post-search{padding-bottom:0}article .entry-content ul,article .entry-content ol,article .entry-content dl{margin-left:25px}
.page-navi .prev{float:left}
.page-navi .next{float:right}.page-navi .center{margin:auto;text-align:center;width:80px}#comments{border-top:1px solid #fff;border-bottom:1px solid #ddd;padding:20px 0}#comments,#searchResult{min-height:350px}#toc-container,#toc{float:right}
#toc{border:1px solid #e2e2e2;font-size:14px;margin:0 0 15px 20px;max-width:260px;min-width:120px;padding:6px}
#search form{position:relative}#toc strong{border-bottom:1px solid #e2e2e2;display:block}#toc p{margin:0;padding:0 4px}#toc ul{margin:.5em .5em .5em 1.5em}#toc ul ul{margin-top:0;margin-bottom:0}
.post-tag:link, .post-tag:visited {padding: 3px 5px; line-height: 100%; background-color: #f0f0f0; border-radius: 10px; margin: 0px 2px; display: inline-block; }
.post-tag:hover {background-color: #99a; color: #fff; text-decoration: none; }
================================================
FILE: template/theme/default/about.html
================================================
{{define "title"}}关于{{end}}
{{define "content"}}
<article class="post">
<h1>关于</h1>
<br>
<div class="entry-content">
{{noescape .about.Content}}
</div>
<div class="post-footer">
<p class="copyright-info">本站使用「<a href="http://creativecommons.org/licenses/by/4.0/deed.zh" target="_blank">署名 4.0 国际</a>」创作共享协议</p>
</div>
</article>
{{end}}
================================================
FILE: template/theme/default/archives.html
================================================
{{define "title"}}归档{{end}}
{{define "content"}}
<article class="post post-list">
<h1 class="title">归档</h1>
<div class="entry-content" style="border-bottom: none;">
{{range .archives}}
<h3>{{.Year}} 年</h3>
{{range .MonthArchives}}
<ul>
{{range .Posts}}
<li><a href="/post/{{.Path}}">{{.Title}}</a> <span class="date">({{formatTime .PostTime "Jan 02, 2006"}})</span></li>
{{end}}
</ul>
{{end}}
{{end}}
</div>
</article>
{{end}}
================================================
FILE: template/theme/default/friends.html
================================================
{{define "title"}}友情链接{{end}}
{{define "content"}}
<article class="post post-list">
<h1>友情链接</h1>
<br>
<ul style="list-style:none;">
{{range .friends}}
<li style="float:left;margin:20px"><a href="{{.Link}}" title="{{.Name}}"><img style="height:64px;" src="{{.Logo}}" alt="{{.Name}}" /></a></li>
{{end}}
</ul>
</div>
</article>
{{end}}
================================================
FILE: template/theme/default/index.html
================================================
{{define "title"}}首页{{end}}
{{define "content"}}
{{range .posts}}
<article class="post post-list" style="border-bottom: 1px solid #ddd;">
<h1 class="title"><a href="/post/{{.Path}}">{{.Title}}</a></h1>
<div class="meta">
<div class="date"><i class="fa fa-calendar" aria-hidden="true"></i> {{.PubTime}}</div>
</div>
<div class="entry-content" style="border-bottom: none;">
<p>{{.Content}}</p>
<p><a href="/post/{{.Path}}" class="more-link">继续阅读 »</a></p>
</div>
</article>
{{end}}
<nav class="page-navi"><div class="center"><a href="/archives">更多博文</a></div></nav>
{{end}}
================================================
FILE: template/theme/default/layout.html
================================================
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<meta name="format-detection" content="telephone=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{template "title" .}} - {{.site_name}}</title>
<meta name="theme-color" content="#007b8b">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="{{.site_name}}">
<meta name="msapplication-starturl" content="https://studygolang.com">
<meta name="msapplication-navbutton-color" content="#007b8b">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="{{.site_name}}">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="logo">
<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="/rss.html">
{{template "seo" .}}
<link rel="stylesheet" type="text/css" href="/static/css/main.css">
<link rel="stylesheet" type="text/css" href="/static/css/post.css">
<link rel="stylesheet" type="text/css" href="/static/css/atelier-savanna-light.min.css">
<link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="left-col">
<div class="intrude-less">
<header id="header" class="inner">
<div class="profilepic"><a href="/" aria-label="Home"></a></div>
<h1><a href="/">{{.title}}</a></h1>
<p class="subtitle">{{.subtitle}}</p>
<nav id="main-nav">
<ul>
<li><a href="/"><span>首页</span></a></li>
<li><a href="/archives"><span>归档</span></a></li>
<li><a href="/tags"><span>标签</span></a></li>
<li><a href="/friends"><span>友链</span></a></li>
<li><a href="/about"><span>关于</span></a></li>
</ul>
</nav>
<nav id="sub-nav">
<div class="social">
<a class="weibo external" rel="nofollow" href="http://weibo.com/studygolang" title="Weibo" aria-label="Weibo" target="_blank"><i class="fa fa-weibo fa-2x" aria-hidden="true"></i></a>
<a class="github external" rel="nofollow" href="https://github.com/studygolang" title="github" aria-label="github" target="_blank"><i class="fa fa-github fa-2x" aria-hidden="true"></i></a>
</div>
</nav>
</header>
</div>
</div>
<div class="mid-col">
<div class="mid-col-container">
<div id="content" class="inner">
{{template "content" .}}
</div>
</div>
<div style="clear:both;"></div>
<footer id="footer" class="inner">
© {{.app.Copyright}} - {{.site_name}} Powered by <a href="https://github.com/go-chinese-site/dreamgo" target="_blank">DreamGo</a>
<div style="color: #ccc; font-size: 86%;">
VERSION: {{.app.Version}}
<span>·</span>
{{.response_time}}
</div>
</footer>
</div>
</div>
</body>
</html>
================================================
FILE: template/theme/default/single.html
================================================
{{define "title"}}{{.post.Title}}{{end}}
{{define "content"}}
<article class="post">
<div class="entry-content">
{{noescape .post.Content}}
<p>--EOF--</p>
</div>
<div class="post-footer">
<p class="post-info">posted @ <span class="date">{{.post.PubTime}}</span>
{{if .post.Tags}}
,并被添加「
{{range .post.Tags}}
<a href="/tag/{{.}}" class="post-tag">{{.}}</a>
{{end}}
」
{{end}}
标签。<a href="/post/about-dreamgo.md">查看本文 Markdown 版本 »</a>
</p>
<p class="copyright-info">本站使用「<a href="http://creativecommons.org/licenses/by/4.0/deed.zh" target="_blank">署名 4.0 国际</a>」创作共享协议</p>
</div>
</article>
{{end}}
================================================
FILE: template/theme/default/tag.html
================================================
{{define "title"}}标签{{end}}
{{define "content"}}
<article class="post post-list">
<h1 class="title">标签:{{.tag.Name}}</h1>
<div class="entry-content" style="border-bottom: none;">
相关文章(<strong>{{len .tag.Posts}}</strong>):
<br/>
<ul>
{{range .tag.Posts}}
<li><a href="/post/{{.Path}}">{{.Title}}</a> <span class="date">({{formatTime .PostTime "Jan 02, 2006"}})</span></li>
{{end}}
</ul>
</div>
</article>
{{end}}
================================================
FILE: template/theme/default/tags.html
================================================
{{define "title"}}标签--{{.tag.Name}}{{end}}
{{define "content"}}
<article class="post post-list">
<h1 class="title">标签</h1>
<div class="entry-content" style="border-bottom: none;">
<ul>
{{range .tags}}
<li><a href="/tag/{{.Name}}">{{.Name}}({{len .Posts}})</a></li>
{{end}}
</ul>
</div>
</article>
{{end}}
gitextract_maqhqepw/
├── .gitignore
├── .travis.yml
├── Dockerfile
├── Dockerfile_release
├── LICENSE
├── README.md
├── config/
│ └── env.yml
├── dreamgo.sql
├── getpkg.bat
├── getpkg.sh
├── install.bat
├── install.sh
├── run.bat
├── run.sh
├── src/
│ ├── .gitignore
│ ├── config/
│ │ └── config.go
│ ├── datasource/
│ │ ├── ds.go
│ │ ├── github_repo.go
│ │ ├── github_repo_test.go
│ │ ├── mongodb.go
│ │ ├── mysql_repo.go
│ │ └── mysql_repo_test.go
│ ├── dreamgo/
│ │ └── main.go
│ ├── global/
│ │ └── app.go
│ ├── http/
│ │ └── controller/
│ │ ├── about.go
│ │ ├── archive.go
│ │ ├── friends.go
│ │ ├── index.go
│ │ ├── post.go
│ │ ├── routes.go
│ │ ├── static.go
│ │ └── tag.go
│ ├── logger/
│ │ └── log.go
│ ├── model/
│ │ ├── archive.go
│ │ ├── friend.go
│ │ ├── post.go
│ │ └── tag.go
│ ├── route/
│ │ └── mux.go
│ ├── util/
│ │ ├── file.go
│ │ └── util.go
│ ├── vendor/
│ │ └── manifest
│ └── view/
│ └── template.go
├── static/
│ └── css/
│ ├── main.css
│ └── post.css
└── template/
└── theme/
└── default/
├── about.html
├── archives.html
├── friends.html
├── index.html
├── layout.html
├── single.html
├── tag.html
└── tags.html
SYMBOL INDEX (138 symbols across 27 files)
FILE: dreamgo.sql
type `article` (line 25) | CREATE TABLE `article` (
type `article_tag` (line 51) | CREATE TABLE `article_tag` (
type `db_version` (line 76) | CREATE TABLE `db_version` (
type `friend_link` (line 98) | CREATE TABLE `friend_link` (
type `tag` (line 124) | CREATE TABLE `tag` (
FILE: src/config/config.go
function Parse (line 17) | func Parse(configFile string) {
FILE: src/datasource/ds.go
constant TypeGit (line 23) | TypeGit = "git"
constant TypeMysql (line 24) | TypeMysql = "mysql"
type DataSourcer (line 28) | type DataSourcer interface
function Init (line 44) | func Init() {
function replaceCodeParts (line 60) | func replaceCodeParts(htmlFile []byte) (string, error) {
FILE: src/datasource/github_repo.go
constant PostDir (line 32) | PostDir = "data/post/"
constant IndexFile (line 35) | IndexFile = "index.yaml"
constant ArchiveFile (line 37) | ArchiveFile = "archive.yaml"
constant TagsFile (line 39) | TagsFile = "tags.yaml"
constant FriendFile (line 41) | FriendFile = "friends.yaml"
type GithubRepo (line 45) | type GithubRepo struct
method PostList (line 53) | func (self GithubRepo) PostList() []*model.Post {
method PostArchive (line 68) | func (self GithubRepo) PostArchive() []*model.YearArchive {
method ServeMarkdown (line 83) | func (self GithubRepo) ServeMarkdown(w http.ResponseWriter, r *http.Re...
method FindPost (line 90) | func (self GithubRepo) FindPost(path string) (*model.Post, error) {
method Pull (line 102) | func (self GithubRepo) Pull(gitRepoDir string) error {
method GenIndexYaml (line 118) | func (self GithubRepo) GenIndexYaml() {
method GenArchiveYaml (line 137) | func (self GithubRepo) GenArchiveYaml() {
method GenTagsYaml (line 199) | func (self GithubRepo) GenTagsYaml() {
method fetchPosts (line 238) | func (self GithubRepo) fetchPosts() []*model.Post {
method genOnePost (line 278) | func (self GithubRepo) genOnePost(postDir, path string) (*model.Post, ...
method parsePubTime (line 316) | func (self GithubRepo) parsePubTime(pubTime string) time.Time {
method TagList (line 338) | func (self GithubRepo) TagList() []*model.Tag {
method FindTag (line 353) | func (self GithubRepo) FindTag(tagName string) *model.Tag {
method AboutPost (line 364) | func (self GithubRepo) AboutPost() (*model.Post, error) {
method UpdateDataSource (line 381) | func (self GithubRepo) UpdateDataSource() {
method cloneRepo (line 417) | func (self GithubRepo) cloneRepo(gitRepoDir string) {
method GetFriends (line 431) | func (self GithubRepo) GetFriends() ([]*model.Friend, error) {
function NewGithub (line 48) | func NewGithub() *GithubRepo {
FILE: src/datasource/github_repo_test.go
function setup (line 21) | func setup() {
function TestGenIndexYaml (line 28) | func TestGenIndexYaml(t *testing.T) {
function TestGenArchiveYaml (line 34) | func TestGenArchiveYaml(t *testing.T) {
function TestGenTagsYaml (line 40) | func TestGenTagsYaml(t *testing.T) {
FILE: src/datasource/mongodb.go
type MongoDB (line 17) | type MongoDB struct
method sessionclone (line 43) | func (self *MongoDB) sessionclone() *mgo.Session {
method PostList (line 61) | func (self MongoDB) PostList() []*model.Post {
method PostArchive (line 77) | func (self MongoDB) PostArchive() []*model.YearArchive {
method ServeMarkdown (line 141) | func (self MongoDB) ServeMarkdown(w http.ResponseWriter, r *http.Reque...
method FindPost (line 147) | func (self MongoDB) FindPost(path string) (*model.Post, error) {
method TagList (line 164) | func (self MongoDB) TagList() []*model.Tag {
method FindTag (line 206) | func (self MongoDB) FindTag(tagName string) *model.Tag {
method AboutPost (line 217) | func (self MongoDB) AboutPost() (*model.Post, error) {
method UpdateDataSource (line 227) | func (self MongoDB) UpdateDataSource() {
method GetFriends (line 231) | func (self MongoDB) GetFriends() ([]*model.Friend, error) {
function NewMongoDB (line 24) | func NewMongoDB() *MongoDB {
FILE: src/datasource/mysql_repo.go
type MysqlRepo (line 25) | type MysqlRepo struct
method PostList (line 83) | func (self *MysqlRepo) PostList() []*model.Post {
method PostArchive (line 98) | func (self *MysqlRepo) PostArchive() []*model.YearArchive {
method ServeMarkdown (line 113) | func (self *MysqlRepo) ServeMarkdown(w http.ResponseWriter, r *http.Re...
method FindPost (line 118) | func (self *MysqlRepo) FindPost(path string) (*model.Post, error) {
method TagList (line 151) | func (self *MysqlRepo) TagList() []*model.Tag {
method FindTag (line 166) | func (self *MysqlRepo) FindTag(tagName string) *model.Tag {
method AboutPost (line 177) | func (self *MysqlRepo) AboutPost() (*model.Post, error) {
method GenIndexYaml (line 194) | func (self *MysqlRepo) GenIndexYaml() {
method parsePubTime (line 220) | func (self *MysqlRepo) parsePubTime(pubTime int64) string {
method GenArchiveYaml (line 226) | func (self *MysqlRepo) GenArchiveYaml() {
method GenTagsYaml (line 296) | func (self *MysqlRepo) GenTagsYaml() {
method genOnePost (line 346) | func (self *MysqlRepo) genOnePost(info articleInfo) *model.Post {
method GenFriendsYaml (line 359) | func (self *MysqlRepo) GenFriendsYaml() {
method UpdateDataSource (line 384) | func (self *MysqlRepo) UpdateDataSource() {
method GetFriends (line 410) | func (self *MysqlRepo) GetFriends() ([]*model.Friend, error) {
type articleInfo (line 36) | type articleInfo struct
type tagInfo (line 43) | type tagInfo struct
type friendInfo (line 48) | type friendInfo struct
function NewMysql (line 56) | func NewMysql(dbParams string) *MysqlRepo {
function prepare (line 74) | func prepare(db *sql.DB, sql string) *sql.Stmt {
FILE: src/datasource/mysql_repo_test.go
function Init (line 13) | func Init() {
function TestGenMysqlIndexYaml (line 20) | func TestGenMysqlIndexYaml(t *testing.T) {
function TestGenMysqlArchiveYaml (line 26) | func TestGenMysqlArchiveYaml(t *testing.T) {
function TestGenMysqlTagsYaml (line 32) | func TestGenMysqlTagsYaml(t *testing.T) {
FILE: src/dreamgo/main.go
function init (line 26) | func init() {
function main (line 32) | func main() {
FILE: src/global/app.go
type app (line 24) | type app struct
method InitPath (line 74) | func (this *app) InitPath() {
method Uptime (line 81) | func (this *app) Uptime() time.Duration {
method setProjectRoot (line 87) | func (this *app) setProjectRoot() {
method SetTemplateDir (line 106) | func (this *app) SetTemplateDir(theme string) {
function init (line 48) | func init() {
function PrintVersion (line 111) | func PrintVersion(w io.Writer) {
FILE: src/http/controller/about.go
type AboutController (line 17) | type AboutController struct
method RegisterRoutes (line 19) | func (self AboutController) RegisterRoutes() {
method Detail (line 23) | func (AboutController) Detail(w http.ResponseWriter, r *http.Request) {
FILE: src/http/controller/archive.go
type ArchiveController (line 16) | type ArchiveController struct
method RegisterRoute (line 19) | func (self ArchiveController) RegisterRoute() {
method List (line 24) | func (ArchiveController) List(w http.ResponseWriter, r *http.Request) {
FILE: src/http/controller/friends.go
type FriendsController (line 11) | type FriendsController struct
method RegisterRoutes (line 13) | func (self FriendsController) RegisterRoutes() {
method Detail (line 17) | func (FriendsController) Detail(w http.ResponseWriter, r *http.Request) {
FILE: src/http/controller/index.go
type IndexController (line 23) | type IndexController struct
method RegisterRoute (line 26) | func (self IndexController) RegisterRoute() {
method Home (line 31) | func (IndexController) Home(w http.ResponseWriter, r *http.Request) {
FILE: src/http/controller/post.go
type PostController (line 20) | type PostController struct
method RegisterRoute (line 23) | func (self PostController) RegisterRoute() {
method Detail (line 28) | func (PostController) Detail(w http.ResponseWriter, r *http.Request) {
FILE: src/http/controller/routes.go
function RegisterRoutes (line 10) | func RegisterRoutes() {
FILE: src/http/controller/static.go
type StaticController (line 11) | type StaticController struct
method RegisterRoutes (line 13) | func (self StaticController) RegisterRoutes() {
method Default (line 18) | func (StaticController) Default(w http.ResponseWriter, r *http.Request) {
FILE: src/http/controller/tag.go
type TagController (line 19) | type TagController struct
method RegisterRoute (line 22) | func (self TagController) RegisterRoute() {
method Detail (line 28) | func (TagController) Detail(w http.ResponseWriter, r *http.Request) {
method List (line 46) | func (TagController) List(w http.ResponseWriter, r *http.Request) {
FILE: src/logger/log.go
function Instance (line 19) | func Instance() *zap.Logger {
function Init (line 24) | func Init(srvName string) *zap.Logger {
function NewLogger (line 30) | func NewLogger(srvName string) *zap.Logger {
function newRollingFile (line 88) | func newRollingFile(directory string) zapcore.WriteSyncer {
function newZapLogger (line 103) | func newZapLogger(isProduction bool, output zapcore.WriteSyncer) (*zap.L...
type lumberjackWriteSyncer (line 133) | type lumberjackWriteSyncer struct
method run (line 153) | func (l *lumberjackWriteSyncer) run() {
method Stop (line 176) | func (l *lumberjackWriteSyncer) Stop() {
method Write (line 180) | func (l *lumberjackWriteSyncer) Write(bs []byte) (int, error) {
method Sync (line 189) | func (l *lumberjackWriteSyncer) Sync() error {
method sync (line 193) | func (l *lumberjackWriteSyncer) sync() error {
function newLumberjackWriteSyncer (line 141) | func newLumberjackWriteSyncer(l *lumberjack.Logger) *lumberjackWriteSync...
FILE: src/model/archive.go
type YearArchive (line 10) | type YearArchive struct
type MonthArchive (line 16) | type MonthArchive struct
FILE: src/model/friend.go
type Friend (line 3) | type Friend struct
FILE: src/model/post.go
type Post (line 12) | type Post struct
type Meta (line 17) | type Meta struct
FILE: src/model/tag.go
type Tag (line 9) | type Tag struct
FILE: src/route/mux.go
function HandleFunc (line 15) | func HandleFunc(pattern string, handler func(http.ResponseWriter, *http....
type BlogMux (line 20) | type BlogMux struct
method ServeHTTP (line 32) | func (this *BlogMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
function NewBlogMux (line 27) | func NewBlogMux() *BlogMux {
FILE: src/util/file.go
function Exist (line 16) | func Exist(filename string) bool {
function ScanDir (line 23) | func ScanDir(directory string) []string {
function IsDir (line 37) | func IsDir(filename string) bool {
function IsFile (line 43) | func IsFile(filename string) bool {
function Filename (line 48) | func Filename(file string) string {
function isFileOrDir (line 58) | func isFileOrDir(filename string, decideDir bool) bool {
FILE: src/util/util.go
function Contain (line 9) | func Contain(obj interface{}, target interface{}) (bool, error) {
FILE: src/view/template.go
function Render (line 29) | func Render(w http.ResponseWriter, r *http.Request, htmlFile string, dat...
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
{
"path": ".gitignore",
"chars": 311,
"preview": "# Binaries for programs and plugins\n*.exe\n*.dll\n*.so\n*.dylib\n\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output o"
},
{
"path": ".travis.yml",
"chars": 302,
"preview": "language: go\n\ngo:\n - 1.8.x\n - 1.9.x\n - tip\n\nsudo: false\n\ninstall:\n - export GOPATH=$HOME/gopath/src/github.com/go-ch"
},
{
"path": "Dockerfile",
"chars": 218,
"preview": "FROM golang\n\nRUN go get github.com/polaris1119/gvt\nRUN ln -sf /go/bin/gvt /usr/local/bin/\nADD . /dreamgo\n\n# install drea"
},
{
"path": "Dockerfile_release",
"chars": 258,
"preview": "FROM golang\n\nRUN mkdir /dreamgo\nRUN mkdir /dreamgo/log\nADD ./bin /dreamgo/bin\nADD ./config /dreamgo/config\nADD ./static "
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2017 Go Chinese Site\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 1721,
"preview": "# dreamgo\n\n[](https://travis-ci.org/go-c"
},
{
"path": "config/env.yml",
"chars": 410,
"preview": "# listen\nlisten:\n host: 0.0.0.0\n port: 2017\n\nsetting:\n site_name: polaris 的站点\n title: Polaris Xu\n subtitle: 专注 Go 语"
},
{
"path": "dreamgo.sql",
"chars": 5375,
"preview": "-- MySQL dump 10.13 Distrib 5.7.13, for osx10.11 (x86_64)\n--\n-- Host: 13.229.128.253 Database: dreamgo\n-- ----------"
},
{
"path": "getpkg.bat",
"chars": 270,
"preview": "@echo off\r\n\r\nsetlocal\r\n\r\nif exist getpkg.bat goto ok\r\necho getpkg.bat must be run from its folder\r\ngoto end\r\n\r\n:ok\r\n\r\nse"
},
{
"path": "getpkg.sh",
"chars": 603,
"preview": "#!/usr/bin/env bash\n\nset -e\n\nif [ ! -f getpkg.sh ]; then\n echo 'getpkg.sh must be run within its container folder' 1>"
},
{
"path": "install.bat",
"chars": 292,
"preview": "@echo off\r\n\r\nsetlocal\r\n\r\nif exist install.bat goto ok\r\necho install.bat must be run from its folder\r\ngoto end\r\n\r\n:ok\r\n\r\n"
},
{
"path": "install.sh",
"chars": 320,
"preview": "#!/usr/bin/env bash\n\nset -e\n\nif [ ! -f install.sh ]; then\n\techo 'install must be run within its container folder' 1>&2\n\t"
},
{
"path": "run.bat",
"chars": 649,
"preview": "@echo off\n\nsetlocal\n\nif exist run.bat goto exec\n echo run.bat must be run from its folder\n\n:exec\ntasklist /nh|find /i \"d"
},
{
"path": "run.sh",
"chars": 929,
"preview": "#!/usr/bin/env bash\n\n#set -e\n\nif [ ! -f run.sh ]; then\n\techo 'install must be run within its container folder' 1>&2\n\texi"
},
{
"path": "src/.gitignore",
"chars": 26,
"preview": "vendor/**\n!vendor/manifest"
},
{
"path": "src/config/config.go",
"chars": 547,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/datasource/ds.go",
"chars": 2147,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/datasource/github_repo.go",
"chars": 9865,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/datasource/github_repo_test.go",
"chars": 798,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/datasource/mongodb.go",
"chars": 5624,
"preview": "package datasource\n\nimport (\n\t\"config\"\n\t\"log\"\n\t\"model\"\n\t\"net/http\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/russross/blackfriday\"\n\t"
},
{
"path": "src/datasource/mysql_repo.go",
"chars": 10802,
"preview": "package datasource\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"global\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"model\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"s"
},
{
"path": "src/datasource/mysql_repo_test.go",
"chars": 577,
"preview": "package datasource_test\n\nimport (\n\t\"datasource\"\n\t\"global\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\nvar DefaultMysql *datasource.My"
},
{
"path": "src/dreamgo/main.go",
"chars": 1358,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/global/app.go",
"chars": 2405,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/http/controller/about.go",
"chars": 762,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/http/controller/archive.go",
"chars": 744,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/http/controller/friends.go",
"chars": 548,
"preview": "package controller\n\nimport (\n\t\"datasource\"\n\t\"logger\"\n\t\"net/http\"\n\t\"route\"\n\t\"view\"\n)\n\ntype FriendsController struct{}\n\nfu"
},
{
"path": "src/http/controller/index.go",
"chars": 868,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/http/controller/post.go",
"chars": 1165,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/http/controller/routes.go",
"chars": 679,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/http/controller/static.go",
"chars": 597,
"preview": "package controller\n\nimport (\n\t\"global\"\n\t\"net/http\"\n\t\"route\"\n\t\"strings\"\n)\n\n// 静态文件控制器\ntype StaticController struct{}\n\nfun"
},
{
"path": "src/http/controller/tag.go",
"chars": 1300,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/logger/log.go",
"chars": 4447,
"preview": "package logger\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"global\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n"
},
{
"path": "src/model/archive.go",
"chars": 466,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/model/friend.go",
"chars": 119,
"preview": "package model\n\ntype Friend struct {\n\tName string `yaml:\"name\"`\n\tLink string `yaml:\"link\"`\n\tLogo string `yaml:\"logo\"`\n}\n"
},
{
"path": "src/model/post.go",
"chars": 531,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/model/tag.go",
"chars": 334,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/route/mux.go",
"chars": 964,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/util/file.go",
"chars": 1426,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "src/util/util.go",
"chars": 556,
"preview": "package util\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n)\n\n// Contain 判断obj是否在target中,target支持的类型arrary,slice,map\nfunc Contain(obj i"
},
{
"path": "src/vendor/manifest",
"chars": 3114,
"preview": "{\n\t\"version\": 0,\n\t\"dependencies\": [\n\t\t{\n\t\t\t\"importpath\": \"github.com/PuerkitoBio/goquery\",\n\t\t\t\"repository\": \"https://git"
},
{
"path": "src/view/template.go",
"chars": 1979,
"preview": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n//"
},
{
"path": "static/css/main.css",
"chars": 4117,
"preview": "*{margin:0;padding:0}\nhtml,body{height:100%}\nbody{background:#ddd;color:#666;font-size:14px;font-family:\"-apple-system\","
},
{
"path": "static/css/post.css",
"chars": 3800,
"preview": "article input.runcode,article button{-webkit-appearance:none;background:#12b0e6;border:none;border-radius:0;box-shadow:i"
},
{
"path": "template/theme/default/about.html",
"chars": 347,
"preview": "{{define \"title\"}}关于{{end}}\n{{define \"content\"}}\n<article class=\"post\">\n\t<h1>关于</h1>\n\t<br>\n\t<div class=\"entry-content\">\n"
},
{
"path": "template/theme/default/archives.html",
"chars": 470,
"preview": "{{define \"title\"}}归档{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1 class=\"title\">归档</h1>\n\t\t<div cl"
},
{
"path": "template/theme/default/friends.html",
"chars": 353,
"preview": "{{define \"title\"}}友情链接{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1>友情链接</h1>\n\t\t<br>\n\t\t<ul style="
},
{
"path": "template/theme/default/index.html",
"chars": 595,
"preview": "{{define \"title\"}}首页{{end}}\n{{define \"content\"}}\n\t{{range .posts}}\n\t<article class=\"post post-list\" style=\"border-bottom"
},
{
"path": "template/theme/default/layout.html",
"chars": 3571,
"preview": "<html lang=\"zh-CN\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,minimum-scal"
},
{
"path": "template/theme/default/single.html",
"chars": 651,
"preview": "{{define \"title\"}}{{.post.Title}}{{end}}\n{{define \"content\"}}\n<article class=\"post\">\n\t<div class=\"entry-content\">\n\t\t{{no"
},
{
"path": "template/theme/default/tag.html",
"chars": 441,
"preview": "{{define \"title\"}}标签{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1 class=\"title\">标签:{{.tag.Name}}<"
},
{
"path": "template/theme/default/tags.html",
"chars": 323,
"preview": "{{define \"title\"}}标签--{{.tag.Name}}{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1 class=\"title\">标签"
}
]
About this extraction
This page contains the full source code of the go-chinese-site/dreamgo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (80.2 KB), approximately 26.0k tokens, and a symbol index with 138 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.