Hexo使用PythonSDK整站静态发布七牛云

前言

最近因GitLab的国内访问速度问题将本站静态化到了七牛云存储,但是带来的问题也是很明显的。之前在GitLab使用Page来发布工程只需要提交代码即可,会自动静态化并部署项目,于是乎我就想让静态化并同步到七牛对象存储自动化一些,这也是这篇文章的初衷。

其实在用Python做自动同步之前,我使用的官方提供的一个Window上面的同步工具QSunSync,但是使用起来也不是很方便,最主要的原因是不太符合我整站同步的需求,而且速度极慢。

Python SDK

官网为我们提供了基础的对象存储操作SDK, 点击查看

安装Python SDK

pip install qiniu

整个同步过程如下:

  1. 拉取七牛云文件列表。
  2. 获取本地文件列表。
  3. 对比远程文件是否有增量(多余的),进行删除。
  4. 对比本地文件是否有增量(新增的)或者有变化(hash不同),进行上传。

同步脚本文件

from qiniu import Auth, put_file, etag, urlsafe_base64_encode, BucketManager, CdnManager
from typing import List, Dict
import os

from qiniu import build_batch_delete


class Sync:
    """
    同步目录至七牛云
    """

    def __init__(
        self,
        access_key: str,
        secret_key: str,
        bucket_name: str,
        sync_dir: str,
        exclude: List,
        cover: bool,
        remove_redundant: bool,
        host: str,
    ):
        self.bucket_name = bucket_name
        self.q = Auth(access_key, secret_key)
        self.bucket = BucketManager(self.q)
        self.sync_dir = sync_dir
        self.exclude = exclude
        self.cover = cover
        self.remove_redundant = remove_redundant
        self.host = host
        self.sync()

    def sync(self):
        """
        同步操作
        :return:
        """
        remote_files = self.list_remote()

        local_files = self.list_local()

        # 首先删除远端仓库中多余的文件
        remove_remote_files = []
        for remote_filename in remote_files:
            if remote_filename not in local_files:
                remove_remote_files.append(remote_filename)
        print('delete remote file size = ' + str(len(remove_remote_files)))
        self.bucket.batch(build_batch_delete(self.bucket_name, remove_remote_files))

        cdnManager = CdnManager(self.q)
        refreshUrls = []
        refreshDirs = [self.host]
        # 上传本地文件到远端(仅上传远端不存在的以及修改过的)
        for local_filename in local_files:
            if (
                local_filename not in remote_files
                or local_files[local_filename]["hash"]
                != remote_files[local_filename]["hash"]
            ):
                ret, info = put_file(
                    self.q.upload_token(self.bucket_name, local_filename, 3600),
                    local_filename,
                    local_files[local_filename]["fullpath"],
                )
                remotepath = self.host + ret["key"]
                print('uploaded: ' + remotepath)
                #refreshUrls.append(remotepath)
               
        print('uploaded size = ' + len(refreshUrls))
        # 刷新节点资源
        cdnManager.refresh_dirs(refreshDirs)
        print('sycn completed !!!!')

    def list_remote(self) -> Dict:
        """
        列出远程仓库所有的文件信息
        :return: List
        """
        result = {}
        marker = None
        ret = self.bucket.list(self.bucket_name, marker=marker, limit=500)[0]
        for file in ret["items"]:
            result[file["key"]] = file
        if("marker" in ret):
            marker = ret["marker"]
            while(marker):
                print('marker = ' + marker)
                ret = self.bucket.list(self.bucket_name, marker=marker, limit=500)[0]
                for file in ret["items"]:
                    result[file["key"]] = file
                if("marker" in ret):
                    marker = ret["marker"]
                else: 
                    break
        print('pull remote file info success')
        return result

    def list_local(self) -> Dict:
        """
        列出本地仓库所有的文件信息
        """
        files = {}

        def get_files(path):
            for filename in os.listdir(path):
                if filename in self.exclude:
                    continue
                fullpath = os.path.join(path, filename)
                if os.path.isfile(fullpath):
                    key = fullpath.split(self.sync_dir)[1]
                    foldpath = key.replace("\\","/") #实际是相对路径    
                    files[foldpath] = {"fullpath": fullpath, "hash": etag(fullpath)}
                else:
                    get_files(fullpath)
        get_files(self.sync_dir)
        print('list local file info success')
        return files


if __name__ == "__main__":
    Sync(
        access_key="nzt90pr6nkba0dG1TD2oMul0XHBe6ALuBo3hHc_l",  # access_key
        secret_key="yoRI0b8J9dDiYwtJeE844tYbrupdjSiVio6sczf1",  # secret_key
        bucket_name="blog",  # bucket_name
        #sync_dir="~/blog/public/",  # 静态文件目录(后面必须有斜杠/)
        sync_dir="D:\\blog\\gitlab\\public\\",
        exclude=[".DS_Store"],
        cover=True,
        remove_redundant=True,
        host="https://dp2px.com/",
    )

如何使用

如果你也想和我一样将整站的资源同步到七牛云,则只需要第一步使用hexo g来生成静态资源到public目录,然后配置上面Sync类的构造参数,最后执行python blog-qiniu.py即可。

Sync(
    access_key="xxxxxx",  # access_key
    secret_key="xxxxxx",  # secret_key
    bucket_name="blog",  # bucket_name
    sync_dir="D:\\blog\\gitlab\\public\\", #本地public静态资源目录
    host="https://dp2px.com/",  #网站host,用于刷新cdn缓存
)

上面值得注意的是代码中的七牛CDN缓存刷新,官方限制了每日100条缓存文件刷新和10条缓存目录刷新,显然这100条是不够用的,所以每次提交完后我会使用refresh_dirs(refreshDirs)主动刷新更目录一次。或者可以换个思路、刷新主页面几个入口界面即可。

refreshUrls = [
    self.host + 'index.html'   # 刷新首页

]
refreshDirs = [
    self.host + 'tags/',       # 刷新标签页
    self.host + 'categories/', # 刷新分类页
    self.host + 'archives/'    # 刷新归档页
]
cdnManager = CdnManager(self.q)
cdnManager.refresh_urls_and_dirs(refreshUrls, refreshDirs)

除了上面使用官方提供的SDK去刷新外,还可以去融合CDN的刷新预取界面手动操作刷新。

有的朋友可能会发现刷新后去浏览器访问并没有发生改变,那是因为在七牛对象存储的设置界面有一个maxAge缓存时间设置,我建议设置成1000,也就是17分钟左右后会重新请求新页面。

七牛对象存储maxAge设置

最后提一下,本站对整站资源进行了压缩处理,以便有更快的访问速度,安装npm install hexo-neat --save插件,在根目录_config.yml文件中配置如下:

# 文件压缩,设置一些需要跳过的文件 
# hexo-neat
neat_enable: true
# 压缩 html
neat_html:
  enable: true
  exclude:
# 压缩 css
neat_css:
  enable: true
  exclude:
    - '**/*.min.css'
# 压缩 js
neat_js:
  enable: true
  mangle: true
  output:
  compress:
  exclude:
    - '**/*.min.js'
    - '**/jquery.fancybox.pack.js'
    - '**/index.js'

在执行hexo g的同时会压缩静态网站资源,如果发生错误不用理会,不会影响实际展示,只是部分页面得不到正确压缩而已。

GitLab CI/CD

GitLab提供持续集成服务。如果添加一个.gitlab-ci.yml文件到项目根目录,并配置GitLab项目使用某个Runner,然后每一次提交或者是推送都会触发CI pipeline.

.gitlab-ci.yml文件会告诉GitLab Runner 做什么。默认情况下,它运行一个pipeline,分为三个阶段:build,test,deploy。你并不需要用到所有的阶段,没有job的阶段会被忽略。

简而言之,CI所需要的步骤可以归结为:

  1. 添加.gitlab-ci.yml到项目的根目录
  2. 配置一个Runner

从此刻开始,在每一次push到Git仓库的过程中,Runner会自动开启pipline,pipline将显示在项目的Pipline页面中。

GitLab CI/CD

接下来我们可以配置.gitlab-ci.yml来自动静态编译压缩和执行python脚本。

before_script:
  - apt-get update -qq && apt-get install -y -qq pandoc
image: node:10.15.1
pages:
  cache:
    paths:
    - node_modules/
  script:
  - git config --global user.email "lxq_xsyu@163.com"
  - git config --global user.name "lxqxsyu"
  - npm install hexo-cli -g
  - npm install
  - npm uninstall hexo-renderer-marked@0.3.2 --save
  - npm install hexo-generator-sitemap@1.2.0 --save
  - npm install hexo-generator-baidu-sitemap@0.1.5 --save
  - npm install hexo-util@0.6.3 --save
  - npm install hexo-generator-search@2.3.0 --save 
  - npm install image-size@0.6.3 --save
  - npm install hexo-asset-image@0.0.3 --save
  - npm install hexo-generator-feed@1.2.2 --save
  - npm install hexo-neat --save
  - npm install hexo-helper-live2d@3.1.1 --save
  - hexo g
  - python --version
  - apt-get -y install python-pip
  - pip install qiniu
  - pip install typing
  - python blog-qiniu.py
  artifacts:
    paths:
    - public
  only:
  - master

本来这些配置以及可以解决问题,只需要提交代码就可以自动执行,但是在执行过程中发现镜像中的python版本是2.7的,所以我们需要修改之前编写的blog-qiniu.py中的部分代码来适应低版本, 例如asii编码问题需要添加如下代码:

# -*- coding: utf-8 -*-
#!/usr/bin/python
import sys  

reload(sys)  
sys.setdefaultencoding('utf8')