At the beginning of the blog, I used Halo, and I found that there were many problems. From time to time, I reported various errors in the inexplicable theme. Sometimes I had to upgrade, which was troublesome, so I wanted to make it simpler.

In the past two days, I took the time to go back and forth again and again. I accidentally deleted the mirror image to Nima, and the articles I posted are gone. After thinking about it, I need to make changes!

As we all know, I'm lazy and famous. I don't think you should always make me worry about this kind of thing. It's better not to move for a year. It's the same as what it does.

After researching for a while, I finally decided to use docsify+github to get it. The initial idea is to write the MD file locally and push it directly to github, then trigger the webhook of github and trigger the script to pull the code to the server.

博客

In this case, there is still a bit of room for imagination. In the future, a large part of the work of synchronizing articles can be saved, and all of them can call back and synchronize through the API. However, it has not yet been investigated whether these platforms can support it, but it should not be a big problem.

Is it possible to try the solution, I think it is very nice.

docsify build installation

First install the docsify-cli tool

 npm i docsify-cli -g

Then enter your own directory and initialize

 docsify init ./

This is almost the same. There are a few more files, simply modify index.html, configure the information of the name and code repository, and open the sidebar on the left.

At the same time, add some plug-ins, all from the Internet.

 <!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>艾小仙 &copy; 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>

Then run, it will start http://localhost:3000 locally, and see the effect directly

 docsify serve

Probably like this

There is no sidebar yet, use the command to generate it, it will automatically help us create a _sidebar.md file according to the directory, which is our sidebar.

 docsify generate .

Then look at the effect. It's almost like this. It should be noted that the file name cannot have spaces, otherwise the generated sidebar directory will have problems. You need to modify the file name to replace spaces with underscores or underlines (I Can't understand why spaces can't be used).

Multilevel directory generation problem

Another problem is that it is impossible to generate a multi-level directory, that is, it cannot exceed the second-level directory. I changed this at random.

Go to the official git repository of docsify-cli to download the source code, then put this in, execute node generate.js in the directory, and change the bottom test directory to your own directory.

 '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");

Wouldn't everything be fine then? Take a look at the final effect.

nginx configuration

The webhook has not been done yet, first push it manually, then upload the files to the server, and then set up 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;
    }
}

At this time, when you go to reload nginx, you will find that the website may be 403. Change the first line of your ng file, no matter what is behind it, change it to root and you are done.

 user root;

That's it.

Intranet penetration

Then, you need to create a webhook program, but before that, in order to test the effect of webhook locally, you need to configure an intranet penetration program, we use ngrok.

Install first.

 brew install ngrok/ngrok/ngrok

Then you need to register an account, it doesn't matter here, just log in directly with google, and then you will be prompted to use the steps when you enter the personal page.

Follow the steps to add the token, then map port 80.

If you find that the ngrok command cannot be found later, you can go to the official website to manually download the next file and drop it into the /usr/local/bin directory.

After success, you can see ngrok's page and use the Forwarding address he provided us, which is our public network address.

I made a random port of 80. Here, I can use the port number of my own project to map it. We will change it to 9000 later.

webhooks

After configuring, go to our github project to set up a webhook and use the address obtained above.

This is set to OK, and then create a Java program to monitor port 9000, and write Controller .

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

Then just change our document, push it, and receive the callback of the webhook. In fact, we don't pay attention to the content of the callback at all. As long as we receive this notification, we will trigger the instruction to re-pull the code.

 {
    "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
    }
}

Next, we implement the code logic, and pull the latest code to the local according to the callback execution command. In order to operate Git through Java, JGit is introduced.

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

For simplicity, I re-clone the warehouse every time. Normally, it should be cloned for the first time, and then directly pull the code. In order to save trouble, I will do this first.

 @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;
        }
    }
}

After the code is executed, it is already OK, and the code can be pulled directly. So now, it is almost OK. After that, the code is thrown directly on the server and it is pulled down.

server problem

Okay, do you think it's over here? Younger, younger. . .

If you throw this code on the server and run it, you will find that the error cannot be connected to github, if you are an Alibaba Cloud server.

The solution is to find /etc/ssh/ssh_config , delete the comment in front of the GSSAPIAuthentication no line, and then save it, you will find that you can really download it.

At the same time, in nginx, we map another domain name as the callback domain name. Here, you need your https certificate. Because I am a free version, the https domain name cannot take effect. Then the webhook callback should use 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, if you can't solve it in the above way, you can use another way to deal with it directly through Java direct shell script.

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

The purpose of using the script is to use the accelerated git address, otherwise the server access will often fail. Find a plugin to have the accelerated address.

 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);
        }
    }

}

code upload problem

Well, this way of uploading the code manually can actually be used, but in order to make deployment easier, I recommend installing a plug-in Alibaba Cloud Toolkit , and some other cloud servers also have this type of plug-in.

After installing the plugin, you will enter the configuration page, you need to configure a accessKey .

Go to your Alibaba Cloud account to configure it.

After the configuration is complete, click Tools-Deploy to xxx , I am ECS, so select the ECS server.

Then your server information will be automatically loaded here, then select your own deployment directory, and select a Command to execute the command.

This command just finds a directory to create a script, just put it there, and finally click Run, and the code will be uploaded quickly.

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

Finish

The basic work has been completed, but there are still many details that have not been dealt with, such as failure retry, callback authentication, etc. This is only a very basic version.

Synchronization code git address: https://github.com/irwinai/sync-tools

Article warehouse address: https://github.com/irwinai/JavaInterview

Blog address: https://aixiaoxian.vip/#/


艾小仙
206 声望69 粉丝