Onedrive图床Nodebb论坛插件开发


  • 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将静态资源改成查询的。

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

    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)

    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自己来封装一下,如下:

    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

    var res = await odapi.items.listChildren({ // 返回字目录数组,结果有name和id, webUrl
    	accessToken: accessToken,
    	itemId: '01DDEFCALS....', // "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

    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

    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

    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

    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

    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


  • 23333看起来就感觉很复杂梳理辛苦了

yurisbbs by @devseed since 2020, powered by nodebb.