[
  {
    "path": ".gitignore",
    "content": "# 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 of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736\n.glide/\n\n.vscode/\n\n\nbin\npkg\nlog\ndata\n.idea/"
  },
  {
    "path": ".travis.yml",
    "content": "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-chinese-site/dreamgo\n  - export PATH=$PATH:$HOME/gopath/src/github.com/go-chinese-site/dreamgo/bin/\n  - go get -v github.com/FiloSottile/gvt\n\nscript:\n  - sh getpkg.sh\n  - sh install.sh"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang\n\nRUN go get github.com/polaris1119/gvt\nRUN ln -sf /go/bin/gvt /usr/local/bin/\nADD . /dreamgo\n\n# install dreamgo\nWORKDIR /dreamgo\nRUN ./getpkg.sh\nRUN ./install.sh\nEXPOSE 2017\n\nENTRYPOINT [ \"./bin/dreamgo\" ]\n"
  },
  {
    "path": "Dockerfile_release",
    "content": "FROM golang\n\nRUN mkdir /dreamgo\nRUN mkdir /dreamgo/log\nADD ./bin /dreamgo/bin\nADD ./config /dreamgo/config\nADD ./static /dreamgo/static\nADD ./template /dreamgo/template\n\nEXPOSE 2017\n\nWORKDIR /dreamgo\n\n# Define default command.\nCMD [ \"/dreamgo/bin/dreamgo\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Go Chinese Site\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# dreamgo\n\n[![Build Status](https://travis-ci.org/go-chinese-site/dreamgo.svg?branch=master)](https://travis-ci.org/go-chinese-site/dreamgo)\n\n一个新手学习用的博客系统，用 Go 语言实现自己的梦想。\n\n## 开发计划\n\n### 该博客系统计划采用三种方式实现\n\n1. 不使用框架，直接使用标准库 net/http, branch-std\n2. 使用一些基本的路由库，比如 https://github.com/gorilla/mux 或 https://github.com/julienschmidt/httprouter, branch-mux\n3. 使用一个 Web 框架，可能考虑使用 Beego，因为国内貌似用这个的比较多，满足广大 gopher 的要求, branch-beego\n\n### 开发时间\n\n2017 年 10 月 1 日开始\n\n## 合作方式\n\n通过 net/http 搭建起来基本的架子后，大家可以以此为基础，加入进来\n\n## 设计说明\n\n1. 数据源（存储），支持三种：\n\t1. 将文章存在 Github Repo 上；\n\t2. 将文章存入 MySQL 中；\n\t3. 将文字存入 MongoDB 中；\n\n2. 支持自定义模板\n\n3. 通过 yaml 做项目配置\n\n## Roadmap\n\n1. master 分支和 branch-std 分支采用 net/http 方式实现。\n\t- 目前已实现了如下功能：\n\t\t1. 基于 http.ServeMux 的简单封装：route.BlogMux，方便写中间件；\n\t\t2. 完成基于 github repo 的首页、归档、文章；\n\t\t3. 完成日志功能，在main.go中已实例化，其他地方调用logger := logger.Instance()即可；\n\t\t4. tag 列表和 tag 文章列表页\n\t\t5. 关于页面\n\t- 还未实现的功能：（大家可以认领，提 issue 告知要开发哪个或加入 qq 群沟通 195831198）\n\t\t1. ~~tag 列表和 tag 文章列表页~~；\n\t\t2. 友情链接页；\n\t\t3. ~~关于页面~~；\n\t\t4. 基于 mysql、mongodb 的存储实现，通过配置切换存储；\n\t\t5. 管理后台；\n\n2. 使用一些基本的路由库，比如 https://github.com/gorilla/mux 或 https://github.com/julienschmidt/httprouter, branch-mux 还未动工；\n\n3. 使用一个 Web 框架，可能考虑使用 Beego，因为国内貌似用这个的比较多，满足广大 gopher 的要求, branch-beego, 还未动工\n\n## Install\n\n要求：Go 1.8 及以上\n\n**注：如果你是 Windows，请将 `.sh` 的脚本改为 `.bat`**\n\n1. 本项目使用 `gvt` 作为依赖管理工具，通过 `go get github.com/polaris1119/gvt` 安装，并将 gvt 放入 PATH 中；\n2. 下载 dreamgo 源码：`git clone https://github.com/go-chinese-site/dreamgo`，比如下载到 ~/dreamgo 中；\n3. cd ~/dreamgo，执行 ./getpkg.sh；\n4. 执行 ./install.sh\n5. 启动 dreamgo：bin/dreamgo 或 执行 ./run.sh\n\n通过浏览器访问：http://localhost:2017\n\n![screenshot1](screenshot1.png)\n\n## 如何贡献代码\n\n1. fork，编码，pull request；\n2. 通过一次 pull request 后，会把你加入该项目的协作者中，之后就可以直接在该项目中写代码、push 了。\n\n\n"
  },
  {
    "path": "config/env.yml",
    "content": "# listen\nlisten:\n  host: 0.0.0.0\n  port: 2017\n\nsetting:\n  site_name: polaris 的站点\n  title: Polaris Xu\n  subtitle: 专注 Go 语言\n\nseo:\n  keywords: polaris,go,dreamgo\n  description: 这是我的个人博客\n\n# datasource\ndatasource: \n  type: git\n  url: https://github.com/go-chinese-site/dreamgo-demo\n  monogdbaddr: 192.168.0.103:27017\n  monogdbdb: test\n  mysqlAddr: dreamgo:123456@tcp(127.0.0.1:3306)/dreamgo\n# theme\ntheme: default\n\n"
  },
  {
    "path": "dreamgo.sql",
    "content": "-- MySQL dump 10.13  Distrib 5.7.13, for osx10.11 (x86_64)\n--\n-- Host: 13.229.128.253    Database: dreamgo\n-- ------------------------------------------------------\n-- Server version\t5.7.20\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `article`\n--\n\nDROP TABLE IF EXISTS `article`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `article` (\n  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `title` text NOT NULL COMMENT '标题',\n  `pub_time` bigint(20) NOT NULL COMMENT '发布时间',\n  `content` text NOT NULL COMMENT '内容',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='文章';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `article`\n--\n\nLOCK TABLES `article` WRITE;\n/*!40000 ALTER TABLE `article` DISABLE KEYS */;\nINSERT 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');\n/*!40000 ALTER TABLE `article` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `article_tag`\n--\n\nDROP TABLE IF EXISTS `article_tag`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `article_tag` (\n  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `article_id` bigint(20) NOT NULL COMMENT '文章id',\n  `tag_id` int(11) NOT NULL COMMENT '标签id',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='文章标签';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `article_tag`\n--\n\nLOCK TABLES `article_tag` WRITE;\n/*!40000 ALTER TABLE `article_tag` DISABLE KEYS */;\nINSERT INTO `article_tag` VALUES (1,1,1),(2,1,2),(3,1,3),(4,1,4);\n/*!40000 ALTER TABLE `article_tag` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `db_version`\n--\n\nDROP TABLE IF EXISTS `db_version`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `db_version` (\n  `version` int(11) DEFAULT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `db_version`\n--\n\nLOCK TABLES `db_version` WRITE;\n/*!40000 ALTER TABLE `db_version` DISABLE KEYS */;\nINSERT INTO `db_version` VALUES (2017101301);\n/*!40000 ALTER TABLE `db_version` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `friend_link`\n--\n\nDROP TABLE IF EXISTS `friend_link`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `friend_link` (\n  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `name` varchar(128) NOT NULL DEFAULT '' COMMENT '名称',\n  `link` varchar(128) NOT NULL DEFAULT '' COMMENT '链接',\n  `logo` varchar(128) NOT NULL DEFAULT '' COMMENT '图标',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='友情链接';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `friend_link`\n--\n\nLOCK TABLES `friend_link` WRITE;\n/*!40000 ALTER TABLE `friend_link` DISABLE KEYS */;\nINSERT INTO `friend_link` VALUES (1,'Go语言中文网','https://studygolang.com','https://static.studygolang.com/img/favicon.ico');\n/*!40000 ALTER TABLE `friend_link` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `tag`\n--\n\nDROP TABLE IF EXISTS `tag`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `tag` (\n  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',\n  `name` varchar(128) NOT NULL COMMENT '名称',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='标签';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `tag`\n--\n\nLOCK TABLES `tag` WRITE;\n/*!40000 ALTER TABLE `tag` DISABLE KEYS */;\nINSERT INTO `tag` VALUES (1,'dreamgo'),(2,'博客系统'),(3,'标准库'),(4,'路由');\n/*!40000 ALTER TABLE `tag` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2017-12-18 10:54:12\n"
  },
  {
    "path": "getpkg.bat",
    "content": "@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\nset OLDGOPATH=%GOPATH%\r\nset GOPATH=%~dp0\r\n\r\ncd src\r\n\r\ngvt restore -connections 8 -precaire\r\n\r\ncd ..\r\n\r\nset GOPATH=%OLDGOPATH%\r\n\r\n:end\r\necho finished\r\n\r\n"
  },
  {
    "path": "getpkg.sh",
    "content": "#!/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>&2\n    exit 1\nfi\n\nif ! type gvt >/dev/null 2>&1; then\n\techo >&2 \"This script requires the gvt tool.\"\n\techo >&2 \"You may obtain it with the following command:\"\n\techo >&2 \"go get github.com/polaris1119/gvt\"\n\texit 1\nfi\n\nOLDGOPATH=\"$GOPATH\"\nexport GOPATH=`pwd`\n\ncd src\n\nif [ \"$1\" = \"update\" ]; then\n\tif [ -d \"vendor/github.com\" ]; then\n\t\tgvt update -all\n\tfi\nelif [ -f \"vendor/manifest\" ]; then\n\tgvt restore -connections 8 -precaire\nfi\n\ncd ..\n\nexport GOPATH=\"$OLDGOPATH\"\n\necho 'finished'\n"
  },
  {
    "path": "install.bat",
    "content": "@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\nset GOBIN=\r\nset OLDGOPATH=%GOPATH%\r\nset GOPATH=%~dp0\r\n\r\nif not exist log mkdir log\r\n\r\ngofmt -w -s src\r\n\r\ngo install dreamgo\r\n\r\nset GOPATH=%OLDGOPATH%\r\n\r\n:end\r\necho finished"
  },
  {
    "path": "install.sh",
    "content": "#!/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\texit 1\nfi\n\nCURDIR=`pwd`\nOLDGOPATH=\"$GOPATH\"\nexport GOPATH=\"$CURDIR\"\nexport GOBIN=\n\nif [ ! -d log ]; then\n\tmkdir log\nfi\n\ngofmt -w -s src\n\ngo install dreamgo\n\nexport GOPATH=\"$OLDGOPATH\"\n\necho 'finished'"
  },
  {
    "path": "run.bat",
    "content": "@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 \"dreamgo\"\n\nif ERRORLEVEL 1 (\n    goto reload\n ) else (\n     goto kill\n )\n\n\n\n:reload\ncall install.bat\nif exist bin\\dreamgo.exe (\n    echo .........rebuild success.........\n    goto run\n    )else (\n    echo .........the command install fail.........\n    goto end\n    )\n\n:run\necho .........the program is starting.......\nstart /min bin\\dreamgo.exe\necho .........started success.........\ngoto end\n\n:kill\necho .........run kill exist process.........\n taskkill /f /im \"dreamgo.exe\" /t >null 2>&1\n goto reload\n\n:end\necho run finished\n\n \n"
  },
  {
    "path": "run.sh",
    "content": "#!/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\texit 1\nfi\n\nps -ef|grep dreamgo |grep -v grep\nif [ $? -ne 0 ];then\n    source ./install.sh\n    if [ ! -f bin/dreamgo ];then\n     echo .........the command install fail.........\n     exit 1\n     else \n     echo .........the program is starting.......\n     nohup ./bin/dreamgo >/dev/null 2>&1 &\n     echo .........started success.........\n     echo finished\n     fi\nelse\n    echo .........run kill exist process.........\n    ps -ef|grep dreamgo |grep -v grep |awk '{print $2}' |xargs kill -9 >/dev/null 2>&1\n     source ./install.sh\n    if [ ! -f bin/dreamgo ];then\n     echo .........the command install fail.........\n     exit 1\n     else \n     echo .........the program is starting.......\n     nohup ./bin/dreamgo >/dev/null 2>&1 &\n     echo .........started success.........\n     echo start finished\n     fi\nfi\n"
  },
  {
    "path": "src/.gitignore",
    "content": "vendor/**\n!vendor/manifest"
  },
  {
    "path": "src/config/config.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage config\n\nimport (\n\t\"github.com/go-chinese-site/cfg\"\n)\n\n// YamlConfig stores the config content\nvar YamlConfig *cfg.YamlConfig\n\n// Parse parses the configFile into YamlConfig\nfunc Parse(configFile string) {\n\tvar err error\n\tYamlConfig, err = cfg.ParseYaml(configFile)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "src/datasource/ds.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage datasource\n\nimport (\n\t\"bytes\"\n\t\"config\"\n\t\"model\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/sourcegraph/syntaxhighlight\"\n)\n\n// 数据源类型\nconst (\n\tTypeGit   = \"git\"\n\tTypeMysql = \"mysql\"\n)\n\n// DataSourcer 数据源接口\ntype DataSourcer interface {\n\tPostList() []*model.Post\n\tPostArchive() []*model.YearArchive\n\tServeMarkdown(w http.ResponseWriter, r *http.Request, filename string)\n\tFindPost(path string) (*model.Post, error)\n\tTagList() []*model.Tag\n\tFindTag(tagName string) *model.Tag\n\tAboutPost() (*model.Post, error)\n\tUpdateDataSource()\n\tGetFriends() ([]*model.Friend, error)\n}\n\n// DefaultDataSourcer 默认数据源\nvar DefaultDataSourcer DataSourcer\n\n// Init 数据源初始化\nfunc Init() {\n\n\tdataSourcerType := config.YamlConfig.Get(\"datasource.type\").String()\n\tswitch dataSourcerType {\n\tcase \"git\":\n\t\tDefaultDataSourcer = NewGithub()\n\tcase \"mongodb\":\n\t\tDefaultDataSourcer = NewMongoDB()\n\tcase \"mysql\":\n\t\tDefaultDataSourcer = NewMysql(config.YamlConfig.Get(\"datasource.mysqlAddr\").String())\n\tdefault:\n\t\tDefaultDataSourcer = NewGithub()\n\t}\n\tgo DefaultDataSourcer.UpdateDataSource()\n}\n\nfunc replaceCodeParts(htmlFile []byte) (string, error) {\n\tbyteReader := bytes.NewReader(htmlFile)\n\tdoc, err := goquery.NewDocumentFromReader(byteReader)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error while parsing html\")\n\t}\n\n\t// find code-parts via css selector and replace them with highlighted versions\n\tdoc.Find(\"code[class*=\\\"language-\\\"]\").Each(func(i int, s *goquery.Selection) {\n\t\toldCode := s.Text()\n\t\tformatted, _ := syntaxhighlight.AsHTML([]byte(oldCode))\n\t\ts.SetHtml(string(formatted))\n\t})\n\tnew, err := doc.Html()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error while generating html\")\n\t}\n\n\t// replace unnecessarily added html tags\n\tnew = strings.Replace(new, \"<html><head></head><body>\", \"\", 1)\n\tnew = strings.Replace(new, \"</body></html>\", \"\", 1)\n\treturn new, nil\n}\n"
  },
  {
    "path": "src/datasource/github_repo.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage datasource\n\nimport (\n\t\"config\"\n\t\"global\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"model\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\t\"util\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/robfig/cron\"\n\t\"github.com/russross/blackfriday\"\n\tyaml \"gopkg.in/yaml.v2\"\n)\n\nconst (\n\t// PostDir 文章存放目录\n\tPostDir = \"data/post/\"\n\n\t// IndexFile 首页数据文件\n\tIndexFile = \"index.yaml\"\n\t// ArchiveFile 归档数据文件\n\tArchiveFile = \"archive.yaml\"\n\t// TagsFile 标签数据文件\n\tTagsFile = \"tags.yaml\"\n\t// FriendFile 友情链接数据文件\n\tFriendFile = \"friends.yaml\"\n)\n\n// GithubRepo git数据源结构体\ntype GithubRepo struct{}\n\n// NewGithub 创建git数据源实例，相当于构造方法\nfunc NewGithub() *GithubRepo {\n\treturn &GithubRepo{}\n}\n\n// PostList 读取文章列表\nfunc (self GithubRepo) PostList() []*model.Post {\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + IndexFile)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tposts := make([]*model.Post, 0)\n\terr = yaml.Unmarshal(in, &posts)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn posts\n}\n\n// PostArchive 读取归档列表\nfunc (self GithubRepo) PostArchive() []*model.YearArchive {\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + ArchiveFile)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tyearArchives := make([]*model.YearArchive, 0)\n\terr = yaml.Unmarshal(in, &yearArchives)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn yearArchives\n}\n\n// ServeMarkdown 处理查看 Markdown 请求\nfunc (self GithubRepo) ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string) {\n\thttp.ServeFile(w, r, global.App.ProjectRoot+PostDir+util.Filename(filename)+\"/post.md\")\n}\n\nvar titleReg = regexp.MustCompile(`^#\\s(.+)`)\n\n// FindPost 根据路径查找文章\nfunc (self GithubRepo) FindPost(path string) (*model.Post, error) {\n\tpostDir := global.App.ProjectRoot + PostDir + path\n\n\tpost, err := self.genOnePost(postDir, path)\n\tif err == nil {\n\t\tpost.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))\n\t}\n\n\treturn post, err\n}\n\n// Pull 使用 git pull origin master 命令从远程仓库更新文章\nfunc (self GithubRepo) Pull(gitRepoDir string) error {\n\tcmdName := \"git\"\n\tpullArgs := []string{\"pull\", \"origin\", \"master\"}\n\n\tcmd := exec.Command(cmdName, pullArgs...)\n\tcmd.Dir = gitRepoDir\n\n\tif err := cmd.Run(); err != nil {\n\t\tlog.Printf(\"error pulling master at %s: %v\", gitRepoDir, err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GenIndexYaml 生成首页数据文件index.yaml\nfunc (self GithubRepo) GenIndexYaml() {\n\tposts := self.fetchPosts()\n\t// 首页最多显示20篇文章\n\tlength := 20\n\tif len(posts) < length {\n\t\tlength = len(posts)\n\t}\n\n\tbuf, err := yaml.Marshal(posts[:length])\n\tif err != nil {\n\t\tlog.Printf(\"gen index yaml error:%v\\n\", err)\n\t\treturn\n\t}\n\n\tindexYaml := global.App.ProjectRoot + PostDir + IndexFile\n\tioutil.WriteFile(indexYaml, buf, 0777)\n}\n\n// GenArchiveYaml 生成归档数据文件archive.yaml\nfunc (self GithubRepo) GenArchiveYaml() {\n\tposts := self.fetchPosts()\n\n\tyearArchiveMap := make(map[int]*model.YearArchive)\n\n\tfor _, post := range posts {\n\t\tpost.Content = \"\"\n\n\t\tyear := post.PostTime.Year()\n\t\tmonth := int(post.PostTime.Month())\n\n\t\tif yearArchive, ok := yearArchiveMap[year]; ok {\n\t\t\tmonthExists := false\n\t\t\tfor _, monthArchive := range yearArchive.MonthArchives {\n\t\t\t\tif monthArchive.Month == month {\n\t\t\t\t\tmonthArchive.Posts = append(monthArchive.Posts, post)\n\t\t\t\t\tmonthExists = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !monthExists {\n\t\t\t\tyearArchive.MonthArchives = append(yearArchive.MonthArchives, &model.MonthArchive{\n\t\t\t\t\tMonth: month,\n\t\t\t\t\tPosts: []*model.Post{post},\n\t\t\t\t})\n\t\t\t}\n\n\t\t} else {\n\t\t\tmonthArchive := &model.MonthArchive{\n\t\t\t\tMonth: month,\n\t\t\t\tPosts: []*model.Post{post},\n\t\t\t}\n\t\t\tyearArchive = &model.YearArchive{\n\t\t\t\tYear:          year,\n\t\t\t\tMonthArchives: []*model.MonthArchive{monthArchive},\n\t\t\t}\n\n\t\t\tyearArchiveMap[year] = yearArchive\n\t\t}\n\t}\n\n\tyearArchives := make([]*model.YearArchive, 0, len(yearArchiveMap))\n\tfor _, yearArchive := range yearArchiveMap {\n\t\tyearArchives = append(yearArchives, yearArchive)\n\t}\n\n\tsort.Slice(yearArchives, func(i, j int) bool {\n\t\treturn yearArchives[i].Year > yearArchives[j].Year\n\t})\n\n\tbuf, err := yaml.Marshal(yearArchives)\n\tif err != nil {\n\t\tlog.Printf(\"gen archives yaml error:%v\\n\", err)\n\t\treturn\n\t}\n\n\tarchiveYaml := global.App.ProjectRoot + PostDir + ArchiveFile\n\tioutil.WriteFile(archiveYaml, buf, 0777)\n}\n\n// GenTagsYaml 生成标签数据文件tags.yaml\nfunc (self GithubRepo) GenTagsYaml() {\n\tallPosts := self.fetchPosts()\n\ttagMap := make(map[string][]*model.Post)\n\t// 遍历所有文章对象，分析出标签数据\n\tfor _, post := range allPosts {\n\t\tpost.Content = \"\"\n\t\tfor _, tag := range post.Tags {\n\t\t\tposts, ok := tagMap[tag]\n\t\t\tif !ok {\n\t\t\t\tposts = make([]*model.Post, 0)\n\t\t\t}\n\t\t\tposts = append(posts, post)\n\t\t\ttagMap[tag] = posts\n\t\t}\n\t}\n\t// 组装标签列表\n\ttags := make([]*model.Tag, 0)\n\tfor tag, posts := range tagMap {\n\t\tsort.Slice(posts, func(i, j int) bool {\n\t\t\treturn posts[i].PubTime > posts[j].PubTime\n\t\t})\n\t\ttags = append(tags, &model.Tag{Name: tag, Posts: posts})\n\t}\n\t// 按文件数量倒序排序\n\tsort.Slice(tags, func(i, j int) bool {\n\t\treturn len(tags[i].Posts) > len(tags[j].Posts)\n\t})\n\n\tbuf, err := yaml.Marshal(tags)\n\tif err != nil {\n\t\tlog.Printf(\"gen tags yaml error:%v\\n\", err)\n\t\treturn\n\t}\n\n\ttagsYaml := global.App.ProjectRoot + PostDir + TagsFile\n\tioutil.WriteFile(tagsYaml, buf, 0777)\n}\n\n// fetchPosts 读取所有文章数据，遍历目录，解析每个目录中的meta.yaml和post.md\nfunc (self GithubRepo) fetchPosts() []*model.Post {\n\tvar (\n\t\tposts = make([]*model.Post, 0, 31)\n\n\t\tpost *model.Post\n\t\terr  error\n\t)\n\t// 遍历 data/post 下的目录\n\tpostDir := global.App.ProjectRoot + PostDir\n\tnames := util.ScanDir(postDir)\n\tfor _, name := range names {\n\t\tif util.IsFile(postDir + name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif name == \".git\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tpost, err = self.genOnePost(postDir+name, name)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tpos := strings.Index(post.Content, `<!--more-->`)\n\t\tif pos > 0 {\n\t\t\tpost.Content = post.Content[:pos]\n\t\t}\n\n\t\tposts = append(posts, post)\n\t}\n\t// 按发布时间倒序排序\n\tsort.Slice(posts, func(i, j int) bool {\n\t\treturn posts[i].PubTime > posts[j].PubTime\n\t})\n\n\treturn posts\n}\n\n// genOnePost 解析meta.yaml和post.md文件生成model.Post对象\nfunc (self GithubRepo) genOnePost(postDir, path string) (*model.Post, error) {\n\t// 从post.md中读取文章内容\n\tmarkdown, err := ioutil.ReadFile(postDir + \"/post.md\")\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"read post.md error\")\n\t}\n\t// 从meta.yml文件读取文章信息\n\tvar meta = &model.Meta{}\n\tmetaBytes, err := ioutil.ReadFile(postDir + \"/meta.yml\")\n\tif err == nil {\n\t\terr = yaml.Unmarshal(metaBytes, meta)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"yaml unmarshal meta.yml error\")\n\t\t}\n\n\t\tmeta.PostTime = self.parsePubTime(meta.PubTime)\n\t} else {\n\t\tmeta.Path = path + \".html\"\n\t\tfileInfo, _ := os.Stat(postDir + \"/post.md\")\n\t\tmeta.PostTime = fileInfo.ModTime()\n\t\tmeta.PubTime = meta.PostTime.Format(\"2006-01-02 15:04\")\n\t\tmatches := titleReg.FindStringSubmatch(string(markdown))\n\t\tif len(matches) > 2 {\n\t\t\tmeta.Title = matches[1]\n\t\t} else {\n\t\t\tmeta.Title = path\n\t\t}\n\t}\n\n\tpost := &model.Post{\n\t\tContent: string(markdown),\n\t\tMeta:    meta,\n\t}\n\n\treturn post, nil\n}\n\n// parsePubTime 解析发布时间\nfunc (self GithubRepo) parsePubTime(pubTime string) time.Time {\n\tlayouts := []string{\n\t\t\"2006-01-02 15:04:05\",\n\t\t\"2006-01-02 15:04\",\n\t\t\"2006年01月02 15:04:05\",\n\t\t\"2006年01月02 15:04\",\n\t}\n\n\tfor _, layout := range layouts {\n\n\t\tt, err := time.ParseInLocation(layout, pubTime, time.Local)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn t\n\t}\n\n\treturn time.Now()\n}\n\n// TagList 读取标签列表\nfunc (self GithubRepo) TagList() []*model.Tag {\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + TagsFile)\n\tif err != nil {\n\t\treturn nil\n\t}\n\ttags := make([]*model.Tag, 0)\n\terr = yaml.Unmarshal(in, &tags)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn tags\n}\n\n// FindTag 通过标签名查找标签\nfunc (self GithubRepo) FindTag(tagName string) *model.Tag {\n\ttags := self.TagList()\n\tfor _, tag := range tags {\n\t\tif tag.Name == tagName {\n\t\t\treturn tag\n\t\t}\n\t}\n\treturn nil\n}\n\n// AboutPost 获取关于页\nfunc (self GithubRepo) AboutPost() (*model.Post, error) {\n\t// 从 about.md 中读取关于内容\n\tpostDir := global.App.ProjectRoot + PostDir\n\tmarkdown, err := ioutil.ReadFile(postDir + \"/about.md\")\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"read about.md error\")\n\t}\n\t// 关于页不需要 meta.yml\n\tvar meta = &model.Meta{}\n\tpost := &model.Post{\n\t\tContent: string(markdown),\n\t\tMeta:    meta,\n\t}\n\treturn post, nil\n}\n\n// UpdateDataSource 更新数据\nfunc (self GithubRepo) UpdateDataSource() {\n\t// 检查文章目录(data/post/)是否存在,不存在则克隆远程仓库\n\tgitRepoDir := global.App.ProjectRoot + PostDir\n\tif !util.Exist(gitRepoDir) {\n\t\tif err := os.MkdirAll(gitRepoDir, os.ModePerm); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tself.cloneRepo(gitRepoDir)\n\t}\n\n\tgitFolder := gitRepoDir + \".git\"\n\tfor {\n\t\tif util.Exist(gitFolder) {\n\t\t\tbreak\n\t\t}\n\n\t\tself.cloneRepo(gitRepoDir)\n\t}\n\t// 解析仓库文件，生成首页、归档、标签数据\n\tself.GenIndexYaml()\n\tself.GenArchiveYaml()\n\tself.GenTagsYaml()\n\n\t// 定时每天自动更新仓库，并生成首页、归档、标签数据\n\tc := cron.New()\n\tc.AddFunc(\"@daily\", func() {\n\t\tself.Pull(gitRepoDir)\n\t\tself.GenIndexYaml()\n\t\tself.GenArchiveYaml()\n\t\tself.GenTagsYaml()\n\t})\n\tc.Start()\n}\n\n// 使用git clone命令克隆文章仓库\nfunc (self GithubRepo) cloneRepo(gitRepoDir string) {\n\tcmdName := \"git\"\n\tpullArgs := []string{\"clone\", config.YamlConfig.Get(\"datasource.url\").String(), \".\"}\n\n\tcmd := exec.Command(cmdName, pullArgs...)\n\tcmd.Dir = gitRepoDir\n\n\tif err := cmd.Run(); err != nil {\n\t\tlog.Printf(\"error clone master at %s: %v\", gitRepoDir, err)\n\t\treturn\n\t}\n}\n\n// GetFriends 友情链接\nfunc (self GithubRepo) GetFriends() ([]*model.Friend, error) {\n\t// 从friends.yaml 中读取友情链接内容\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + FriendFile)\n\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"read friends.yaml error\")\n\t}\n\n\tfriends := make([]*model.Friend, 0)\n\terr = yaml.Unmarshal(in, &friends)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"Unmarshal friends.yaml error\")\n\t}\n\treturn friends, nil\n}\n"
  },
  {
    "path": "src/datasource/github_repo_test.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage datasource_test\n\nimport (\n\t\"datasource\"\n\t\"global\"\n\t\"os\"\n\t\"strings\"\n\n\t\"testing\"\n)\n\n// DefaultGithub git数据源结构体实例\nvar DefaultGithub *datasource.GithubRepo\n\nfunc setup() {\n\tcwd, _ := os.Getwd()\n\tpos := strings.LastIndex(cwd, \"src\")\n\tglobal.App.ProjectRoot = cwd[:pos]\n\tDefaultGithub = datasource.NewGithub()\n}\n\nfunc TestGenIndexYaml(t *testing.T) {\n\tsetup()\n\n\tDefaultGithub.GenIndexYaml()\n}\n\nfunc TestGenArchiveYaml(t *testing.T) {\n\tsetup()\n\n\tDefaultGithub.GenArchiveYaml()\n}\n\nfunc TestGenTagsYaml(t *testing.T) {\n\tsetup()\n\n\tDefaultGithub.GenTagsYaml()\n}\n"
  },
  {
    "path": "src/datasource/mongodb.go",
    "content": "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\"gopkg.in/mgo.v2\"\n\t\"gopkg.in/mgo.v2/bson\"\n)\n\n// MongoDB 数据源结构体\ntype MongoDB struct {\n\tsession *mgo.Session\n\taddr    string\n\tdb      string\n}\n\n// NewMongoDB 创建MongoDB数据源实例，相当于构造方法\nfunc NewMongoDB() *MongoDB {\n\taddr := config.YamlConfig.Get(\"datasource.monogdbaddr\").String()\n\tdb := config.YamlConfig.Get(\"datasource.monogdbdb\").String()\n\tif len(addr) <= 0 || len(addr) <= 0 {\n\t\tlog.Fatalf(\"get mongodb addr or db failed addr [%s] db[%s]\\n\", addr, db)\n\t}\n\tmongoDBDialInfo := &mgo.DialInfo{\n\t\tAddrs:     []string{addr},\n\t\tTimeout:   10 * time.Second,\n\t\tPoolLimit: 4096,\n\t}\n\tsession, err := mgo.DialWithInfo(mongoDBDialInfo)\n\tif err != nil {\n\t\tlog.Fatalf(\"dial mongodb failed err:%s\\n\", err)\n\t}\n\tsession.SetMode(mgo.Monotonic, true)\n\treturn &MongoDB{session: session, addr: addr, db: db}\n}\n\nfunc (self *MongoDB) sessionclone() *mgo.Session {\n\tif self.session == nil {\n\t\tvar err error\n\t\tmongoDBDialInfo := &mgo.DialInfo{\n\t\t\tAddrs:     []string{self.addr},\n\t\t\tTimeout:   10 * time.Second,\n\t\t\tPoolLimit: 4096,\n\t\t}\n\t\tself.session, err = mgo.DialWithInfo(mongoDBDialInfo)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"err:%s\", err)\n\t\t}\n\t\tself.session.SetMode(mgo.Monotonic, true)\n\t}\n\treturn self.session.Clone()\n}\n\n// PostList 读取文章列表\nfunc (self MongoDB) PostList() []*model.Post {\n\n\ts := self.sessionclone()\n\tdefer s.Close()\n\tposts := make([]*model.Post, 0)\n\tc := s.DB(self.db).C(\"index\")\n\t// 根据meta.pubtime 逆序排序并 取出20个\n\terr := c.Find(nil).Sort(\"-meta.pubtime\").Limit(20).All(&posts)\n\tif err != nil {\n\t\tlog.Printf(\"get list failed from mongodb err: %s\\n\", err)\n\t\treturn nil\n\t}\n\treturn posts\n}\n\n// PostArchive 归档\nfunc (self MongoDB) PostArchive() []*model.YearArchive {\n\t// 目前先从mongodb中将所有的文章都取出来 在进行处理\n\ts := self.sessionclone()\n\tdefer s.Close()\n\tposts := make([]*model.Post, 0)\n\tc := s.DB(self.db).C(\"index\")\n\t// 根据meta.pubtime 逆序排序并 取出20个\n\terr := c.Find(nil).Sort(\"-meta.pubtime\").All(&posts)\n\tif err != nil {\n\t\tlog.Printf(\"get list failed from mongodb err: %s\\n\", err)\n\t\treturn nil\n\t}\n\n\tyearArchiveMap := make(map[int]*model.YearArchive)\n\tfor _, post := range posts {\n\t\tpost.Content = \"\"\n\n\t\tyear := post.PostTime.Year()\n\t\tmonth := int(post.PostTime.Month())\n\n\t\tif yearArchive, ok := yearArchiveMap[year]; ok {\n\t\t\tmonthExists := false\n\t\t\tfor _, monthArchive := range yearArchive.MonthArchives {\n\t\t\t\tif monthArchive.Month == month {\n\t\t\t\t\tmonthArchive.Posts = append(monthArchive.Posts, post)\n\t\t\t\t\tmonthExists = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !monthExists {\n\t\t\t\tyearArchive.MonthArchives = append(yearArchive.MonthArchives, &model.MonthArchive{\n\t\t\t\t\tMonth: month,\n\t\t\t\t\tPosts: []*model.Post{post},\n\t\t\t\t})\n\t\t\t}\n\n\t\t} else {\n\t\t\tmonthArchive := &model.MonthArchive{\n\t\t\t\tMonth: month,\n\t\t\t\tPosts: []*model.Post{post},\n\t\t\t}\n\t\t\tyearArchive = &model.YearArchive{\n\t\t\t\tYear:          year,\n\t\t\t\tMonthArchives: []*model.MonthArchive{monthArchive},\n\t\t\t}\n\n\t\t\tyearArchiveMap[year] = yearArchive\n\t\t}\n\t}\n\n\tyearArchives := make([]*model.YearArchive, 0, len(yearArchiveMap))\n\tfor _, yearArchive := range yearArchiveMap {\n\t\tyearArchives = append(yearArchives, yearArchive)\n\t}\n\n\tsort.Slice(yearArchives, func(i, j int) bool {\n\t\treturn yearArchives[i].Year > yearArchives[j].Year\n\t})\n\n\treturn yearArchives\n}\n\n// ServeMarkdown 处理Markdown\nfunc (self MongoDB) ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string) {\n\t//TODO\n\t//\thttp.ServeFile(w, r, global.App.ProjectRoot+PostDir+util.Filename(filename)+\"/post.md\")\n}\n\n// FindPost 根据路径查找文章\nfunc (self MongoDB) FindPost(path string) (*model.Post, error) {\n\tvar post *model.Post\n\ts := self.sessionclone()\n\tdefer s.Close()\n\n\tc := s.DB(self.db).C(\"index\")\n\n\terr := c.Find(bson.M{\"meta.path\": path + \".html\"}).One(&post)\n\tif err != nil {\n\t\tlog.Printf(\"Find post failed from mongodb err:%s\\n\", err)\n\t\treturn post, err\n\t}\n\tpost.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))\n\treturn post, err\n}\n\n// TagList 标签列表\nfunc (self MongoDB) TagList() []*model.Tag {\n\t// 目前先从mongodb中将所有的文章都取出来 在进行处理\n\ts := self.sessionclone()\n\tdefer s.Close()\n\tallPosts := make([]*model.Post, 0)\n\tc := s.DB(self.db).C(\"index\")\n\t// 根据meta.pubtime 逆序排序并 取出20个\n\terr := c.Find(nil).Sort(\"-meta.pubtime\").All(&allPosts)\n\tif err != nil {\n\t\tlog.Printf(\"get list failed from mongodb err: %s\\n\", err)\n\t\treturn nil\n\t}\n\ttagMap := make(map[string][]*model.Post)\n\t//遍历所有文章对象，分析出标签数据\n\tfor _, post := range allPosts {\n\t\tpost.Content = \"\"\n\t\tfor _, tag := range post.Tags {\n\t\t\tposts, ok := tagMap[tag]\n\t\t\tif !ok {\n\t\t\t\tposts = make([]*model.Post, 0)\n\t\t\t}\n\t\t\tposts = append(posts, post)\n\t\t\ttagMap[tag] = posts\n\t\t}\n\t}\n\t//组装标签列表\n\ttags := make([]*model.Tag, 0)\n\tfor tag, posts := range tagMap {\n\t\tsort.Slice(posts, func(i, j int) bool {\n\t\t\treturn posts[i].PubTime > posts[j].PubTime\n\t\t})\n\t\ttags = append(tags, &model.Tag{Name: tag, Posts: posts})\n\t}\n\t//按文件数量倒序排序\n\tsort.Slice(tags, func(i, j int) bool {\n\t\treturn len(tags[i].Posts) > len(tags[j].Posts)\n\t})\n\n\treturn tags\n}\n\n// FindTag 查找标签\nfunc (self MongoDB) FindTag(tagName string) *model.Tag {\n\ttags := self.TagList()\n\tfor _, tag := range tags {\n\t\tif tag.Name == tagName {\n\t\t\treturn tag\n\t\t}\n\t}\n\treturn nil\n}\n\n// AboutPost 关于\nfunc (self MongoDB) AboutPost() (*model.Post, error) {\n\tvar meta = &model.Meta{}\n\tpost := &model.Post{\n\t\tContent: string(\"\"),\n\t\tMeta:    meta,\n\t}\n\treturn post, nil\n}\n\n// UpdateDataSource 更新数据\nfunc (self MongoDB) UpdateDataSource() {\n}\n\n// GetFriends 友情链接\nfunc (self MongoDB) GetFriends() ([]*model.Friend, error) {\n\tvar friends = []*model.Friend{\n\t\t{Name: \"go语言中文网\", Link: \"https://studygolang.com\"},\n\t}\n\n\treturn friends, nil\n}\n"
  },
  {
    "path": "src/datasource/mysql_repo.go",
    "content": "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\"strconv\"\n\t\"time\"\n\t\"util\"\n\n\t_ \"github.com/go-sql-driver/mysql\" // data source\n\t\"github.com/pkg/errors\"\n\t\"github.com/robfig/cron\"\n\t\"github.com/russross/blackfriday\"\n\t\"gopkg.in/yaml.v2\"\n)\n\n// MysqlRepo mysql 数据源结构体\ntype MysqlRepo struct {\n\tdb                    *sql.DB\n\tselectTag             *sql.Stmt\n\tselectArticleById     *sql.Stmt\n\tselectArticleIndex    *sql.Stmt\n\tselectArticleTagsById *sql.Stmt\n\tselectArticleArchives *sql.Stmt\n\tselectArticlesByTag   *sql.Stmt\n\tselectFriends         *sql.Stmt\n}\n\ntype articleInfo struct {\n\tId      int64  `json:\"id\"`\n\tTitle   string `json:\"title\"`\n\tPubTime int64  `json:\"pub_time\"`\n\tContent string `json:\"content\"`\n}\n\ntype tagInfo struct {\n\tId   int64  `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype friendInfo struct {\n\tId   int64  `json:\"id\"`\n\tName string `json:\"name\"`\n\tLink string `json:\"link\"`\n\tLogo string `json:\"logo\"`\n}\n\n// NewMysql 创建mysql数据源实例，相当于构造方法\nfunc NewMysql(dbParams string) *MysqlRepo {\n\tdb, err := sql.Open(\"mysql\", dbParams)\n\tif err != nil {\n\t\tlog.Fatalf(\"Couldn't connect to database: %s\", err)\n\t}\n\n\treturn &MysqlRepo{\n\t\tdb:                    db,\n\t\tselectTag:             prepare(db, \"SELECT * FROM `tag`\"),\n\t\tselectArticleById:     prepare(db, \"SELECT * FROM `article` WHERE `id`= ?\"),\n\t\tselectArticleIndex:    prepare(db, \"SELECT * FROM `article` ORDER BY `pub_time` DESC LIMIT 20\"),\n\t\tselectArticleTagsById: prepare(db, \"SELECT t.`name` FROM `article_tag` at LEFT JOIN `tag` t ON at.`tag_id`=t.`id` WHERE `article_id`= ?\"),\n\t\tselectArticleArchives: prepare(db, \"SELECT `id`,`title`,`pub_time` FROM `article`\"),\n\t\tselectArticlesByTag:   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`=?\"),\n\t\tselectFriends:         prepare(db, \"SELECT * FROM `friend_link`\"),\n\t}\n}\n\nfunc prepare(db *sql.DB, sql string) *sql.Stmt {\n\tstmt, err := db.Prepare(sql)\n\tif err != nil {\n\t\tlog.Fatalf(\"Prepare SQL '%s' failed: %s\", sql, err)\n\t}\n\treturn stmt\n}\n\n// PostList 读取文章列表\nfunc (self *MysqlRepo) PostList() []*model.Post {\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + IndexFile)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tposts := make([]*model.Post, 0)\n\terr = yaml.Unmarshal(in, &posts)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn posts\n}\n\n// PostArchive 读取归档列表\nfunc (self *MysqlRepo) PostArchive() []*model.YearArchive {\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + ArchiveFile)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tyearArchives := make([]*model.YearArchive, 0)\n\terr = yaml.Unmarshal(in, &yearArchives)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn yearArchives\n}\n\n// ServeMarkdown 处理查看 Markdown 请求\nfunc (self *MysqlRepo) ServeMarkdown(w http.ResponseWriter, r *http.Request, filename string) {\n\thttp.ServeFile(w, r, global.App.ProjectRoot+PostDir+util.Filename(filename)+\"/post.md\")\n}\n\n// FindPost 根据路径查找文章\nfunc (self *MysqlRepo) FindPost(path string) (*model.Post, error) {\n\tid, err := strconv.Atoi(path)\n\tif err != nil {\n\t\tlog.Printf(\"Invalid path :%s\\n\", err)\n\t\treturn nil, fmt.Errorf(\"文章不存在\")\n\t}\n\trow := self.selectArticleById.QueryRow(id)\n\tinfo := articleInfo{}\n\trow.Scan(&info.Id, &info.Title, &info.PubTime, &info.Content)\n\n\tpost := self.genOnePost(info)\n\trows, err := self.selectArticleTagsById.Query(info.Id)\n\tif err != nil {\n\t\tlog.Printf(\"Query article tags error:%s\", err)\n\t\treturn nil, fmt.Errorf(\"文章不存在\")\n\t}\n\tvar tags []string\n\tfor rows.Next() {\n\t\tvar tagName string\n\t\terr = rows.Scan(&tagName)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Scan tag error: %s\\n\", err)\n\t\t}\n\t\ttags = append(tags, tagName)\n\t}\n\tpost.Tags = tags\n\n\tpost.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))\n\n\treturn post, err\n}\n\n// TagList 读取标签列表\nfunc (self *MysqlRepo) TagList() []*model.Tag {\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + TagsFile)\n\tif err != nil {\n\t\treturn nil\n\t}\n\ttags := make([]*model.Tag, 0)\n\terr = yaml.Unmarshal(in, &tags)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn tags\n}\n\n// FindTag 通过标签名查找标签\nfunc (self *MysqlRepo) FindTag(tagName string) *model.Tag {\n\ttags := self.TagList()\n\tfor _, tag := range tags {\n\t\tif tag.Name == tagName {\n\t\t\treturn tag\n\t\t}\n\t}\n\treturn nil\n}\n\n// AboutPost 获取关于页\nfunc (self *MysqlRepo) AboutPost() (*model.Post, error) {\n\t// 从 about.md 中读取关于内容\n\tpostDir := global.App.ProjectRoot + PostDir\n\tmarkdown, err := ioutil.ReadFile(postDir + \"/about.md\")\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"read about.md error\")\n\t}\n\t// 关于页不需要 meta.yml\n\tvar meta = &model.Meta{}\n\tpost := &model.Post{\n\t\tContent: string(markdown),\n\t\tMeta:    meta,\n\t}\n\treturn post, nil\n}\n\n// GenIndexYaml 生成首页数据文件index.yaml\nfunc (self *MysqlRepo) GenIndexYaml() {\n\t// 首页最多显示20篇文章\n\tvar posts []*model.Post\n\trows, err := self.selectArticleIndex.Query()\n\tif err != nil {\n\t\tlog.Fatalf(\"query index error:%s\", err)\n\t}\n\tfor rows.Next() {\n\t\tinfo := articleInfo{}\n\t\terr = rows.Scan(&info.Id, &info.Title, &info.PubTime, &info.Content)\n\t\tif err != nil {\n\t\t\tlog.Println(\"scan error\", err)\n\t\t}\n\t\t// post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))\n\t\tposts = append(posts, self.genOnePost(info))\n\t}\n\n\tbuf, err := yaml.Marshal(posts)\n\tif err != nil {\n\t\tlog.Printf(\"gen index yaml error: %v\\n\", err)\n\t\treturn\n\t}\n\tindexYaml := global.App.ProjectRoot + PostDir + IndexFile\n\tioutil.WriteFile(indexYaml, buf, 0777)\n}\n\nfunc (self *MysqlRepo) parsePubTime(pubTime int64) string {\n\tt := time.Unix(pubTime, 0).In(time.Local)\n\treturn t.Format(\"2006-01-02 15:04:05\")\n}\n\n// GenArchiveYaml 生成归档数据文件archive.yaml\nfunc (self *MysqlRepo) GenArchiveYaml() {\n\tvar posts []*model.Post\n\trows, err := self.selectArticleArchives.Query()\n\tfor rows.Next() {\n\t\tinfo := articleInfo{}\n\t\terr = rows.Scan(&info.Id, &info.Title, &info.PubTime)\n\t\tif err != nil {\n\t\t\tlog.Println(\"query error\", err)\n\t\t}\n\t\tposts = append(posts, self.genOnePost(info))\n\t}\n\n\tyearArchiveMap := make(map[int]*model.YearArchive)\n\n\tfor _, post := range posts {\n\n\t\tyear := post.PostTime.Year()\n\t\tmonth := int(post.PostTime.Month())\n\n\t\tif yearArchive, ok := yearArchiveMap[year]; ok {\n\t\t\tmonthExists := false\n\t\t\tfor _, monthArchive := range yearArchive.MonthArchives {\n\t\t\t\tif monthArchive.Month == month {\n\t\t\t\t\tmonthArchive.Posts = append(monthArchive.Posts, post)\n\t\t\t\t\tmonthExists = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !monthExists {\n\t\t\t\tyearArchive.MonthArchives = append(yearArchive.MonthArchives, &model.MonthArchive{\n\t\t\t\t\tMonth: month,\n\t\t\t\t\tPosts: []*model.Post{post},\n\t\t\t\t})\n\t\t\t}\n\n\t\t} else {\n\t\t\tmonthArchive := &model.MonthArchive{\n\t\t\t\tMonth: month,\n\t\t\t\tPosts: []*model.Post{post},\n\t\t\t}\n\t\t\tyearArchive = &model.YearArchive{\n\t\t\t\tYear:          year,\n\t\t\t\tMonthArchives: []*model.MonthArchive{monthArchive},\n\t\t\t}\n\n\t\t\tyearArchiveMap[year] = yearArchive\n\t\t}\n\t}\n\n\tyearArchives := make([]*model.YearArchive, 0, len(yearArchiveMap))\n\tfor _, yearArchive := range yearArchiveMap {\n\t\tyearArchives = append(yearArchives, yearArchive)\n\t}\n\n\tsort.Slice(yearArchives, func(i, j int) bool {\n\t\treturn yearArchives[i].Year > yearArchives[j].Year\n\t})\n\n\tbuf, err := yaml.Marshal(yearArchives)\n\tif err != nil {\n\t\tlog.Printf(\"gen archives yaml error:%v\\n\", err)\n\t\treturn\n\t}\n\n\tarchiveYaml := global.App.ProjectRoot + PostDir + ArchiveFile\n\tioutil.WriteFile(archiveYaml, buf, 0777)\n}\n\n// GenTagsYaml 生成标签数据文件tags.yaml\nfunc (self *MysqlRepo) GenTagsYaml() {\n\ttagMap := make(map[string][]*model.Post)\n\ttagRows, err := self.selectTag.Query()\n\tif err != nil {\n\t\tlog.Fatalf(\"query tag error:%s\", err)\n\t}\n\tfor tagRows.Next() {\n\t\tinfo := tagInfo{}\n\t\terr = tagRows.Scan(&info.Id, &info.Name)\n\t\tif err != nil {\n\t\t\tlog.Println(\"scan error\", err)\n\t\t}\n\t\tarticleRows, err := self.selectArticlesByTag.Query(info.Id)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"query tag articles error:%s\", err)\n\t\t}\n\t\tfor articleRows.Next() {\n\t\t\tarticle := articleInfo{}\n\t\t\terr = articleRows.Scan(&article.Id, &article.Title, &article.PubTime)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"query error\", err)\n\t\t\t}\n\t\t\ttagMap[info.Name] = append(tagMap[info.Name], self.genOnePost(article))\n\t\t}\n\t}\n\n\t// 组装标签列表\n\ttags := make([]*model.Tag, 0)\n\tfor tag, posts := range tagMap {\n\t\tsort.Slice(posts, func(i, j int) bool {\n\t\t\treturn posts[i].PubTime > posts[j].PubTime\n\t\t})\n\t\ttags = append(tags, &model.Tag{Name: tag, Posts: posts})\n\t}\n\t// 按文件数量倒序排序\n\tsort.Slice(tags, func(i, j int) bool {\n\t\treturn len(tags[i].Posts) > len(tags[j].Posts)\n\t})\n\n\tbuf, err := yaml.Marshal(tags)\n\tif err != nil {\n\t\tlog.Printf(\"gen tags yaml error:%v\\n\", err)\n\t\treturn\n\t}\n\n\ttagsYaml := global.App.ProjectRoot + PostDir + TagsFile\n\tioutil.WriteFile(tagsYaml, buf, 0777)\n}\n\n// genOnePost 组装一个post\nfunc (self *MysqlRepo) genOnePost(info articleInfo) *model.Post {\n\treturn &model.Post{\n\t\tContent: info.Content,\n\t\tMeta: &model.Meta{\n\t\t\tTitle:    info.Title,\n\t\t\tPath:     fmt.Sprintf(\"%d.html\", info.Id),\n\t\t\tPubTime:  self.parsePubTime(info.PubTime),\n\t\t\tPostTime: time.Unix(info.PubTime, 0).In(time.Local),\n\t\t},\n\t}\n}\n\n// GenFriendsYaml 生成友情链接数据文件friends.yaml\nfunc (self *MysqlRepo) GenFriendsYaml() {\n\trows, err := self.selectFriends.Query()\n\tif err != nil {\n\t\tlog.Fatalf(\"query friend error:%s\", err)\n\t}\n\tvar friends []*model.Friend\n\tfor rows.Next() {\n\t\tinfo := friendInfo{}\n\t\terr = rows.Scan(&info.Id, &info.Name, &info.Link, &info.Logo)\n\t\tif err != nil {\n\t\t\tlog.Println(\"scan error\", err)\n\t\t}\n\t\t// post.Content, err = replaceCodeParts(blackfriday.MarkdownCommon([]byte(post.Content)))\n\t\tfriends = append(friends, &model.Friend{Name: info.Name, Link: info.Link, Logo: info.Logo})\n\t}\n\tbuf, err := yaml.Marshal(friends)\n\tif err != nil {\n\t\tlog.Printf(\"gen friends yaml error:%v\\n\", err)\n\t\treturn\n\t}\n\tfriendsYaml := global.App.ProjectRoot + PostDir + FriendFile\n\tioutil.WriteFile(friendsYaml, buf, 0777)\n}\n\n// UpdateDataSource 更新mysql数据\nfunc (self *MysqlRepo) UpdateDataSource() {\n\t// 检查文章目录(data/post/)是否存在，不存在则连接mysql生成\n\tmysqlRepoDir := global.App.ProjectRoot + PostDir\n\tif !util.Exist(mysqlRepoDir) {\n\t\tif err := os.MkdirAll(mysqlRepoDir, os.ModePerm); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\t// 解析仓库文件，生成首页、归档、标签数据\n\tself.GenIndexYaml()\n\tself.GenArchiveYaml()\n\tself.GenTagsYaml()\n\tself.GenFriendsYaml()\n\n\t// 定时每天自动更新仓库，并生成首页、归档、标签数据\n\tc := cron.New()\n\tc.AddFunc(\"@daily\", func() {\n\t\tself.GenIndexYaml()\n\t\tself.GenArchiveYaml()\n\t\tself.GenTagsYaml()\n\t\tself.GenFriendsYaml()\n\t})\n\tc.Start()\n}\n\n// GetFriends 友情链接\nfunc (self *MysqlRepo) GetFriends() ([]*model.Friend, error) {\n\t// 从friends.yaml 中读取友情链接内容\n\tin, err := ioutil.ReadFile(global.App.ProjectRoot + PostDir + FriendFile)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"read friends.yaml error\")\n\t}\n\n\tfriends := make([]*model.Friend, 0)\n\terr = yaml.Unmarshal(in, &friends)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"Unmarshal friends.yaml error\")\n\t}\n\treturn friends, nil\n}\n"
  },
  {
    "path": "src/datasource/mysql_repo_test.go",
    "content": "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.MysqlRepo\n\nfunc Init() {\n\tcwd, _ := os.Getwd()\n\tpos := strings.LastIndex(cwd, \"src\")\n\tglobal.App.ProjectRoot = cwd[:pos]\n\tDefaultMysql = datasource.NewMysql(\"dreamgo:123456@tcp(127.0.0.1:3306)/dreamgo\")\n}\n\nfunc TestGenMysqlIndexYaml(t *testing.T) {\n\tInit()\n\n\tDefaultMysql.GenIndexYaml()\n}\n\nfunc TestGenMysqlArchiveYaml(t *testing.T) {\n\tInit()\n\n\tDefaultMysql.GenArchiveYaml()\n}\n\nfunc TestGenMysqlTagsYaml(t *testing.T) {\n\tInit()\n\n\tDefaultMysql.GenTagsYaml()\n}\n"
  },
  {
    "path": "src/dreamgo/main.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage main\n\nimport (\n\t\"config\"\n\t\"datasource\"\n\t\"flag\"\n\t\"global\"\n\t\"http/controller\"\n\t\"log\"\n\t\"logger\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"route\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar configFile string\n\nfunc init() {\n\trand.Seed(time.Now().Unix())\n\n\tflag.StringVar(&configFile, \"config\", \"config/env.yml\", \"The config file. Default is $ProjectRoot/config/env.yml\")\n}\n\nfunc main() {\n\t// 日志\n\tlogger := logger.Init(\"dreamgo\")\n\tlogger.Info(\"main ... \")\n\t// 解析命令行参数\n\tflag.Parse()\n\t// 初始化程序路径\n\tglobal.App.InitPath()\n\n\tif strings.HasPrefix(configFile, \"/\") { //以/开头为绝对路径，直接解析\n\t\tconfig.Parse(configFile)\n\t} else { // 相对路径，以程序根目录为基础解析\n\t\tconfig.Parse(global.App.ProjectRoot + configFile)\n\t}\n\tdatasource.Init()\n\t// 设置模板目录，默认为default\n\tglobal.App.SetTemplateDir(config.YamlConfig.MustValue(\"theme\", \"default\"))\n\t// 从配置文件中获取监听IP和端口\n\tglobal.App.Host = config.YamlConfig.Get(\"listen.host\").String()\n\tglobal.App.Port = config.YamlConfig.Get(\"listen.port\").String()\n\n\taddr := global.App.Host + \":\" + global.App.Port\n\t// 注册路由\n\tcontroller.RegisterRoutes()\n\t// 启动监听，使用封装的 route.DefaultBlogMux 处理http请求\n\tlog.Fatal(http.ListenAndServe(addr, route.DefaultBlogMux))\n}\n"
  },
  {
    "path": "src/global/app.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\n// global 全局信息\npackage global\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Build 构建信息，从 git 仓库获取\nvar Build string\n\ntype app struct {\n\tName      string\n\tBuild     string\n\tVersion   string\n\tBuildDate time.Time\n\n\tProjectRoot string\n\tTemplateDir string\n\n\tCopyright string\n\n\tLaunchTime time.Time\n\n\tHost string\n\tPort string\n\n\tlocker sync.Mutex\n}\n\n// App is the App Info\nvar App = &app{}\n\nvar showVersion = flag.Bool(\"version\", false, \"Print version of this binary\")\n\nfunc init() {\n\tApp.Name = os.Args[0]\n\tApp.Version = \"V1.0.0\"\n\tApp.Build = Build\n\tApp.LaunchTime = time.Now()\n\t// 查找可执行程序的路径\n\tbinaryPath, err := exec.LookPath(os.Args[0])\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// 获取可执行程序的绝对路径\n\tbinaryPath, err = filepath.Abs(binaryPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// 获取可执行程序的文件信息\n\tfileInfo, err := os.Stat(binaryPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// 构建时间为可执行程序的修改时间\n\tApp.BuildDate = fileInfo.ModTime()\n\tApp.Copyright = fmt.Sprintf(\"%d\", time.Now().Year())\n}\n\n// InitPath 初始化相关路径，包括项目根目录、模板目录\nfunc (this *app) InitPath() {\n\tApp.setProjectRoot()\n\n\tApp.SetTemplateDir(\"default\")\n}\n\n// Uptime calculates the duration of lauching\nfunc (this *app) Uptime() time.Duration {\n\tthis.locker.Lock()\n\tdefer this.locker.Unlock()\n\treturn time.Now().Sub(this.LaunchTime)\n}\n\nfunc (this *app) setProjectRoot() {\n\tcurFilename := os.Args[0]\n\n\tbinaryPath, err := exec.LookPath(curFilename)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tbinaryPath, err = filepath.Abs(binaryPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprojectRoot := filepath.Dir(filepath.Dir(binaryPath))\n\n\tthis.ProjectRoot = projectRoot + \"/\"\n}\n\n// SetTemplateDir 设置模板目录\nfunc (this *app) SetTemplateDir(theme string) {\n\tthis.TemplateDir = this.ProjectRoot + \"template/theme/\" + theme + \"/\"\n}\n\n// PrintVersion prints current version info\nfunc PrintVersion(w io.Writer) {\n\tif !flag.Parsed() {\n\t\tflag.Parse()\n\t}\n\n\tif showVersion == nil || !*showVersion {\n\t\treturn\n\t}\n\n\tfmt.Fprintf(w, \"Binary: %s\\n\", App.Name)\n\tfmt.Fprintf(w, \"Version: %s\\n\", App.Version)\n\tfmt.Fprintf(w, \"Build: %s\\n\", App.Build)\n\tfmt.Fprintf(w, \"Compile date: %s\\n\", App.BuildDate.Format(\"2006-01-02 15:04:05\"))\n\tos.Exit(0)\n}\n"
  },
  {
    "path": "src/http/controller/about.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: tk103331\ttk103331@gmail.com\n\npackage controller\n\nimport (\n\t\"datasource\"\n\t\"logger\"\n\t\"net/http\"\n\t\"route\"\n\t\"view\"\n)\n\ntype AboutController struct{}\n\nfunc (self AboutController) RegisterRoutes() {\n\troute.HandleFunc(\"/about\", self.Detail)\n}\n\nfunc (AboutController) Detail(w http.ResponseWriter, r *http.Request) {\n\tabout, err := datasource.DefaultDataSourcer.AboutPost()\n\tif err == nil {\n\t\tview.Render(w, r, \"about.html\", map[string]interface{}{\"about\": about})\n\t} else {\n\t\tlogger.Instance().Error(\"get about.md error \" + err.Error())\n\t\thttp.NotFound(w, r)\n\t}\n}\n"
  },
  {
    "path": "src/http/controller/archive.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage controller\n\nimport (\n\t\"datasource\"\n\t\"net/http\"\n\t\"route\"\n\t\"view\"\n)\n\ntype ArchiveController struct{}\n\n// RegisterRoute 注册路由\nfunc (self ArchiveController) RegisterRoute() {\n\troute.HandleFunc(\"/archives\", self.List)\n}\n\n// List 处理归档列表请求\nfunc (ArchiveController) List(w http.ResponseWriter, r *http.Request) {\n\n\t// 从数据源查询归档列表\n\tyearArchives := datasource.DefaultDataSourcer.PostArchive()\n\t// 渲染模板archives.html，并传入数据\n\tview.Render(w, r, \"archives.html\", map[string]interface{}{\"archives\": yearArchives})\n}\n"
  },
  {
    "path": "src/http/controller/friends.go",
    "content": "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\nfunc (self FriendsController) RegisterRoutes() {\n\troute.HandleFunc(\"/friends\", self.Detail)\n}\n\nfunc (FriendsController) Detail(w http.ResponseWriter, r *http.Request) {\n\tfriends, err := datasource.DefaultDataSourcer.GetFriends()\n\tif err == nil {\n\t\tview.Render(w, r, \"friends.html\", map[string]interface{}{\"friends\": friends})\n\t} else {\n\t\tlogger.Instance().Error(\"get friends.yaml error \" + err.Error())\n\t\thttp.NotFound(w, r)\n\t}\n}\n"
  },
  {
    "path": "src/http/controller/index.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage controller\n\nimport (\n\t\"datasource\"\n\t\"net/http\"\n\t\"route\"\n\t\"view\"\n)\n\nvar defaults = map[string]bool{\n\t\"/\":           true,\n\t\"/index.html\": true,\n\t\"/index.htm\":  true,\n}\n\n// IndexController 首页 controller\ntype IndexController struct{}\n\n// RegisterRoute 注册路由\nfunc (self IndexController) RegisterRoute() {\n\troute.HandleFunc(\"/\", self.Home)\n}\n\n// Home 首页\nfunc (IndexController) Home(w http.ResponseWriter, r *http.Request) {\n\tif _, ok := defaults[r.RequestURI]; !ok {\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\tposts := datasource.DefaultDataSourcer.PostList()\n\tview.Render(w, r, \"index.html\", map[string]interface{}{\"posts\": posts})\n}\n"
  },
  {
    "path": "src/http/controller/post.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage controller\n\nimport (\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"route\"\n\t\"strings\"\n\n\t\"datasource\"\n\t\"util\"\n\t\"view\"\n)\n\ntype PostController struct{}\n\n// RegisterRoute register route\nfunc (self PostController) RegisterRoute() {\n\troute.HandleFunc(\"/post/\", self.Detail)\n}\n\n// Detail 处理文件详情请求\nfunc (PostController) Detail(w http.ResponseWriter, r *http.Request) {\n\t// 获取文章文件名，即文章的路径\n\tfilename := filepath.Base(r.RequestURI)\n\tif strings.HasSuffix(filename, \".md\") {\n\t\t// 处理markdown\n\t\tdatasource.DefaultDataSourcer.ServeMarkdown(w, r, filename)\n\n\t} else if strings.HasSuffix(filename, \".html\") {\n\t\t// 根据路径查找文件\n\t\tpost, err := datasource.DefaultDataSourcer.FindPost(util.Filename(filename))\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\t// 渲染模板single.html，并传入数据\n\t\tview.Render(w, r, \"single.html\", map[string]interface{}{\n\t\t\t\"post\": post,\n\t\t})\n\t} else {\n\t\t// 返回404\n\t\thttp.NotFound(w, r)\n\t}\n}\n"
  },
  {
    "path": "src/http/controller/routes.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage controller\n\n// RegisterRoutes 注册路由\nfunc RegisterRoutes() {\n\tnew(PostController).RegisterRoute()     // 注册文章相关路由\n\tnew(ArchiveController).RegisterRoute()  // 注册归档相关路由\n\tnew(IndexController).RegisterRoute()    // 注册首页相关路由\n\tnew(TagController).RegisterRoute()      // 注册标签相关路由\n\tnew(AboutController).RegisterRoutes()   // 注册关于页面路由\n\tnew(StaticController).RegisterRoutes()  // 注册静态文件路由\n\tnew(FriendsController).RegisterRoutes() // 注册友联相关路由\n}\n"
  },
  {
    "path": "src/http/controller/static.go",
    "content": "package controller\n\nimport (\n\t\"global\"\n\t\"net/http\"\n\t\"route\"\n\t\"strings\"\n)\n\n// 静态文件控制器\ntype StaticController struct{}\n\nfunc (self StaticController) RegisterRoutes() {\n\troute.HandleFunc(\"/static/\", self.Default)\n}\n\n// Default 以/static/开头的URL为静态文件，使用 http.FileServer 直接处理\nfunc (StaticController) Default(w http.ResponseWriter, r *http.Request) {\n\treqURI := r.RequestURI\n\t//以/结尾的URL，直接返回404\n\tif strings.HasSuffix(reqURI, \"/\") {\n\t\thttp.NotFound(w, r)\n\t} else {\n\t\tfileHandler := http.StripPrefix(\"/static/\", http.FileServer(http.Dir(global.App.ProjectRoot+\"/static\")))\n\t\tfileHandler.ServeHTTP(w, r)\n\t}\n}\n"
  },
  {
    "path": "src/http/controller/tag.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage controller\n\nimport (\n\t\"datasource\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"route\"\n\t\"sort\"\n\t\"view\"\n)\n\ntype TagController struct{}\n\n// RegisterRoute 注册路由\nfunc (self TagController) RegisterRoute() {\n\troute.HandleFunc(\"/tag/\", self.Detail)\n\troute.HandleFunc(\"/tags\", self.List)\n}\n\n// Detail 处理标签详情请求\nfunc (TagController) Detail(w http.ResponseWriter, r *http.Request) {\n\t// 从URL中获取标签名\n\treqUrl, _ := url.ParseRequestURI(r.RequestURI)\n\ttagName := filepath.Base(reqUrl.Path)\n\n\t// 根据标签名查询标签\n\ttag := datasource.DefaultDataSourcer.FindTag(tagName)\n\n\tif tag != nil {\n\t\t// 渲染模板tag.html，并传入数据\n\t\tview.Render(w, r, \"tag.html\", map[string]interface{}{\"tag\": tag})\n\t} else {\n\t\t// 返回404\n\t\thttp.NotFound(w, r)\n\t}\n}\n\n// List 处理标签列表请求\nfunc (TagController) List(w http.ResponseWriter, r *http.Request) {\n\n\t// 从数据源获取标签列表\n\ttags := datasource.DefaultDataSourcer.TagList()\n\t// 按文章数量倒序排序\n\n\tsort.Slice(tags, func(i, j int) bool {\n\t\treturn len(tags[i].Posts) > len(tags[j].Posts)\n\t})\n\t// 渲染模板tags.html，并传入数据\n\tview.Render(w, r, \"tags.html\", map[string]interface{}{\"tags\": tags})\n}\n"
  },
  {
    "path": "src/logger/log.go",
    "content": "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\t\"gopkg.in/natefinch/lumberjack.v2\"\n)\n\nvar instance *zap.Logger\n\n// Instance 唯一实例\nfunc Instance() *zap.Logger {\n\treturn instance\n}\n\n// Init作用初始化,srvName 生成的日志文件夹名字\nfunc Init(srvName string) *zap.Logger {\n\tinstance = NewLogger(srvName)\n\treturn instance\n}\n\n// NewLogger 新建日志\nfunc NewLogger(srvName string) *zap.Logger {\n\n\tdirectory := global.App.ProjectRoot\n\tif len(directory) == 0 {\n\t\tdirectory = path.Join(\"\", \"log\", srvName)\n\t} else {\n\t\tdirectory = path.Join(directory, \"log\", srvName)\n\t}\n\twriters := []zapcore.WriteSyncer{newRollingFile(directory)}\n\twriters = append(writers, os.Stdout)\n\tlogger, _ := newZapLogger(true, zapcore.NewMultiWriteSyncer(writers...))\n\tzap.RedirectStdLog(logger)\n\n\t/*updateLogLevel( serviceName, dyn, isProduction)\n\tgo func() {\n\t\tticker := time.NewTicker(30 * time.Second)\n\t\tfor range ticker.C {\n\t\t\tupdateLogLevel( serviceName, dyn, isProduction)\n\t\t}\n\t}()*/\n\n\treturn logger\n}\n\n/*func updateLogLevel(serviceName string, dyn *zap.AtomicLevel, isProduction bool) {\n\n\toriginLevelString := \"info\"\n\tif !isProduction {\n\t\toriginLevelString = \"debug\"\n\t}\n\n\tlevelConf := make(map[string]map[string]string)\n\n\tnewLevelString, ok := levelConf[serviceName][\"127.0.0.1\"]\n\tif !ok {\n\t\tnewLevelString, ok = levelConf[serviceName][\"*\"]\n\t\tif !ok {\n\t\t\tnewLevelString = originLevelString\n\t\t}\n\t}\n\n\tif !ok {\n\t\tnewLevelString, ok = levelConf[serviceName][\"*\"]\n\t\tif !ok {\n\t\t\tnewLevelString = originLevelString\n\t\t}\n\t}\n\n\tnewLevel := new(zapcore.Level)\n\tif err := newLevel.Set(newLevelString); err != nil {\n\t\tnewLevel.Set(originLevelString)\n\t}\n\tif dyn.Level() != *newLevel {\n\t\tlog.Println(\"修改日志等级: \", dyn.Level().String(), \"=>\", newLevel.String())\n\t\tdyn.SetLevel(*newLevel)\n\t}\n}*/\n\nfunc newRollingFile(directory string) zapcore.WriteSyncer {\n\tif err := os.MkdirAll(directory, 0766); err != nil {\n\t\tlog.Println(\"failed create log directory:\", directory, \":\", err)\n\t\treturn nil\n\t}\n\n\treturn newLumberjackWriteSyncer(&lumberjack.Logger{\n\t\tFilename:  path.Join(directory, \"output.log\"),\n\t\tMaxSize:   100, //megabytes\n\t\tMaxAge:    7,   //days\n\t\tLocalTime: true,\n\t\tCompress:  false,\n\t})\n}\n\nfunc newZapLogger(isProduction bool, output zapcore.WriteSyncer) (*zap.Logger, *zap.AtomicLevel) {\n\tencCfg := zapcore.EncoderConfig{\n\t\tTimeKey:        \"@timestamp\",\n\t\tLevelKey:       \"level\",\n\t\tNameKey:        \"logger\",\n\t\tCallerKey:      \"caller\",\n\t\tMessageKey:     \"msg\",\n\t\tStacktraceKey:  \"stacktrace\",\n\t\tEncodeCaller:   zapcore.ShortCallerEncoder,\n\t\tEncodeDuration: zapcore.NanosDurationEncoder,\n\t\tEncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {\n\t\t\tenc.AppendString(t.Format(\"2006-01-02 15:04:05.000\"))\n\t\t},\n\t}\n\n\tvar encoder zapcore.Encoder\n\tdyn := zap.NewAtomicLevel()\n\tif isProduction {\n\t\tdyn.SetLevel(zap.InfoLevel)\n\t\tencCfg.EncodeLevel = zapcore.LowercaseLevelEncoder\n\t\tencoder = zapcore.NewConsoleEncoder(encCfg) // zapcore.NewJSONEncoder(encCfg)\n\t} else {\n\t\tdyn.SetLevel(zap.DebugLevel)\n\t\tencCfg.EncodeLevel = zapcore.LowercaseColorLevelEncoder\n\t\tencoder = zapcore.NewConsoleEncoder(encCfg)\n\t}\n\n\treturn zap.New(zapcore.NewCore(encoder, output, dyn), zap.AddCaller()), &dyn\n}\n\ntype lumberjackWriteSyncer struct {\n\t*lumberjack.Logger\n\tbuf       *bytes.Buffer\n\tlogChan   chan []byte\n\tcloseChan chan interface{}\n\tmaxSize   int\n}\n\nfunc newLumberjackWriteSyncer(l *lumberjack.Logger) *lumberjackWriteSyncer {\n\tws := &lumberjackWriteSyncer{\n\t\tLogger:    l,\n\t\tbuf:       bytes.NewBuffer([]byte{}),\n\t\tlogChan:   make(chan []byte, 5000),\n\t\tcloseChan: make(chan interface{}),\n\t\tmaxSize:   1024,\n\t}\n\tgo ws.run()\n\treturn ws\n}\n\nfunc (l *lumberjackWriteSyncer) run() {\n\tticker := time.NewTicker(1 * time.Second)\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tif l.buf.Len() > 0 {\n\t\t\t\tl.sync()\n\t\t\t}\n\t\tcase bs := <-l.logChan:\n\t\t\t_, err := l.buf.Write(bs)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif l.buf.Len() > l.maxSize {\n\t\t\t\tl.sync()\n\t\t\t}\n\t\tcase <-l.closeChan:\n\t\t\tl.sync()\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (l *lumberjackWriteSyncer) Stop() {\n\tclose(l.closeChan)\n}\n\nfunc (l *lumberjackWriteSyncer) Write(bs []byte) (int, error) {\n\tb := make([]byte, len(bs))\n\tfor i, c := range bs {\n\t\tb[i] = c\n\t}\n\tl.logChan <- b\n\treturn 0, nil\n}\n\nfunc (l *lumberjackWriteSyncer) Sync() error {\n\treturn nil\n}\n\nfunc (l *lumberjackWriteSyncer) sync() error {\n\tdefer l.buf.Reset()\n\t_, err := l.Logger.Write(l.buf.Bytes())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/model/archive.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage model\n\n// YearArchive 归档\ntype YearArchive struct {\n\tYear int `yaml:\"year\"`\n\n\tMonthArchives []*MonthArchive `yaml:\"month_archive\"`\n}\n\ntype MonthArchive struct {\n\tMonth int `yaml:\"month\"`\n\n\tPosts []*Post `yaml:\"posts\"`\n}\n"
  },
  {
    "path": "src/model/friend.go",
    "content": "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",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage model\n\nimport \"time\"\n\n// 文章\ntype Post struct {\n\tContent string `yaml:\"content\"`\n\t*Meta\n}\n\ntype Meta struct {\n\tTitle   string   `yaml:\"title\"`\n\tPath    string   `yaml:\"path\"`\n\tPubTime string   `yaml:\"pub_time\"`\n\tTags    []string `yaml:\"tags\"`\n\n\tPostTime time.Time `yaml:\"post_time\"`\n}\n"
  },
  {
    "path": "src/model/tag.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: tk103331\ttk103331@gmail.com\npackage model\n\n// 标签\ntype Tag struct {\n\tName  string  `yaml:\"name\"`\n\tPosts []*Post `yaml:\"posts\"`\n}\n"
  },
  {
    "path": "src/route/mux.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage route\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n)\n\nfunc HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {\n\tDefaultBlogMux.HandleFunc(pattern, handler)\n}\n\n// BlogMux 路由处理器，扩展http.ServeMux\ntype BlogMux struct {\n\t*http.ServeMux\n}\n\n// DefaultBlogMux 默认路由处理器\nvar DefaultBlogMux = NewBlogMux()\n\nfunc NewBlogMux() *BlogMux {\n\treturn &BlogMux{ServeMux: http.DefaultServeMux}\n}\n\n// ServeHTTP 路由分发方法，封装 http.DefaultServeMux.ServeHTTP()\nfunc (this *BlogMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\t// 创建上下文，并写入start_time\n\tctx := context.WithValue(r.Context(), \"start_time\", time.Now())\n\t// 使用上下文\n\tr = r.WithContext(ctx)\n\t// 调用http.DefaultServeMux的路由分发方法\n\tthis.ServeMux.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "src/util/file.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage util\n\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\n// Exist 检查文件或目录是否存在\n// 如果由 filename 指定的文件或目录存在则返回 true，否则返回 false\nfunc Exist(filename string) bool {\n\t_, err := os.Stat(filename)\n\treturn err == nil || os.IsExist(err)\n}\n\n// ScanDir 列出指定路径中的文件和目录\n// 如果目录不存在，则返回空 slice\nfunc ScanDir(directory string) []string {\n\tfile, err := os.Open(directory)\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\tnames, err := file.Readdirnames(-1)\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\treturn names\n}\n\n// IsDir 判断给定文件名是否是一个目录\n// 如果文件名存在并且为目录则返回 true。如果 filename 是一个相对路径，则按照当前工作目录检查其相对路径。\nfunc IsDir(filename string) bool {\n\treturn isFileOrDir(filename, true)\n}\n\n// IsFile 判断给定文件名是否为一个正常的文件\n// 如果文件存在且为正常的文件则返回 true\nfunc IsFile(filename string) bool {\n\treturn isFileOrDir(filename, false)\n}\n\n// Filename returns the filename except ext\nfunc Filename(file string) string {\n\tif file == \"\" {\n\t\treturn \"\"\n\t}\n\n\tpos := strings.LastIndex(file, \".\")\n\treturn file[:pos]\n}\n\n// 判断是文件还是目录，根据decideDir为true表示判断是否为目录；否则判断是否为文件\nfunc isFileOrDir(filename string, decideDir bool) bool {\n\tfileInfo, err := os.Stat(filename)\n\tif err != nil {\n\t\treturn false\n\t}\n\tisDir := fileInfo.IsDir()\n\tif decideDir {\n\t\treturn isDir\n\t}\n\treturn !isDir\n}\n"
  },
  {
    "path": "src/util/util.go",
    "content": "package util\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n)\n\n// Contain 判断obj是否在target中，target支持的类型arrary,slice,map\nfunc Contain(obj interface{}, target interface{}) (bool, error) {\n\ttargetValue := reflect.ValueOf(target)\n\tswitch reflect.TypeOf(target).Kind() {\n\tcase reflect.Slice, reflect.Array:\n\t\tfor i := 0; i < targetValue.Len(); i++ {\n\t\t\tif targetValue.Index(i).Interface() == obj {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\tcase reflect.Map:\n\t\tif targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, errors.New(\"not in array\")\n}\n"
  },
  {
    "path": "src/vendor/manifest",
    "content": "{\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://github.com/PuerkitoBio/goquery\",\n\t\t\t\"revision\": \"1a71cc719d0d5b9e4f14ae072bef08b576b0bab5\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/andybalholm/cascadia\",\n\t\t\t\"repository\": \"https://github.com/andybalholm/cascadia\",\n\t\t\t\"revision\": \"349dd0209470eabd9514242c688c403c0926d266\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/go-chinese-site/cfg\",\n\t\t\t\"repository\": \"https://github.com/go-chinese-site/cfg\",\n\t\t\t\"revision\": \"844c2049bca3b3f99bae93339d7c5f417ed0f3ef\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/go-sql-driver/mysql\",\n\t\t\t\"repository\": \"https://github.com/go-sql-driver/mysql\",\n\t\t\t\"revision\": \"fade21009797158e7b79e04c340118a9220c6f9e\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/pkg/errors\",\n\t\t\t\"repository\": \"https://github.com/pkg/errors\",\n\t\t\t\"revision\": \"f15c970de5b76fac0b59abb32d62c17cc7bed265\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/robfig/cron\",\n\t\t\t\"repository\": \"https://github.com/robfig/cron\",\n\t\t\t\"revision\": \"736158dc09e10f1911ca3a1e1b01f11b566ce5db\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/russross/blackfriday\",\n\t\t\t\"repository\": \"https://github.com/russross/blackfriday\",\n\t\t\t\"revision\": \"6d1ef893fcb01b4f50cb6e57ed7df3e2e627b6b2\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/sourcegraph/annotate\",\n\t\t\t\"repository\": \"https://github.com/sourcegraph/annotate\",\n\t\t\t\"revision\": \"f4cad6c6324d3f584e1743d8b3e0e017a5f3a636\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"github.com/sourcegraph/syntaxhighlight\",\n\t\t\t\"repository\": \"https://github.com/sourcegraph/syntaxhighlight\",\n\t\t\t\"revision\": \"bd320f5d308e1a3c4314c678d8227a0d72574ae7\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"go.uber.org/atomic\",\n\t\t\t\"repository\": \"https://github.com/uber-go/atomic\",\n\t\t\t\"revision\": \"54f72d32435d760d5604f17a82e2435b28dc4ba5\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"go.uber.org/multierr\",\n\t\t\t\"repository\": \"https://github.com/uber-go/multierr\",\n\t\t\t\"revision\": \"fb7d312c2c04c34f0ad621048bbb953b168f9ff6\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"go.uber.org/zap\",\n\t\t\t\"repository\": \"https://github.com/uber-go/zap\",\n\t\t\t\"revision\": \"35aad584952c3e7020db7b839f6b102de6271f89\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"golang.org/x/net/html\",\n\t\t\t\"repository\": \"https://github.com/golang/net\",\n\t\t\t\"revision\": \"0a9397675ba34b2845f758fe3cd68828369c6517\",\n\t\t\t\"branch\": \"master\",\n\t\t\t\"path\": \"/html\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"gopkg.in/mgo.v2\",\n\t\t\t\"repository\": \"https://gopkg.in/mgo.v2\",\n\t\t\t\"revision\": \"3f83fa5005286a7fe593b055f0d7771a7dce4655\",\n\t\t\t\"branch\": \"v2\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"gopkg.in/natefinch/lumberjack.v2\",\n\t\t\t\"repository\": \"https://gopkg.in/natefinch/lumberjack.v2\",\n\t\t\t\"revision\": \"a96e63847dc3c67d17befa69c303767e2f84e54f\",\n\t\t\t\"branch\": \"master\"\n\t\t},\n\t\t{\n\t\t\t\"importpath\": \"gopkg.in/yaml.v2\",\n\t\t\t\"repository\": \"https://gopkg.in/yaml.v2\",\n\t\t\t\"revision\": \"eb3733d160e74a9c7e442f435eb3bea458e1d19f\",\n\t\t\t\"branch\": \"v2\"\n\t\t}\n\t]\n}"
  },
  {
    "path": "src/view/template.go",
    "content": "// Copyright 2017 The StudyGolang Authors. All rights reserved.\n// Use of this source code is governed by a MIT-style\n// license that can be found in the LICENSE file.\n// https://studygolang.com\n// Author: polaris\tpolaris@studygolang.com\n\npackage view\n\nimport (\n\t\"config\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"global\"\n)\n\n// funcMap is the customize template functions\nvar funcMap = template.FuncMap{\n\t\"noescape\": func(s string) template.HTML {\n\t\treturn template.HTML(s)\n\t},\n\t\"formatTime\": func(t time.Time, layout string) string {\n\t\treturn t.Format(layout)\n\t},\n}\n\n// Render 渲染模板并输出\nfunc Render(w http.ResponseWriter, r *http.Request, htmlFile string, data map[string]interface{}) {\n\tif data == nil {\n\t\tdata = make(map[string]interface{})\n\t}\n\tdata[\"app\"] = global.App\n\tdata[\"site_name\"] = config.YamlConfig.Get(\"setting.site_name\").String()\n\tdata[\"title\"] = config.YamlConfig.Get(\"setting.title\").String()\n\tdata[\"subtitle\"] = config.YamlConfig.Get(\"setting.subtitle\").String()\n\n\t// 加载布局模板layout.html\n\ttpl, err := template.New(\"layout.html\").Funcs(funcMap).\n\t\tParseFiles(global.App.TemplateDir+\"layout.html\", global.App.TemplateDir+htmlFile)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\t// 加载seo关键词和描述\n\tif seoTpl := tpl.Lookup(\"seo\"); seoTpl == nil {\n\t\tseoKeywords := config.YamlConfig.Get(\"seo.keywords\").String()\n\t\tseoDescription := config.YamlConfig.Get(\"seo.description\").String()\n\n\t\ttpl.Parse(`{{define \"seo\"}}\n\t\t\t<meta name=\"keywords\" content=\"` + seoKeywords + `\">\n\t\t\t<meta name=\"description\" content=\"` + seoDescription + `\">\n\t\t{{end}}`)\n\t}\n\tstartTime := r.Context().Value(\"start_time\").(time.Time)\n\tdata[\"response_time\"] = time.Since(startTime)\n\n\tw.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\tw.WriteHeader(http.StatusOK)\n\t// 渲染模板，并输出到w\n\terr = tpl.Execute(w, data)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "static/css/main.css",
    "content": "*{margin:0;padding:0}\nhtml,body{height:100%}\nbody{background:#ddd;color:#666;font-size:14px;font-family:\"-apple-system\",\"Open Sans\",\"HelveticaNeue-Light\",\"Helvetica Neue Light\",\"Helvetica Neue\",Helvetica,Arial,sans-serif}\n::selection,::-moz-selection,::-webkit-selection{background-color:#2479CC;color:#eee}\nh1{font-size:2em}\nh3{font-size:1.3em}\nh4{font-size:1.1em}\na:link, a:visited, a:active {color: #076dd0; text-decoration: none; }\na:hover {text-decoration: underline; }\narticle{padding:30px 0;position:relative;overflow: hidden;}\n.container{max-width:1600px;min-height:100%;position:relative}\n.global-tips{display:none}\n.left-col{background-color:#007b8b;background-image:url(/static/image/left-bg.png);background-size:cover;height:100%;position:fixed;width:270px}\n.mid-col{background:#fff;left:0;margin-left:270px;min-height:100%;position:absolute;right:0}\narticle .post-footer{color:#555;float:right;font-size:.8em;line-height:2;position:relative;text-align:right;width:auto}\narticle .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}\narticle .entry-content{color:#444;border-bottom:1px solid #ddd;}\narticle h1.title{color:#333;font-size:2em;font-weight:300;line-height:35px;margin-bottom:25px}article .entry-content p{margin-top:15px}\narticle h1.title a{color:#333;transition:color .3s}\n.mid-col .mid-col-container{padding:0 70px 0 40px;}\narticle .meta .date,article .meta .comment,article .meta .tags{position:relative}\narticle .entry-content a:hover{text-decoration:underline}\narticle h1.title a:hover{color:#076dd0}\n#header{border-bottom:none;height:auto;line-height:30px;margin-left:50px;padding:30px 0;width:100%}\n#main-nav{margin-left:0}\n#main-nav,#sub-nav{float:none;margin-top:15px}#sub-nav{position:relative}\n#content{margin:0 auto;width:100%}\n#footer{border-top:1px solid #ddd;font-size:.9em;line-height:2.2;padding:15px 70px 15px 40px;text-align:center;width:auto}\n#header a{color:#fff;text-shadow:0 1px #666;transition:color .3s}\n#header h1{float:none;font-weight:300;font-size:30px}\n#main-nav ul li{display:block;margin-left:0;position:relative}\n#header .subtitle{color:#fff}\n#sub-nav .social{margin-bottom:10px}\n#footer .beian{color:#666}\n#header a:hover{color:#ccc}\n#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}\n#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}\n#sub-nav .social a:hover{opacity:1;}\n#sub-nav .social a:first-of-type{margin-left:0}\n#sub-nav .social a:last-of-type{margin-right:0}\n@media screen and (max-width:1024px){\n\tarticle{padding-bottom:15px}\n\t.left-col{width:210px}\n\t.mid-col{margin-left:210px}\n\t.mid-col .mid-col-container{padding:0 20px}\n\t#header{margin-left:30px}\n\t#footer{padding:15px 20px}\n\tarticle h1.title,article .entry-content{margin-left:0}\n}\n@media screen and (max-width:640px){\n\t#header{margin-left:0;padding:20px 0;text-align:center}\n\t#main-nav{margin-top:10px}\n\t#main-nav ul li{display:inline;margin:0 10px;text-align:center}\n\t#header .profilepic a{height:56px;left:12px;margin:0;position:absolute;top:12px;width:56px}\n\t#sub-nav .social,#sub-nav .social a{margin-bottom:0}\n\tarticle{padding:20px 0}\n\t.left-col{background-image:none;position:relative;width:100%}\n\t.mid-col{float:none;margin-left:0;width:100%}\n\tarticle .meta{margin-bottom:10px;position:static;width:auto}\n\t.mid-col .mid-col-container{padding:0 10px}\n\t.mid-col article .meta{float:none;overflow:hidden}\n\tarticle .meta .date,article .meta .comment,article .meta .tags{display:inline;margin-right:5px;padding-left:0}\n\tarticle .meta .date{margin-right:30px}#footer{padding:15px 10px}\n\t#sub-nav .social a{opacity:1}\n\t#content #toc-container,#content #toc{float:none}\n\t#content #toc{margin:0;max-width:100%}\n}"
  },
  {
    "path": "static/css/post.css",
    "content": "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}\narticle button{margin-top:0}\narticle input.runcode:hover,article input.runcode:focus,article input.runcode:active,article button:hover,article button:focus,article button:active{background:#f6ad08}\narticle strong{font-weight:700}\narticle em{font-style:italic}\narticle blockquote{background-color:#f8f8f8;border-left:5px solid #2479CC;margin-top:10px;overflow:hidden;padding:15px 20px}\narticle 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}\narticle 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}\narticle img{border:1px solid #ccc;display:block;margin:10px 0 5px;max-width:100%;padding:0}\narticle table{border:0;border-collapse:collapse;border-spacing:0}\narticle pre code{background-color:transparent;border-radius:0 0 0 0;border:0;display:block;font-size:100%;margin:0;padding:0;position:relative}\narticle table th,article table td{border:0}\narticle table th{border-bottom:2px solid #848484;padding:6px 20px;text-align:left}\narticle table td{border-bottom:1px solid #d0d0d0;padding:6px 20px}\narticle .copyright-info{font-size:.8em}\narticle .expire-tips{background-color:#ffffc0;border:1px solid #e2e2e2;border-left:5px solid #fff000;color:#333;font-size:15px;padding:5px 10px}\narticle .post-info,article .entry-content .date{font-size:14px}\narticle img.loaded{height:auto!important}\narticle .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}\narticle 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}\narticle .entry-content .date{color:#666}\narticle .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}\n.total_thread{line-height:1.6}\n.page-navi{line-height:20px;overflow:hidden;padding:20px 0;position:relative;width:100%}\narticle.post-search{padding-bottom:0}article .entry-content ul,article .entry-content ol,article .entry-content dl{margin-left:25px}\n.page-navi .prev{float:left}\n.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}\n#toc{border:1px solid #e2e2e2;font-size:14px;margin:0 0 15px 20px;max-width:260px;min-width:120px;padding:6px}\n#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}\n\n.post-tag:link, .post-tag:visited {padding: 3px 5px; line-height: 100%; background-color: #f0f0f0; border-radius: 10px; margin: 0px 2px; display: inline-block; }\n\n.post-tag:hover {background-color: #99a; color: #fff; text-decoration: none; }"
  },
  {
    "path": "template/theme/default/about.html",
    "content": "{{define \"title\"}}关于{{end}}\n{{define \"content\"}}\n<article class=\"post\">\n\t<h1>关于</h1>\n\t<br>\n\t<div class=\"entry-content\">\n\t\t{{noescape .about.Content}}\n\t</div>\n\t<div class=\"post-footer\">\n\n\t\t<p class=\"copyright-info\">本站使用「<a href=\"http://creativecommons.org/licenses/by/4.0/deed.zh\" target=\"_blank\">署名 4.0 国际</a>」创作共享协议</p>\n\t</div>\n</article>\n{{end}}"
  },
  {
    "path": "template/theme/default/archives.html",
    "content": "{{define \"title\"}}归档{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1 class=\"title\">归档</h1>\n\t\t<div class=\"entry-content\" style=\"border-bottom: none;\">\n\t\t{{range .archives}}\n\t\t\t<h3>{{.Year}} 年</h3>\n\t\t\t{{range .MonthArchives}}\n\t\t\t<ul>\n\t\t\t\t{{range .Posts}}\n\t\t\t\t<li><a href=\"/post/{{.Path}}\">{{.Title}}</a>&nbsp;<span class=\"date\">({{formatTime .PostTime \"Jan 02, 2006\"}})</span></li>\n\t\t\t\t{{end}}\n\t\t\t</ul>\n\t\t\t{{end}}\n\t\t{{end}}\n\t\t</div>\n\t</article>\n{{end}}"
  },
  {
    "path": "template/theme/default/friends.html",
    "content": "{{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=\"list-style:none;\">\n\t\t{{range .friends}}\n\t\t\t<li style=\"float:left;margin:20px\"><a href=\"{{.Link}}\" title=\"{{.Name}}\"><img style=\"height:64px;\" src=\"{{.Logo}}\" alt=\"{{.Name}}\" /></a></li>\n\t\t{{end}}\n\t\t</ul>\n\t\t</div>\n\t</article>\n{{end}}"
  },
  {
    "path": "template/theme/default/index.html",
    "content": "{{define \"title\"}}首页{{end}}\n{{define \"content\"}}\n\t{{range .posts}}\n\t<article class=\"post post-list\" style=\"border-bottom: 1px solid #ddd;\">\n\t\t<h1 class=\"title\"><a href=\"/post/{{.Path}}\">{{.Title}}</a></h1>\n\t\t<div class=\"meta\">\n\t\t\t<div class=\"date\"><i class=\"fa fa-calendar\" aria-hidden=\"true\"></i> {{.PubTime}}</div>\n\t\t</div>\n\t\t<div class=\"entry-content\" style=\"border-bottom: none;\">\n\t\t\t<p>{{.Content}}</p>\n\t\t\t<p><a href=\"/post/{{.Path}}\" class=\"more-link\">继续阅读 »</a></p>\n\t\t</div>\n\t</article>\n\t{{end}}\n\t<nav class=\"page-navi\"><div class=\"center\"><a href=\"/archives\">更多博文</a></div></nav>\n{{end}}"
  },
  {
    "path": "template/theme/default/layout.html",
    "content": "<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width,minimum-scale=1,initial-scale=1\">\n    <meta name=\"format-detection\" content=\"telephone=no\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <title>{{template \"title\" .}} - {{.site_name}}</title>\n    <meta name=\"theme-color\" content=\"#007b8b\">\n    <meta name=\"mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"application-name\" content=\"{{.site_name}}\">\n    <meta name=\"msapplication-starturl\" content=\"https://studygolang.com\">\n    <meta name=\"msapplication-navbutton-color\" content=\"#007b8b\">\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-title\" content=\"{{.site_name}}\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n    <link rel=\"apple-touch-icon\" href=\"logo\">\n    <link rel=\"alternate\" type=\"application/rss+xml\" title=\"RSS 2.0\" href=\"/rss.html\">\n    {{template \"seo\" .}}\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/css/main.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/css/post.css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/css/atelier-savanna-light.min.css\">\n    <link href=\"https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n</head>\n<body>\n    <div class=\"container\">\n        <div class=\"left-col\">\n            <div class=\"intrude-less\">\n                <header id=\"header\" class=\"inner\">\n                    <div class=\"profilepic\"><a href=\"/\" aria-label=\"Home\"></a></div>\n                    <h1><a href=\"/\">{{.title}}</a></h1>\n                    <p class=\"subtitle\">{{.subtitle}}</p>\n                    <nav id=\"main-nav\">\n                        <ul>\n                            <li><a href=\"/\"><span>首页</span></a></li>\n                            <li><a href=\"/archives\"><span>归档</span></a></li>\n                            <li><a href=\"/tags\"><span>标签</span></a></li>\n                            <li><a href=\"/friends\"><span>友链</span></a></li>\n                            <li><a href=\"/about\"><span>关于</span></a></li>\n                        </ul>\n                    </nav>\n                    <nav id=\"sub-nav\">\n                        <div class=\"social\">\n                            <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>\n                            <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>\n                        </div>\n                    </nav>\n                </header>\n            </div>\n        </div>\n        <div class=\"mid-col\">\n            <div class=\"mid-col-container\">\n                <div id=\"content\" class=\"inner\">\n                    {{template \"content\" .}}\n                </div>\n            </div>\n            <div style=\"clear:both;\"></div>\n            <footer id=\"footer\" class=\"inner\">\n                ©&nbsp;{{.app.Copyright}}&nbsp;-&nbsp;{{.site_name}}&nbsp;Powered by&nbsp;<a href=\"https://github.com/go-chinese-site/dreamgo\" target=\"_blank\">DreamGo</a>\n                <div style=\"color: #ccc; font-size: 86%;\">\n                    VERSION: {{.app.Version}}&nbsp;\n                    <span>·</span>\n                    &nbsp;{{.response_time}}\n                </div>\n            </footer>\n        </div>\n    </div>\n</body>\n</html>"
  },
  {
    "path": "template/theme/default/single.html",
    "content": "{{define \"title\"}}{{.post.Title}}{{end}}\n{{define \"content\"}}\n<article class=\"post\">\n\t<div class=\"entry-content\">\n\t\t{{noescape .post.Content}}\n\t\t<p>--EOF--</p>\n\t</div>\n\t<div class=\"post-footer\">\n\t\t<p class=\"post-info\">posted&nbsp;@&nbsp;<span class=\"date\">{{.post.PubTime}}</span>\n\t\t\t{{if .post.Tags}}\n\t\t\t\t，并被添加「\n\t\t\t\t{{range .post.Tags}}\n\t\t\t\t\t<a href=\"/tag/{{.}}\" class=\"post-tag\">{{.}}</a> \n\t\t\t\t{{end}}\n\t\t\t\t」\n\t\t\t{{end}}\n\t\t\t标签。<a href=\"/post/about-dreamgo.md\">查看本文 Markdown 版本 »</a>\n\t\t</p>\n\t\t<p class=\"copyright-info\">本站使用「<a href=\"http://creativecommons.org/licenses/by/4.0/deed.zh\" target=\"_blank\">署名 4.0 国际</a>」创作共享协议</p>\n\t</div>\n</article>\n{{end}}"
  },
  {
    "path": "template/theme/default/tag.html",
    "content": "{{define \"title\"}}标签{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1 class=\"title\">标签：{{.tag.Name}}</h1>\n\n\t\t<div class=\"entry-content\" style=\"border-bottom: none;\">\n\n\t\t相关文章（<strong>{{len .tag.Posts}}</strong>）：\n\t\t<br/>\n\t\t<ul>\n\t\t{{range .tag.Posts}}\n\t\t\t<li><a href=\"/post/{{.Path}}\">{{.Title}}</a>&nbsp;<span class=\"date\">({{formatTime .PostTime \"Jan 02, 2006\"}})</span></li>\n\t\t{{end}}\n\t\t</ul>\n\t\t</div>\n\t</article>\n{{end}}"
  },
  {
    "path": "template/theme/default/tags.html",
    "content": "{{define \"title\"}}标签--{{.tag.Name}}{{end}}\n{{define \"content\"}}\n\t<article class=\"post post-list\">\n\t\t<h1 class=\"title\">标签</h1>\n\t\t<div class=\"entry-content\" style=\"border-bottom: none;\">\n\t\t<ul>\n\t\t{{range .tags}}\n\t\t\t<li><a href=\"/tag/{{.Name}}\">{{.Name}}（{{len .Posts}}）</a></li>\n\t\t{{end}}\n\t\t</ul>\n\t\t</div>\n\t</article>\n{{end}}"
  }
]