博客部署调整记录

This commit is contained in:
结发受长生
2019-05-11 21:27:41 +08:00
parent 1618668b14
commit 10ca6c1d4c
6 changed files with 304 additions and 4 deletions
@@ -1,109 +0,0 @@
---
title: 博客图片迁移-后续
date: 2019-04-23 19:13:17
tags:
- nodejs
categories:
- linux
---
本着不折腾不舒服的原则, 之前的图片迁移与构建时的自动同步虽然已经实现
但是仍有很多可以改进的地方
<!-- more -->
#### 引入二分法查找
在执行本地文件列表与仓库内的文件列表比对的过程中
直接逐个比对找差异的复杂度高达**O(n²)**
何况还考虑之后要把本地没有, 但仓库里有的文件删除
这个执行时间太过漫长
所以考虑对文件列表排序后采用二分法查找
先写个二分法查找的js实现
```javascript
/**
* 二分法查找
* @param {Array} arr 执行查找的数组
* @param {Object} target 要找到的目标元素
* @param {String} key 数组元素上的键
* @param {Number} start 查找的范围 起点
* @param {Number} end 查找的范围 终点
*/
function _binarySearch(arr, target, key, start, end) {
if(!Array.isArray(arr) || !arr.length) {
return -1
}
if(start >= end) {
return arr[start][key] === target ? start : -1
}
let index = Math.ceil((start + end)/2)
if(arr[index][key] === target) {
return index
} else if(arr[index][key] > target) {
return _binarySearch(arr, target, key, start, index-1)
} else {
return _binarySearch(arr, target, key, index+1, end)
}
}
```
> 二分法查找既可以使用递归方式实现, 也可以用循环实现
使用二分法查找需要保证数组是有序的
虽然现在看起来接口返回的数据本身就是有序, 但是还是执行一下排序保证不会出错
先使用`Array.prototype.sort`方法, 并指定排序规则进行排序
之后考虑更换为原数组基本有序的情况下, 更为高效的`插入排序`
```javascript
let storageItems = ret.items.filter((item) => {
return /^images.+?\.(png|jpe?g|gif)$/.test(item.key)
}).sort((item1, item2) => {
if(item1.key > item2.key) {
return 1
} else if(item1.key < item2.key) {
return -1
}
return 0
})
// 待上传的文件列表
let pendingUploadFiles = imagesList.filter(item => {
let index = _binarySearch(storageItems, item.name, 'key', 0, storageItems.length-1)
if(index === -1) {
// 文件名不存在, 代表是新文件
item.type = 'new'
return true
} else if(storageItems[index].eTag !== item.md5) {
// 文件名存在, 但是hash值不同, 代表有变化
item.type = 'change'
return true
}
return false
})
```
整体来看效率提升不少
#### 删除仓库内的文件
要找出本地不存在但是仓库内存在的文件, 调用删除文件的接口进行删除
方法基本雷同, 甚至简单很多
当然 imagesList 也需要是有序的
```javascript
// 待删除的文件列表( 仓库中存在, 本地不存在 )
let pendingDeleteFiles = storageItems.filter(item => {
return _binarySearch(imagesList, item.key, 'name', 0, imagesList.length-1) === -1
})
_deleteObjects(pendingDeleteFiles.map(item => item.key))
/**
* 批量删除文件
* @param {Array} fileNamesList 文件名数组
*/
function _deleteObjects(fileNamesList) {
if(!Array.isArray(fileNamesList) || !fileNamesList.length) return
client.deleteMultiObject({
objectKeys: fileNamesList
}).then(err => {
console.log('===> 文件删除成功')
fileNamesList.forEach(item => console.log(item))
})
}
```
为了节约接口的调用次数, 还是选择批量删除的接口了
@@ -1,265 +0,0 @@
---
title: 博客图片迁移记
date: 2019-4-18 10:53:34
tags:
- jenkins
- nodejs
categories:
- linux
---
之前博客的图片都是直接访问自己的服务器的, 无奈我的服务器带宽太小
大图加载缓慢
考虑使用一些第三方的对象存储服务来保存图片
比较主流的像是七牛云 又拍云, 都必须要绑定备案的域名才能访问
所以选择了网易云的对象存储服务( 现在只能通过企业认证后才能创建资源了, 好在创建的比较早 )
<!-- more -->
网易云的对象存储体验总体来讲还不错, 提供若干API以及针对不同语言的SDK包
简单整理了一下思路, 觉得同步图片的脚本还是用比较上手的js来写, 在服务器上用nodejs运行
### 1.对象存储的开通以及准备
创建一个桶对象, 之后会获得Endpoint以及访问域名
先记下来, 之后会用到
> 要绑定自定义的域名同样需要该域名是备案的
![bucket](/images/linux/nos/bucket.png)
然后创建Access Key
![Access Key](/images/linux/nos/AccessKey.png)
> **注意** : 网易云的对象存储对于权限控制做的比较简单
这个key具备所有的API访问权限, 一定不要上传到公共仓库以免泄露
### 2.nodejs脚本编写以及调试
官方虽然有提供nodejs使用的sdk包, 但是年久失修, 官方已经基本不维护, 问题很多
调用起来非常麻烦
感谢 XGHeaven 大佬自己编写的sdk包 [@xgheaven/nos-node-sdk](https://www.npmjs.com/package/@xgheaven/nos-node-sdk)
使用`npm init`初始化一个nodejs项目
然后安装需要的依赖包`npm install @xgheaven/nos-node-sdk optimist --save`
optimist这个包可以方便处理运行时的命令行参数
为之后在持续集成当中的调用提供方便
#### list_images.js
我的博客图片都保存在 **source/images** 当中, 要与对象存储库中同步, 首先需要读取到本地有的图片文件列表
把这部分功能作为一个module封装一下
```javascript
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
/**
* 递归遍历目录中的所有文件
* @param {String} imageFolderPath 文件夹路径
* @param {Array} images 图片列表
* @param {String} rootPath 根路径
*/
function readDirSync(imageFolderPath, images, rootPath){
var files = fs.readdirSync(imageFolderPath)
files.forEach((item,index) => {
var fileInfo = fs.statSync(`${imageFolderPath}/${item}`)
if(fileInfo.isDirectory()){
// 该文件是一个目录, 则遍历该目录内容
readDirSync(`${imageFolderPath}/${item}`, images, rootPath)
}else{
//读取一个Buffer
let buffer = fs.readFileSync(`${imageFolderPath}/${item}`)
let fsHash = crypto.createHash('md5')
fsHash.update(buffer)
images.push({
name: `${imageFolderPath}/${item}`.replace(rootPath, ''),
md5: fsHash.digest('hex')
})
}
})
return images
}
module.exports = function (rootPath, imageFloder) {
return readDirSync(path.resolve(rootPath, imageFloder), [], rootPath)
}
```
1. 由于目录当中可能包含子目录, 所以需要进行递归遍历
2. 使用nodejs提供的`crypto`模块来计算文件的md5哈希值
( 由于接口返回的数据也是md5的哈希值, 可以用它来比对文件差异 )
> 调用该模块暴露出的函数传入根目录和图片目录
比如imageFloder传入的是'images/'
获得的是形如 _\[{name:'images/a.png',md5:'xxx'},...\]_ 的一个数组
#### auth_info.json
把访问对象仓库API需要的认证信息放在一个json文件当中
```json
{
"defaultBucket":"桶名称",
"endpoint":"http://nos-eastchina1.126.net",
"accessKey":"XXXXXXXXX",
"accessSecret":"XXXXXXXXX"
}
```
其中的字段根据第1步当中获得的写入
#### index.js
在这个模块里面大概需要做以下几件事
1. 调用sdk当中提供的listObject接口获取到对象仓库里面已有的文件列表
2. 将对象仓库的文件列表与本地文件列表进行比对, 包括文件名和hash值的比对
找出本地有但是对象仓库里面没有的, 或者都有但是hash值不同, 就是文件有差别的
3. 调用sdk当中提供的putObject接口上传差异文件
```javascript
// 程序执行的传参
var argv = require('optimist')
.usage('$0 --rootPath [str]')
.demand(['rootPath'])
.argv
const fs = require('fs')
const path = require('path')
const listImages = require('./list_images')
// 当前本地存在的所有图片
const rootPath = argv.rootPath, prefix = 'images/'
const imagesList = listImages(rootPath, prefix)
// 网易云对象存储调用接口client
const NosClient = require('@xgheaven/nos-node-sdk').NosClient
const client = new NosClient(require('./auth_info.json'))
queryObjects.call(client, {limit: imagesList.length+1, prefix})
/**
* 查询所有对象存储库已存在的文件
* @param {Object} params
*/
function queryObjects(params) {
// 列出所有已存储的对象
this.listObject(params).then(ret => {
// ret 包括 items(元素)limit(请求的数量)nextMarker(下一个标记)
let storageItems = ret.items.filter((item) => {
return /^images.+?\.(png|jpe?g|gif)$/.test(item.key)
})
let notExistFiles = imagesList.filter((item) => {
let index = storageItems.findIndex(storageItem => {
return storageItem.key === item.name
})
if(index === -1) {
// 文件名不存在, 代表是新文件
item.type = 'new'
return true
} else if(storageItems[index].eTag !== item.md5) {
// 文件名存在, 但是hash值不同, 代表有变化
item.type = 'change'
return true
}
return false
})
uploadObject.call(this, notExistFiles, 0)
})
}
/**
* 上传文件对象
* @param {Array} filesList 待上传的文件列表
* @param {Number} index 索引值
*/
function uploadObject(filesList, index) {
if(index >= filesList.length) return
this.putObject({
objectKey: filesList[index].name,
body: fs.createReadStream(path.resolve(rootPath, filesList[index].name)), // 支持 Buffer/Readable/string
}).then(result => {
// eTag是上传后远端校验的md5值, 用于和本地进行比对
let eTag = result.eTag.replace(/"/g,'')
if(filesList[index].md5 === eTag) {
console.log(`${filesList[index].name} 上传成功, md5:${eTag} 类型: ${filesList[index].type}`)
} else {
console.warn(`${filesList[index].name} 上传出错, md5值不一致`)
console.warn(`===> 本地文件: ${filesList[index].md5}, 接口返回: ${eTag}`)
}
uploadObject.call(this, filesList, ++index)
})
}
```
有以下几点注意:
1. listObject 接口返回的数据不只有文件, 还有目录, 所以需要筛选到是文件的item, 因为这里我只是存图片, 就简单用正则来处理了
2. listObject 接口调用时传入的prefix用于过滤item的前缀, 可以用于查询某个目录当中的所有对象
3. limit参数用于指定获取最大的条数, 配合响应返回的nextMarker以及marker参数可以简单实现分页
但是实际使用发现官方的接口实现似乎存在bug, 导致获取到的内容不全
所以只好使用`imagesList.length + 1`一次性获取所有了, 该值存在上限, 此处算是一个隐患
4. 由于在服务器上使用jenkins构建博客, rootPath会不同 ( 取决于jenkins的工作空间 )
所以把该路径作为一个参数, 用于执行该脚本时从命令行传入
`optimist`库可以对传参进行校验, 如果指定参数未传入会抛出一个Error
5. putObject 接口传入的 objectKey 是保存在仓库中的位置, 比如可以是`images/sub/a.png`
这里正好可以与listImages当中获取到的文件路径对应, 实现本地与对象仓库的目录结构完全一致
6. prefix取决于图片目录在项目中所处的位置, 这个也可以考虑提取为参数传入, 让脚本更具备通用性
![图片目录](/images/linux/nos/图片目录.png)
#### package.json
主要是加上执行index.js的script
```json
{
"name": "nos-test",
"version": "1.0.0",
"description": "网易云对象存储",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "sookie",
"license": "ISC",
"dependencies": {
"@xgheaven/nos-node-sdk": "^0.2.5",
"optimist": "^0.6.1"
}
}
```
### 3.持续集成
相关的js脚本在服务器上准备好之后, 就可以在持续集成当中写shell添加该脚本的调用了
```bash
# 切换到nodejs脚本所在位置
cd /root/upload_picture
npm run start -- --rootPath ${WORKSPACE}/source/
```
> `WORKSPACE`是jenkins提供的一个环境变量, 代表工作空间分配给构建的目录的绝对路径
### 4.hexo静态化钩子函数编写
由于以往的文章当中的图片路径都是写的本地路径
形如`/images/linux/nos/AccessKey.png`, 批量修改又不利于将来的迁移
所以参考了hexo的官方文档之后, 考虑在静态化过程当中添加一个过滤器
#### 添加配置
在根目录的`_config.yml`文件添加自定义的配置项
```yaml
# 图片存储仓库地址
picture_cdn: https://blog-cdn.nos-eastchina1.126.net
```
#### filter.js
创建**scripts**目录 ( 可以是根目录或者主题目录下, 均有效 )
创建**filter.js**文件
```javascript
hexo.extend.filter.register('before_post_render', function(data){
// data.raw 是原始的文件内容
// data.content 是处理过代码块语法高亮的内容
if(hexo.config.picture_cdn) {
data.content = data.content.replace(/\]\s*\((?=(?!http).*?\))/gi,
']'+`(${hexo.config.picture_cdn}`)
}
return data;
});
```
`before_post_render`这个钩子在文章开始渲染前执行
如果此时修改文章中的图片路径, 静态化之后的文件就是指向对象仓库的URL地址了
日常编写正则处理一下( 注意使用前瞻断言, 正则尽可能精确, 避免影响到其他内容 )
### 5.验证
以上所有做好之后, 在jenkins里面执行构建验证文件同步是否成功
![执行构建](/images/linux/nos/执行构建.png)
之前的构建步骤没什么变化, 最后调用nodejs脚本只要成功把新图片上传上去就可以了
然后访问博客
查看文章页面中的图片路径是否正确
![文章图片路径](/images/linux/nos/文章图片路径.png)