blog-web/source/_posts/linux/博客图片迁移记.md
结发受长生 a9cef770f7 僵尸镇
2019-04-19 22:21:19 +08:00

265 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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|jpg|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值, 用于和本地进行比对
if(filesList[index].md5 === result.eTag) {
console.log(`${filesList[index].name} 上传成功, md5:${result.eTag} 类型: ${item.type}`)
} else {
console.warn(`${filesList[index].name} 上传出错, md5值不一致`)
console.warn(`===> 本地文件: ${item.md5}, 接口返回: ${result.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
workspacePath=$(pwd)
# 切换到nodejs脚本所在位置
cd /root/upload_picture
npm run start -- --rootPath ${workspacePath}/source/
```
> shell里面定义变量, 等号两边不能带空格
### 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)