Skip to content

Commit 920076f

Browse files
authored
Merge pull request #104 from LetTTGACO/img2cos-master
feat(语雀图片转腾讯云图床): 支持语雀图片转腾讯云图床
2 parents c5e2d29 + 355d2b0 commit 920076f

File tree

9 files changed

+350
-11
lines changed

9 files changed

+350
-11
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@ typings/
6363
package-lock.json
6464

6565
yarn.lock
66+
.idea

README.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ A downloader for articles from yuque(语雀知识库同步工具)
2929

3030
## Config
3131

32-
### 配置 TOKEN
32+
### 配置 YUQUE_TOKEN
3333

3434
出于对知识库安全性的调整,使用第三方 API 访问知识库,需要传入环境变量 YUQUE_TOKEN,在语雀上点击 个人头像 -> 设置 -> Token 即可获取。传入 YUQUE_TOKEN 到 yuque-hexo 的进程有两种方式:
3535

@@ -38,6 +38,17 @@ A downloader for articles from yuque(语雀知识库同步工具)
3838
- mac / linux: `YUQUE_TOKEN=xxx yuque-hexo sync`
3939
- windows: `set YUQUE_TOKEN=xxx && yuque-hexo sync`
4040

41+
### 配置 腾讯云对象存储TOKEN(可选)
42+
语雀的url存在防盗链的问题,直接部署可能导致图片无法加载。
43+
如果需要语雀URL上传到腾讯云的COS中并替换原链接,就需要配置上传密钥。
44+
45+
访问[API密钥管理](https://console.cloud.tencent.com/cam/capi) 获取密钥,然后传入密钥到yuque-hexo
46+
- 在设置YUQUE_TOKEN的基础上配置SECRET_ID和SECRET_KEY
47+
- 命令执行时传入环境变量
48+
- mac / linux: `YUQUE_TOKEN=xxx SECRET_ID=xxx SECRET_KEY=xxx yuque-hexo sync`
49+
- windows: `set YUQUE_TOKEN=xxx SECRET_ID=xxx SECRET_KEY=xxx && yuque-hexo sync`
50+
51+
4152
### 配置知识库
4253

4354
> package.json
@@ -56,7 +67,13 @@ A downloader for articles from yuque(语雀知识库同步工具)
5667
"repo": "blog",
5768
"onlyPublished": false,
5869
"onlyPublic": false,
59-
"lastGeneratePath": "lastGeneratePath.log"
70+
"lastGeneratePath": "lastGeneratePath.log",
71+
"imgCdn": {
72+
"enabled": false,
73+
"bucket": "",
74+
"region": "",
75+
"prefixKey": ""
76+
}
6077
}
6178
}
6279
```
@@ -74,9 +91,28 @@ A downloader for articles from yuque(语雀知识库同步工具)
7491
| repo | 语雀仓库短名称,也称为语雀知识库路径 | - |
7592
| onlyPublished | 只展示已经发布的文章 | false |
7693
| onlyPublic | 只展示公开文章 | false |
77-
94+
| imgCdn | 语雀图片转CDN配置 | |
7895
> slug 是语雀的永久链接名,一般是几个随机字母。
7996
97+
imgCdn 语雀图片转COS(对象存储)配置说明
98+
99+
注意:开启后会将匹配到的所有的图片都上传到COS
100+
101+
| 参数名 | 含义 | 默认值 |
102+
| ------------- | ------------------------------------ | -------------------- |
103+
| enabled | 是否开启 | false |
104+
| bucket | 腾讯COS的bucket名称 | - |
105+
| region | 腾讯COS的region(地域名称) | - |
106+
| prefixKey | 文件前缀 | - |
107+
> prefixKey 说明
108+
>
109+
> 如果需要将图片上传到COS的根目录,那么prefixKey不用配置。
110+
>
111+
> 如果想上传到指定目录blog/image下,则需要配置prefixKey为"prefixKey": "blog/image"。
112+
>
113+
> 目录名前后都不需要加斜杠
114+
115+
80116
## Install
81117

82118
```bash
@@ -143,8 +179,10 @@ DEBUG=yuque-hexo.* yuque-hexo sync
143179

144180
more detail
145181
```
182+
- 为什么选择腾讯COS作为图床:腾讯的COS费用相对便宜,对于博客来说是非常划算且方便的。
183+
当然,如果想用其他图床,可以参考源码中实现方式,自行修改配置。
146184

147-
- 如果遇到上传到语雀的图片无法加载的问题,可以参考这个处理方式 [#41](https://github.com/x-cold/yuque-hexo/issues/41)
185+
- 如果遇到上传到语雀的图片无法加载的问题,可以考虑开启imgCdn配置或者参考这个处理方式 [#41](https://github.com/x-cold/yuque-hexo/issues/41)
148186

149187
# Example
150188

adapter/hexo.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const ejs = require('ejs');
44
const Entities = require('html-entities').AllHtmlEntities;
55
const FrontMatter = require('hexo-front-matter');
66
const { formatDate, formatRaw } = require('../util');
7+
const img2Cos = require('../util/img2cdn');
8+
const config = require('../config');
9+
710

811
const entities = new Entities();
912
// 背景色区块支持
@@ -60,7 +63,11 @@ function parseMatter(body) {
6063
* @param {Object} post 文章
6164
* @return {String} text
6265
*/
63-
module.exports = function(post) {
66+
module.exports = async function(post) {
67+
// 语雀img转成自己的cdn图片
68+
if (config.imgCdn.enabled) {
69+
post = await img2Cos(post);
70+
}
6471
// matter 解析
6572
const parseRet = parseMatter(post.body);
6673
const { body, ...data } = parseRet;

adapter/markdown.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
'use strict';
22

33
const { formatRaw } = require('../util');
4+
const img2Cos = require('../util/img2cdn');
5+
const config = require('../config');
46

57
/**
68
* markdown 文章生产适配器
79
*
810
* @param {Object} post 文章
911
* @return {String} text
1012
*/
11-
module.exports = function(post) {
13+
module.exports = async function(post) {
14+
// 语雀img转成自己的cdn图片
15+
if (config.imgCdn.enabled) {
16+
post = await img2Cos(post);
17+
}
1218
const { body } = post;
1319
const raw = formatRaw(body);
1420
return raw;

config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const defaultConfig = {
1919
concurrency: 5,
2020
onlyPublished: false,
2121
onlyPublic: false,
22+
imgCdn: {
23+
enabled: false,
24+
bucket: '',
25+
region: '',
26+
prefixKey: '',
27+
},
2228
};
2329

2430
function loadConfig() {

lib/Downloader.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class Downloader {
169169
*
170170
* @param {Object} post 文章详情
171171
*/
172-
generatePost(post) {
172+
async generatePost(post) {
173173
if (!isPost(post)) {
174174
out.error(`invalid post: ${post}`);
175175
return;
@@ -197,7 +197,7 @@ class Downloader {
197197
process.exit(-1);
198198
}
199199
out.info(`generate post file: ${postPath}`);
200-
const text = transform(post);
200+
const text = await transform(post);
201201
fs.writeFileSync(postPath, text, {
202202
encoding: 'UTF8',
203203
});
@@ -206,19 +206,22 @@ class Downloader {
206206
/**
207207
* 全量生成所有 markdown 文章
208208
*/
209-
generatePosts() {
209+
async generatePosts() {
210210
const { _cachedArticles, postBasicPath } = this;
211211
mkdirp.sync(postBasicPath);
212212
out.info(`create posts directory (if it not exists): ${postBasicPath}`);
213-
_cachedArticles.forEach(this.generatePost);
213+
const promiseList = _cachedArticles.map(async item => {
214+
return await this.generatePost(item);
215+
});
216+
await Promise.all(promiseList);
214217
}
215218

216219
// 文章下载 => 增量更新文章到缓存 json 文件 => 全量生成 markdown 文章
217220
async autoUpdate() {
218221
this.readYuqueCache();
219222
await this.fetchArticles();
220223
this.writeYuqueCache();
221-
this.generatePosts();
224+
await this.generatePosts();
222225
if (this.lastGeneratePath) {
223226
fs.writeFileSync(this.lastGeneratePath, new Date().getTime().toString());
224227
}

lib/qetag.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict';
2+
3+
// 计算文件的eTag,参数为buffer或者readableStream或者文件路径
4+
function getEtag(buffer, callback) {
5+
6+
// 判断传入的参数是buffer还是stream还是filepath
7+
let mode = 'buffer';
8+
9+
if (typeof buffer === 'string') {
10+
buffer = require('fs').createReadStream(buffer);
11+
mode = 'stream';
12+
} else if (buffer instanceof require('stream')) {
13+
mode = 'stream';
14+
}
15+
16+
// sha1算法
17+
const sha1 = function(content) {
18+
const crypto = require('crypto');
19+
const sha1 = crypto.createHash('sha1');
20+
sha1.update(content);
21+
return sha1.digest();
22+
};
23+
24+
// 以4M为单位分割
25+
const blockSize = 4 * 1024 * 1024;
26+
const sha1String = [];
27+
let prefix = 0x16;
28+
let blockCount = 0;
29+
30+
// eslint-disable-next-line default-case
31+
switch (mode) {
32+
case 'buffer':
33+
// eslint-disable-next-line
34+
const bufferSize = buffer.length;
35+
blockCount = Math.ceil(bufferSize / blockSize);
36+
37+
for (let i = 0; i < blockCount; i++) {
38+
sha1String.push(sha1(buffer.slice(i * blockSize, (i + 1) * blockSize)));
39+
}
40+
process.nextTick(function() {
41+
callback(calcEtag());
42+
});
43+
break;
44+
case 'stream':
45+
// eslint-disable-next-line no-case-declarations
46+
const stream = buffer;
47+
stream.on('readable', function() {
48+
let chunk;
49+
// eslint-disable-next-line no-cond-assign
50+
while (chunk = stream.read(blockSize)) {
51+
sha1String.push(sha1(chunk));
52+
blockCount++;
53+
}
54+
});
55+
stream.on('end', function() {
56+
callback(calcEtag());
57+
});
58+
break;
59+
}
60+
61+
function calcEtag() {
62+
if (!sha1String.length) {
63+
return 'Fto5o-5ea0sNMlW_75VgGJCv2AcJ';
64+
}
65+
let sha1Buffer = Buffer.concat(sha1String, blockCount * 20);
66+
67+
// 如果大于4M,则对各个块的sha1结果再次sha1
68+
if (blockCount > 1) {
69+
prefix = 0x96;
70+
sha1Buffer = sha1(sha1Buffer);
71+
}
72+
73+
sha1Buffer = Buffer.concat(
74+
[ new Buffer([ prefix ]), sha1Buffer ],
75+
sha1Buffer.length + 1
76+
);
77+
78+
return sha1Buffer.toString('base64')
79+
.replace(/\//g, '_').replace(/\+/g, '-');
80+
81+
}
82+
83+
}
84+
85+
module.exports = getEtag;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@yuque/sdk": "^1.1.1",
2929
"chalk": "^2.4.1",
3030
"common-bin": "^2.7.3",
31+
"cos-nodejs-sdk-v5": "^2.11.6",
3132
"debug": "^3.1.0",
3233
"depd": "^2.0.0",
3334
"ejs": "^3.1.6",
@@ -40,6 +41,7 @@
4041
"prettier": "^2.0.4",
4142
"queue": "^4.5.0",
4243
"rimraf": "^2.6.2",
44+
"superagent": "^7.0.2",
4345
"update-check": "^1.5.3",
4446
"urllib": "^2.29.1"
4547
},

0 commit comments

Comments
 (0)