• 搭建个人博客,Docsify+Github webhook+JGit解决方案


    一开始博客使用的 Halo,发现问题比较多啊,时不时的莫名其妙主题各种报错,有时候还要升级,麻烦的要死,于是就想弄简单点。

    这两天抽空反复倒腾了一遍,不小心还把镜像给尼玛删了,发的文章都没了,痛定思痛,要做改变!

    众所周知,我懒出名了,我觉得这种事情你不要老是让我操心啊,最好是一年都不要动一下才好,这搞的跟什么一样。

    研究一会儿,最终还是决定 docsify+github 来弄,初步的想法是本地写好 MD 文件直接 push 到 github上,然后触发 github 的webhook,触发脚本去 pull 代码到服务器上。

    这样的话还有点想象空间,以后可以省去一大部分同步文章的工作,都可以出发回调去通过 API 同步,不过暂时还没有调研这些平台是否能支持,不过应该问题不大。

    试试解决方案可不可行吧,我觉得很 nice。

    docsify 搭建安装

    首先安装 docsify-cli 工具

    npm i docsify-cli -g

    然后进入自己的目录,初始化

    docsify init ./

    这样就差不多了,多了几个文件,简单修改一下 index.html,配置下名字和代码仓库的信息,开启下左边的侧边栏。

    同时补充一点插件,都从网上搂的。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Document</title>
      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
      <meta name="description" content="Description">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
      <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
    </head>
    <body>
      <div id="app"></div>
      <script>
        window.$docsify = {
          name: '艾小仙',
          repo: 'https://github.com/irwinai/JavaInterview.git',
          loadSidebar: true,
          autoHeader: true,
          subMaxLevel: 3,
          sidebarDisplayLevel: 1, // set sidebar display level
          count:{
            countable:true,
            fontsize:'0.9em',
            color:'rgb(90,90,90)',
            language:'chinese'
          },
          search: {
            maxAge: 86400000, // Expiration time, the default one day
            paths: [], // or 'auto'
            placeholder: '开始搜索',
            noData: '啥也没有!'
          },
          copyCode: {
            buttonText : '点击复制',
            errorText  : '错误',
            successText: '复制成功'
          },
          footer: {
            copy: '<span>艾小仙 © 2022</span>',
            auth: '赣ICP备2021008029号-1',
            pre: '<hr/>',
            style: 'text-align: right;',
            class: 'className'
          }
        }
      </script>
      <!-- Docsify v4 -->
      <script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
      <!-- 搜索 -->
      <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script>
      <!-- 图片放大缩小支持 -->
      <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script>
      <!-- 拷贝文字内容 -->
      <script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
      <!-- 字数插件 -->
      <script src="https://cdn.jsdelivr.net/npm/docsify-count@latest/dist/countable.min.js"></script>
      <!-- 分页导航 -->
      <script src="//unpkg.com/docsify-pagination/dist/docsify-pagination.min.js"></script>
      <!-- 侧边栏扩展与折叠 -->
      <script src="//cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/docsify-sidebar-collapse.min.js"></script>
      <!-- 页脚信息 -->
      <script src="//unpkg.com/docsify-footer-enh/dist/docsify-footer-enh.min.js"></script>
      <!-- GitTalk评论 -->
      <!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/gitalk/dist/gitalk.css">
      <script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/gitalk.min.js"></script>
      <script src="//cdn.jsdelivr.net/npm/gitalk/dist/gitalk.min.js"></script> -->
    </body>
    </html>

    然后运行,本地会启动 http://localhost:3000 ,直接看看效果

    docsify serve

    大概就这个样子了

    侧边栏还没有,使用命令生成一下,会自动帮我们根据目录创建一个 _sidebar.md 文件,也就是我们的侧边栏了。

    docsify generate .

    然后再看看效果怎么样,差不多就这样,需要注意的是文件名不能有空格,否则生成的侧边栏目录会有问题,需要修改一下文件名使用中划线或者下换线替换空格(我无法理解为什么不能用空格)。

    多级目录生成问题

    另外还有一个问题是无法生成多级目录,也就是不能超过二级目录,这个我自己随便改了一下。

    去 docsify-cli 官方 git 仓库下载源码,然后把这个丢进去,在目录下执行 node generate.js 就可以,底部测试目录改成自己的目录。

    'use strict'
    
    const fs = require('fs')
    const os = require('os')
    const {cwd, exists} = require('../util')
    const path = require('path')
    const logger = require('../util/logger')
    const ignoreFiles = ['_navbar', '_coverpage', '_sidebar']
    
    // eslint-disable-next-line
    function test (path = '', sidebar) {
      // 获取当前目录
      const cwdPath = cwd(path || '.')
    
      // console.log('cwdPath', cwdPath, !!exists(cwdPath));
    
      // console.log('///', cwdPath, path, cwd(path || '.'))
      if (exists(cwdPath)) {
        if (sidebar) {
          const sidebarPath = cwdPath + '/' + sidebar || '_sidebar.md';
    
          if (!exists(sidebarPath)) {
            genSidebar(cwdPath, sidebarPath)
            logger.success(`Successfully generated the sidebar file '${sidebar}'.`)
            return true
          }
    
          logger.error(`The sidebar file '${sidebar}' already exists.`)
          process.exitCode = 1
          return false
        }
        return false;
      }
    
      logger.error(`${cwdPath} directory does not exist.`)
    }
    
    let tree = '';
    function genSidebar(cwdPath, sidebarPath) {
      // let tree = '';
      let lastPath = ''
      let nodeName = ''
      let blankspace = '';
      let test = 0;
    
      const files = getFiles(cwdPath);
      console.log(JSON.stringify(files));
      getTree(files);
    
      fs.writeFile(sidebarPath, tree, 'utf8', err => {
        if (err) {
          logger.error(`Couldn't generate the sidebar file, error: ${err.message}`)
        }
      })
    
      return;
    
      getDirFiles(cwdPath, function (pathname) {
        path.relative(pathname, cwdPath) // 找cwdPath的相对路径
        pathname = pathname.replace(cwdPath + '/', '')
        let filename = path.basename(pathname, '.md') // 文件名
        let splitPath = pathname.split(path.sep) // 路径分割成数组
        let blankspace = '';
    
        if (ignoreFiles.indexOf(filename) !== -1) {
          return true
        }
    
        nodeName = '- [' + toCamelCase(filename) + '](' + pathname + ')' + os.EOL
    
        if (splitPath.length > 1) {
          if (splitPath[0] !== lastPath) {
            lastPath = splitPath[0]
            tree += os.EOL + '- ' + toCamelCase(splitPath[0]) + os.EOL
          }
          tree += '  ' + nodeName
          // console.error('tree=====', tree, splitPath, splitPath.length);
        } else {
          if (lastPath !== '') {
            lastPath = ''
            tree += os.EOL
          }
    
          tree += nodeName
        }
      })
      fs.writeFile(sidebarPath, tree, 'utf8', err => {
        if (err) {
          logger.error(`Couldn't generate the sidebar file, error: ${err.message}`)
        }
      })
    }
    
    function getFiles (dir) {
      // let path = require('path');
      // let fs = require('fs');
      let rootDir = dir;
      var filesNameArr = []
      let cur = 0
      // 用个hash队列保存每个目录的深度
      var mapDeep = {}
      mapDeep[dir] = 0
      // 先遍历一遍给其建立深度索引
      function getMap(dir, curIndex) {
        var files = fs.readdirSync(dir) //同步拿到文件目录下的所有文件名
        files.map(function (file) {
          //var subPath = path.resolve(dir, file) //拼接为绝对路径
          var subPath = path.join(dir, file) //拼接为相对路径
          var stats = fs.statSync(subPath) //拿到文件信息对象
          // 必须过滤掉node_modules文件夹
          if (file != 'node_modules') {
            mapDeep[file] = curIndex + 1
            if (stats.isDirectory()) { //判断是否为文件夹类型
              return getMap(subPath, mapDeep[file]) //递归读取文件夹
            }
          }
        })
      }
      getMap(dir, mapDeep[dir])
      function readdirs(dir, folderName, myroot) {
        var result = { //构造文件夹数据
          path: dir,
          title: path.basename(dir),
          type: 'directory',
          deep: mapDeep[folderName]
        }
        var files = fs.readdirSync(dir) //同步拿到文件目录下的所有文件名
        result.children = files.map(function (file) {
          //var subPath = path.resolve(dir, file) //拼接为绝对路径
          var subPath = path.join(dir, file) //拼接为相对路径
          var stats = fs.statSync(subPath) //拿到文件信息对象
          if (stats.isDirectory()) { //判断是否为文件夹类型
            return readdirs(subPath, file, file) //递归读取文件夹
          }
          if (path.extname(file) === '.md') {
            const path =  subPath.replace(rootDir + '/', '');
            // console.log(subPath, rootDir, '========', path);
            return { //构造文件数据
              path: path,
              name: file,
              type: 'file',
              deep: mapDeep[folderName] + 1,
            }
          }
        })
        return result //返回数据
      }
      filesNameArr.push(readdirs(dir, dir))
      return filesNameArr
    }
    
    function getTree(files) {
      for (let i=0; i<files.length;i++) {
        const item = files[i];
        if (item) {
          if (item.deep === 0) {
            if (item.children) {
              getTree(item.children)
            }
          } else {
            let blankspace = ''
            for (let i = 1; i < item.deep; i++) {
              blankspace += '  '
            }
            // console.log('-' + blankspace + '-', item.deep)
            if (item.type === 'directory') {
              tree += os.EOL + blankspace + '- ' + toCamelCase(item.title) + os.EOL
            } else if (item.type === 'file') {
              tree += os.EOL + blankspace + '- [' + item.name + '](' + item.path + ')' + os.EOL
              // console.log('tree', tree);
            }
            if (item.children) {
              getTree(item.children)
            }
          }
        }
      }
    }
    
    function getDirFiles(dir, callback) {
      fs.readdirSync(dir).forEach(function (file) {
        let pathname = path.join(dir, file)
    
        if (fs.statSync(pathname).isDirectory()) {
          getDirFiles(pathname, callback)
        } else if (path.extname(file) === '.md') {
          callback(pathname)
        }
      })
    }
    
    function toCamelCase(str) {
      return str.replace(/\b(\w)/g, function (match, capture) {
        return capture.toUpperCase()
      }).replace(/-|_/g, ' ')
    }
    
    
    test("/Users/user/Documents/JavaInterview/", "sidebar.md");

    这样的话一切不就都好起来了吗?看看最终的效果。

    nginx 配置

    webhook 暂时还没弄,先手动 push 上去然后把文件都传到服务器上去,再设置一下 nginx 。

    server {
        listen 80;
        listen [::]:80;
        server_name aixiaoxian.vip;
        client_max_body_size 1024m;
        location / {
            root /你的目录;
            index index.html;
        }
    }
    
    server {
        listen       443 ssl;
        server_name  aixiaoxian.vip;
        root         /usr/share/nginx/html;
    
        ssl_certificate cert/aixiaoxian.pem;
        ssl_certificate_key cert/aixiaoxian.key;
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_prefer_server_ciphers on;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #表示使用的TLS协议的类型。
        include /etc/nginx/default.d/*.conf;
    
        location / {
            root /你的目录;
            index index.html;
        }
    }

    这时候你去 reload nginx 会发现网站可能 403 了,把你的 ng 文件第一行改了,甭管后面是啥,改成 root 完事儿。

    user root;

    这样就 OK 了。

    内网穿透

    然后,就需要搞一个 webhook 的程序了,但是在此之前,为了本地能测试 webhook 的效果,需要配置个内网穿透的程序,我们使用 ngrok 。

    先安装。

    brew install ngrok/ngrok/ngrok

    然后需要注册一个账号,这里没关系,直接 google 登录就好了,之后进入个人页面会提示你使用步骤。

    按照步骤来添加 token,然后映射 80 端口。

    如果后面发现 ngrok 命令找不到,可以去官网手动下一个文件,丢到 /usr/local/bin 目录中就可以了。

    成功之后可以看到 ngrok 的页面,使用他给我们提供的 Forwarding 地址,就是我们的公网地址。

    我随便弄了个 80 端口,这里用自己到时候项目的端口号映射就行了,后面我们改成 9000。

    webhook

    配置好之后,就去我们的 github 项目设置 webhook,使用上面得到的地址。

    这样就设置OK了,然后搞个 Java 程序去,监听 9000 端口,随便写个 Controller 。

    @PostMapping("/webhook")
    public void webhook(@RequestBody JsonNode jsonNode) {
    	System.out.println("json===" + jsonNode);
    }

    然后随便改点我们的文档,push 一下,收到了 webhook 的回调,其实我们根本不关注回调的内容,我们只要收到这个通知,就去触发重新拉取代码的指令就行。

    {
        "repository": {
            "id": 306793662,
            "name": "JavaInterview",
            "full_name": "irwinai/JavaInterview",
            "private": false,
            "owner": {
                "name": "irwinai",
                "email": "415586662@qq.com",
                "login": "irwinai"
            },
            "html_url": "https://github.com/irwinai/JavaInterview",
            "description": null,
            "fork": false
        },
        "pusher": {
            "name": "irwinai",
            "email": "415586662@qq.com"
        },
        "sender": {
            "login": "irwinai",
            "id": 4981449
        }
    }

    接着,我们实现代码逻辑,根据回调执行命令去拉取最新的代码到本地,为了能通过 Java 操作 Git,引入 JGit。

    <dependency>
      <groupId>org.eclipse.jgit</groupId>
      <artifactId>org.eclipse.jgit</artifactId>
      <version>5.13.1.202206130422-r</version>
    </dependency>

    为了简单,我每次都是重新 clone 仓库下来,正常来说应该第一次 clone,后面直接 pull 代码,这里为了省事儿,就先这样操作。

    @RestController
    public class WebhookController {
        private static final String REMOTE_URL = "https://github.com/irwinai/JavaInterview.git";
    
        @PostMapping("/webhook")
        public void webhook(@RequestBody JsonNode jsonNode) throws Exception {
            File localPath = new File("/Users/user/Downloads/TestGitRepository");
    
            // 不管那么多,先删了再说
            FileUtils.deleteDirectory(localPath);
    
            //直接 clone 代码
            try (Git result = Git.cloneRepository()
                    .setURI(REMOTE_URL)
                    .setDirectory(localPath)
                    .setProgressMonitor(new SimpleProgressMonitor())
                    .call()) {
                System.out.println("Having repository: " + result.getRepository().getDirectory());
            }
        }
    
        private static class SimpleProgressMonitor implements ProgressMonitor {
            @Override
            public void start(int totalTasks) {
                System.out.println("Starting work on " + totalTasks + " tasks");
            }
    
            @Override
            public void beginTask(String title, int totalWork) {
                System.out.println("Start " + title + ": " + totalWork);
            }
    
            @Override
            public void update(int completed) {
                System.out.print(completed + "-");
            }
    
            @Override
            public void endTask() {
                System.out.println("Done");
            }
    
            @Override
            public boolean isCancelled() {
                return false;
            }
        }
    }

    代码执行后已经是 OK 了,可以直接拉取到代码,那么至此,差不多已经 OK 了,后面把代码直接丢服务器上去跑着就拉倒了。

    服务器的问题

    好了,你以为到这里就结束了吗?年轻了,年轻了。。。

    这代码丢到服务器上跑会发现报错连不上 github,如果你是阿里云服务器的话。

    解决方案是找到 /etc/ssh/ssh_config ,删掉 GSSAPIAuthentication no 这行前面的注释,然后保存,你才会发现真的是能下载了。

    同时,nginx 我们映射另外一个域名作为回调的域名,这里需要主要以下你的 https 证书,因为我是免费版的,所以 https 这个域名无法生效,那 webhook 回调注意用 http 就行。

    server {
        listen 80;
        listen [::]:80;
        server_name test.aixiaoxian.vip;
        client_max_body_size 1024m;
        location / {
            proxy_pass http://127.0.0.1:9000;
            proxy_set_header HOST $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
    server {
        listen       443 ssl;
        server_name  test.aixiaoxian.vip;
        root         /usr/share/nginx/html;
    
        ssl_certificate cert/aixiaoxian.pem;
        ssl_certificate_key cert/aixiaoxian.key;
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_prefer_server_ciphers on;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #表示使用的TLS协议的类型。
        include /etc/nginx/default.d/*.conf;
    
        location / {
            proxy_pass http://127.0.0.1:9000;
            proxy_set_header HOST $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }

    OK,如果你用上面的方式还无法解决,可以换一种方式,直接通过 Java 直接 shell 脚本去处理。

    rm -rf /root/docs/JavaInterview
    git clone https://github.91chi.fun//https://github.com/irwinai/JavaInterview.git

    用脚本的目的是可以用加速的 git 地址,否则服务器访问经常会失败,这个找个插件就可以有加速地址了。

    public class ShellCommandsHelper extends CommandHelper {
        private static final File file = new File(FILE_PATH);
    
        @Override
        public void exec() {
            try {
                FileUtils.forceMkdir(file);
    
                log.info("Starting clone repository...");
    
                Process process = Runtime.getRuntime().exec("sh " + SHELL_PATH, null, file);
                int status = process.waitFor();
    
                if (status != 0) {
                    log.error("[ShellCommandsHelper] exec shell error {}", status);
                }
            } catch (Exception e) {
                log.error("[ShellCommandsHelper] exec shell error", e);
            }
        }
    
    }

    代码上传的问题

    好了,这样通过手动上传代码的方式其实已经可以用了,但是为了部署更方便一点,我建议安装一个插件 Alibaba Cloud Toolkit ,其他云服务器有的也有这种类型的插件。

    安装好插件之后会进入配置的页面,需要配置一个 accessKey 。

    去自己的阿里云账号下面配置。

    配置好了之后,点击 Tools-Deploy to xxx ,我是 ECS ,所以选择 ECS 服务器即可。

    然后在这里会自动加载出你的服务器信息,然后选择自己的部署目录,同时选择一个 Command 也就是执行命令。

    这个命令随便找个目录创建一个这个脚本,放那里就行了,最后点击 Run,代码就飞快的上传了。

    source /etc/profile
    killall -9 java
    nohup java -jar /root/tools/sync-tools-0.0.1-SNAPSHOT.jar > nohup.log 2>&1 &

    结束

    基本的工作已经做完了,那其实还有挺多细节问题没有处理的,比如失败重试、回调鉴权等等问题,这只是一个非常初级的版本。

    同步代码 git 地址: GitHub - irwinai/sync-tools

  • 相关阅读:
    推荐一个高质量专栏:「前端面试必备」
    精选大厂10道常考python面试题!
    中国业务型CDP白皮书 | 爱分析报告
    开漏输出,推挽输出,开集输出
    八股文(Web篇——网络通讯部分)第十二天
    多方面浅谈互联网技术
    Codeforces Round #476 (Div. 2)——D. Single-use Stones(二分做法)
    275. 传统文化青铜器主题网页 大学生期末大作业 Web前端网页制作 html+css
    【力扣】43. 字符串相乘(大数相乘)<模拟>
    mysq基础语句+高级操作(学这篇就够了)
  • 原文地址:https://blog.csdn.net/JavaShark/article/details/125413188