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 [![Build Status](https://travis-ci.org/go-chinese-site/dreamgo.svg?branch=master)](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 ![screenshot1](screenshot1.png) ## 如何贡献代码 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, "", "", 1) new = strings.Replace(new, "", "", 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, ``) 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"}} {{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"}}

关于


{{noescape .about.Content}}
{{end}} ================================================ FILE: template/theme/default/archives.html ================================================ {{define "title"}}归档{{end}} {{define "content"}}

归档

{{range .archives}}

{{.Year}} 年

{{range .MonthArchives}} {{end}} {{end}}
{{end}} ================================================ FILE: template/theme/default/friends.html ================================================ {{define "title"}}友情链接{{end}} {{define "content"}}

友情链接


{{end}} ================================================ FILE: template/theme/default/index.html ================================================ {{define "title"}}首页{{end}} {{define "content"}} {{range .posts}}

{{.Title}}

{{.PubTime}}

{{.Content}}

继续阅读 »

{{end}} {{end}} ================================================ FILE: template/theme/default/layout.html ================================================ {{template "title" .}} - {{.site_name}} {{template "seo" .}}
{{template "content" .}}
================================================ FILE: template/theme/default/single.html ================================================ {{define "title"}}{{.post.Title}}{{end}} {{define "content"}}
{{noescape .post.Content}}

--EOF--

{{end}} ================================================ FILE: template/theme/default/tag.html ================================================ {{define "title"}}标签{{end}} {{define "content"}}

标签:{{.tag.Name}}

相关文章({{len .tag.Posts}}):
{{end}} ================================================ FILE: template/theme/default/tags.html ================================================ {{define "title"}}标签--{{.tag.Name}}{{end}} {{define "content"}}

标签

{{end}}