1. 前言

说到图床,真是一言难尽呀。找了半天发现没有一个完全令人满意。不谈是否能长远稳定,但就使用多少有些问题。

国内的很多图床限制太多,单张图片限制,总容量太小,流量限制,大多都没有api。
国外的图床限制少点儿,单张容量可达30m,不限流量,也有api可以满足需求,体验也不错。
但是最大的问题在于国内访问的速度是真慢,有的网络甚至无法访问。

至于为什么一定要api支持,因为我对可控性(control)和可移植性(portable)非常执念,通过api用脚本可以方便转移和部署。

因此想着用onedrive作为后端来搞个图床,顺便来diy个共享网盘。这种方法的好处是自己完全掌控,可以很方便的进行备份,迁移等操作。虽然api访问次数没有明说。

由于相关api开发教程不是很多,自己也踩了不少坑,onedrive的api对于新手不太友好,很多概念令人很费解,感觉有必要写个文章来梳理一下。

这个插件我已经上传到GitHubnpm上了。

2 . Onedrive共享盘搭建方案

这部分倒是已经有现成的方案做的不错了,没必要重复造轮子了,我就直接用的FODI了。

由于我不会php,也不想装php环境,此处就没有考虑。为了练习node,以js语言为主。

FODI

这个非常界面非常清爽,但是功能不少:支持markdown,音视频图片等媒体文件在线预览,直链下载。而且配置起来也很方便,cloudflare worker单文件就行。

logi向导界面选择版本后点前往登录,然后把浏览器地址栏返回的地址复制到对应填表位置(页面本身是显示失败的),为了得到refresh token。之后将生成代码复制到worker里。
参数proxied表示用代理。注意网盘不能是空的,否则会报错。

ondrive-cf-index

还有另外一个框架是onedrive-cf-index,但是这个部署起来就麻烦多了。要自己去azure手动配置,多文件要用cf的命令行工具,我这里一直报错。而且还要去用firebase数据库。试了一下此方案放弃。

cuteone

cuteone, python引擎编写的,据说很强大的工具,可以多盘管理,还有用户权限管理相关的。

不过我没有尝试,但是看着不错,就放在这里参考了。

3. Onedrive以FODI为例图床搭建

这里以FODI为例来说明怎么改造成图床。其实最简单的方法,直接看其生成的链接,可直接用。

在cf worker上部署后是 xxx.yyy.workers.dev?file=/path/to/file 格式。

但是总觉得挺奇怪的,明明是静态资源却还要query,强迫症感觉很别扭。

因此我们用nginx加一层反向代理,通过rewrite或者set $args将静态资源改成查询的。

这样的好处是之后可以多网盘负载均衡,而且反向代理有点像符号链接,迁移网盘改个映射就行了。

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name static.domainname; # 绑定的域名,我习惯用static放到开头
location / {
# rewrite ^(.*) https://xxx.yyy.workers.dev?file=$request_uri;
set $url localhost; # 可以通过set改变url和args
set $args "file=$request_uri";
proxy_pass https://xxx.yyy.workers.dev;
}
}

顺便一提,我不太喜欢直接修改源码,因为每次更新后都要merge,非常麻烦,我的原则尽量不在原来的上面直接改。多用间接的方案,比如插件或hook。

4. Onedrive api 分析

关于onedrive的api,可以来看onedrive文档,在nodejs环境下,可以用封装好的onedrive-api,引入 const odapi = require('onedrive-api')

auth

使用onedrive api之前必须要进行认证,认证流程如下图, 详见graph-oauth

Authorization Code Flow Diagram

.1 进入azure将redirect_uri指向microsoft-graph-api-auth页面,并开启offline_access Files.Read Files.Read.All Files.Write Files.WriteAll权限,创建secret

.2 然后在microsoft-graph-api-auth填写client id, scope(权限)认证后, 填client secret即可得到accesstoken, refreshtoken。

access_token是临时的为了暂时共享,refresh_token长时有效来获取access_token

.3 用refreshtoken获取accesstoken(需要offline_access 权限,可省略redirect_uri)

1
2
3
4
5
POST https://login.microsoftonline.com/common/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}
&refresh_token={refresh_token}&grant_type=refresh_token

关于refresh_token的获取,或者用simple-oauth2来获取。

由于onedrive-api里面没有auth相关的用法,我们需要根据上述api和axios自己来封装一下,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function odauth(client_id, client_secret, refresh_token){
var res=null;
try {
res = await axios.post("https://login.microsoftonline.com/common/oauth2/v2.0/token" , new URLSearchParams({
client_id: client_id,
client_secret: client_secret,
refresh_token: refresh_token,
grant_type: "refresh_token"
}).toString(), {
headers : {
"Content-Type": "application/x-www-form-urlencoded"
},
})
} catch(e){
console.log("odauth error!")
console.log(e)
}
return res; // res.data.access_token
}

需要注意的是,这个api是用了application/x-www-form-urlencoded格式,因此payload需要是get里的query字符串拼接格式

listChildren

1
2
3
4
5
6
7
var res = await odapi.items.listChildren({ // 返回字目录数组,结果有name和id, webUrl
accessToken: accessToken,
itemId: '01CNNH3...', // "root" 或者本函数返回目录的id值
drive: 'me', // 'me' | 'user' | 'drive' | 'group' | 'site'
driveId: '' // BLANK | {user_id} | {drive_id} | {group_id} | {sharepoint_site_id}
})
console.log(res) //returns body of https://dev.onedrive.com/items/list.htm#response

createFolder

1
2
3
4
5
6
7
8
odapi.items.createFolder({ // 同理也返回name,id和webUrl
accessToken: accessToken,
rootItemId: "root",
name: "Folder name"
}).then((item) => {
// console.log(item)
// returns body of https://dev.onedrive.com/items/create.htm#response
})

delete

1
2
3
4
5
6
7
8
9
odapi.items.delete({
accessToken: accessToken,
itemId: createdFolder.id
}).then(() => {
// file is deleted
}).catch((error) => {
// error.response.statusCode => error code
// error.response.statusMessage => error message
})

update

1
2
3
4
5
6
7
8
9
10
odapi.items.update({ // 改名
accessToken: accessToken,
itemId: createdFolder.id,
toUpdate: {
name: "newFolderName"
}
}).then((item) => {
// console.log(item);
// returns body of https://dev.onedrive.com/items/update.htm#response
})

download

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var fileStream = odapi.items.download({ // 全部下载
accessToken: accessToken,
itemId: createdFolder.id
});
fileStream.pipe(SomeWritableStream);

var partialPromise = odapi.items.partialDownload({ // 部分下载
accessToken: accessToken,
bytesFrom: 0, // start byte
bytesTo: 1034, // to byte
graphDownloadURL: createdItem['@microsoft.graph.downloadUrl'],
// optional params
itemId: createdItem.id, // only be used when `graphDownloadURL` is NOT provided
drive: 'me', // only be used when only `itemId` is provided
driveId: 'me' // only be required when `drive` is provided
}).then(
(fileStream) => fileStream.pipe(SomeWritableStream)
);

upload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
odapi.items.uploadSimple({
accessToken: accessToken,
filename: filename,
[params.parentPath] :
[params.parentId] :
readableStream: readableStream }).then((item) => {
// console.log(item);
// returns body of https://dev.onedrive.com/items/upload_put.htm#response
})

odapi.items.uploadSession({ // 返回webUrl 等信息, @content.downloadUrl
accessToken: accessToken,
filename: filename,
fileSize: fileSize,
[parentPath] : // 上传的路径,如/public/tmp形式,路径不存在会自动创建
[parentId] :
readableStream: readableStream
}, (bytesUploaded) => { console.log(bytesUploaded)}).then((item) => {
// console.log(item);
// returns body of https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online#http-response
})

share

参考https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createlink?view=odsp-graph-online

5. Nodebb onedrive 上传插件编写

熟悉了上述的api,我们终于可以编写论坛上传图到OneDrive的插件了!

思路是在触发filter:uploadImage这个的时候将图片通过api上传的onedrive上面,

并await图片位置和nginx反向代理的地址返回给nodebb。

开发nodebb的插件还是挺费劲的,详见中文文档中的吐槽。说一下我的调试方法吧

  • mongodump , mongoestore 克隆数据库到本地,不要在线上生产环境开发
  • npm linknodebb dev来观看日志慢慢调,各个变量用之前先打印看看
  • 报错非常不直观,一个拼写错误可能就使得插件无法加载,不能定位到出错点

详细代码见 nodebb-plugin-onedrive,最后来个插件截图吧~

test

config