├── logo.png ├── picgo.png ├── .npmignore ├── tslint.json ├── .gitignore ├── .travis.yml ├── tsconfig.json ├── package.json ├── docs └── Storj存储后端图床配置.md ├── README.md ├── src ├── utils.ts └── index.ts └── LICENSE /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yabostone/picgo-plugin-rclone/HEAD/logo.png -------------------------------------------------------------------------------- /picgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yabostone/picgo-plugin-rclone/HEAD/picgo.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | temp.js 4 | package-lock.json 5 | tsconfig.json 6 | tslint.json 7 | .vscode/ 8 | src/ 9 | .travis.yml 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "target": "es6", 7 | "types": [ 8 | "mocha", 9 | "chai", 10 | "jest" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | node_modules 25 | dist/ 26 | .DS_Store 27 | yarn-error.log 28 | temp.js 29 | package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | 4 | # Travis-CI Caching 5 | cache: 6 | directories: 7 | - node_modules 8 | yarn: true 9 | 10 | # S: Build Lifecycle 11 | install: 12 | - yarn 13 | 14 | stages: 15 | - name: deploy 16 | 17 | jobs: 18 | include: 19 | - stage: deploy 20 | script: 21 | - npm run build 22 | deploy: 23 | provider: npm 24 | email: "" 25 | api_key: "${NPM_TOKEN}" 26 | skip_cleanup: true 27 | on: 28 | branch: master 29 | branches: 30 | only: 31 | - master 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "resolveJsonModule": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "sourceMap": false, 9 | "target": "es2017", 10 | "declaration": true, 11 | "outDir": "dist", 12 | // It's shit. 13 | // "baseUrl": "src", 14 | // "paths": { 15 | // "@core/*": ["core/*"], 16 | // "@lib/*": ["lib/*"], 17 | // "@plugins/*": ["plugins/*"], 18 | // "@utils/*": ["utils/*"] 19 | // }, 20 | "lib": [ 21 | "es2017", 22 | "es2015", 23 | "es6" 24 | ] 25 | }, 26 | "include": [ 27 | "./src/**/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "picgo-plugin-rclone", 3 | "version": "1.1.15", 4 | "description": "支持40多种后端,支持备份", 5 | "main": "dist/index.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "homepage": "https://github.com/yabostone/picgo-plugin-rclone", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "tsc -p .", 13 | "dev": "tsc -w -p .", 14 | "patch": "npm version patch && git push origin master && git push origin --tags", 15 | "minor": "npm version minor && git push origin master && git push origin --tags", 16 | "major": "npm version major && git push origin master && git push origin --tags" 17 | }, 18 | "keywords": [ 19 | "picgo", 20 | "picgo-gui-plugin", 21 | "picgo-plugin", 22 | "rclone", 23 | "Storj", 24 | "S3", 25 | "WEBDAV", 26 | "HTTP", 27 | "SeaweedFS", 28 | "Seafile", 29 | "B2", 30 | "Backblaze" 31 | ], 32 | "author": "xuan", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@types/node": "^10.10.1", 36 | "eslint": "^5.0.1", 37 | "eslint-config-standard": "^11.0.0", 38 | "eslint-plugin-import": "^2.13.0", 39 | "eslint-plugin-node": "^6.0.1", 40 | "eslint-plugin-promise": "^3.8.0", 41 | "eslint-plugin-standard": "^3.1.0", 42 | "picgo": "^1.4.0", 43 | "tslint": "^5.10.0", 44 | "tslint-config-standard": "^7.1.0", 45 | "typescript": "^3.7.3" 46 | }, 47 | "dependencies": { 48 | "file-type": "^16.2.0", 49 | "mime": "^2.5.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/Storj存储后端图床配置.md: -------------------------------------------------------------------------------- 1 | ### Storj简介 2 | Storj是一款去中心化的云存储服务,并有150GB免费存储和150G免费流量提供(分成三个存储桶)。 3 | 超出免费额度的价格按照月1T/4$,流量1T/7$的方式付费。 4 | (Pay as you go)按量付费。 5 | 6 | ### 作为存储后端配置 7 | #### 注册Project 8 | ![cc4adaebe70e14d22f40ac137cc9070b.png](../../_resources/cc4adaebe70e14d22f40ac137cc9070b.png) 9 | 现在只允许注册一个Project,150GB存储 10 | #### 生成助记词 11 | 在创建新的账号时候,会要求生成一组12个单词或更多的助记词,这是访问账号的最大权限,等价于私钥。 12 | 需要存储记下来 13 | #### 生成APIkey和satellite address 14 | 后面的操作需要用uplink cli 注册生成,需要下载安装 15 | 在Access 和Access Grant 下。 16 | 在第二步,会有 `Continue in CLI` 标识,点击生成 17 | APIKey 和satellite address。 18 | ![cfdbf511dadbafb85fb0539adf62811e.png](../../_resources/cfdbf511dadbafb85fb0539adf62811e.png) 19 | ![32d7c3a6b142b74f40ab88044fb1ba3f.png](../../_resources/32d7c3a6b142b74f40ab88044fb1ba3f.png) 20 | 参见红色图标,上面有三个要填的值,分别是APIKey,satellite address,和passParse,前两个来自上图,而第三个来自初次创建账号生成的助记词。 21 | ![f354446de946b5c667b79b7f52ef8538.png](../../_resources/f354446de946b5c667b79b7f52ef8538.png) 22 | #### 生成LinkShare 23 | 需要到官网上下载uplink,这是必须的用来获取URL的公开访问链接。 24 | 25 | `uplink.exe share sj://picgo/ --readonly=true --url --not-after=none --base-url=https://link.ap1.storjshare.io --auth-service=https://auth.ap1.storjshare.io/` 26 | 这里可以配置整个存储桶都是share。 27 | 注意服务点需要填写对。 28 | 生成有URL值 29 | ``` 30 | =========== BROWSER URL ================================================================== 31 | REMINDER : Object key must end in '/' when trying to share recursively 32 | URL : https://link.ap1.storjshare.io/s/jxl7tkgemjfqomuhhv3epaakfcqq/picgo/ 33 | ``` 34 | 将`s` 改为`raw`,域名前缀就可以获取了。 35 | 例如 这里的域名前缀是`https://link.ap1.storjshare.io/raw/jxl7tkgemjfqomuhhv3epaakfcqq/picgo/` 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 插件简介 2 | rclone 插件是一款基于PicGO调用rclone应用实现上传和备份的插件,支持rclone下各种存储后端的**上传**,**备份**和**图床**。同时rclone可以通过命令行实现图床的全量迁移。 3 | 目前测试支持 4 | + 去中心化存储Storj(免费150GB)。(经过计算,Storj在存储小文件时并不划算,15KB的小文件1万个会每月交0.6元,远高于其他存储,说明Storj仅适用于打包的大文件的备份存储,如视频,压缩包等等。) 5 | + Local(本地备份) 6 | + 自建存储后端(WEBDAV,HTTP,Seafile) 7 | + 分布式存储后端(SeaweedFS,Minio) 8 | + COS,OSS,S3兼容性存储,B2等等 9 | 10 | 11 | ## 功能简介 12 | ### 上传 13 | 在remote 选项中配置相关信息,默认会将图片上传到指定位置。 14 | **上传功能区(必填)**: 15 | ![](https://link.ap1.storjshare.io/raw/jxl7tkgemjfqomuhhv3epaakfcqq/picgo/picgo/2022/04/ed1aa3373bce454f00fc39abee423a8e.png) 16 | 17 | ### 备份 18 | 插件设定了三个备份后端槽,可以同时备份到三个存储后端,只要填写 远端存储名 信息就可以。支持备份到本地,详见: 19 | + 有三个后端槽,最多支持三处备份,需要rclone配置好远端存储名,测试好并且正常连接。 20 | 21 | ![](https://link.ap1.storjshare.io/raw/jxl7tkgemjfqomuhhv3epaakfcqq/picgo/picgo/2022/04/d9cb347e859b567f7d608b4cf9b4e1f9.png) 22 | 23 | #### 可以构建图床后端的快速迁移, 24 | 如网站被D时,切换后端,或者cloudflare使用。 25 | 26 | ### 图床 27 | 图床功能只是拼接了域名前缀和路径地址,可能需要与CDN联动。填写域名前缀,后面和桶名和文件上传路径进行拼接。 28 | + 如果想确保图床后端更新,不用更新文件URL的话,需要配置CDN域名指向合适的后端。 29 | 30 | 31 | ## 配置方法 32 | 33 | ### 前置准备 34 | #### 下载rclone 35 | 需要下载rclone,安装并且在默认路径下可用。 36 | #### 配置rclone 37 | 需要配置rclone的远程源,并确认配置正确: 38 | 在系统命令行下输入: 39 | ```Bash 40 | rclone version 41 | ``` 42 | 有相关rclone输出信息认为安装成功。 43 | 44 | ### 配置项解释 45 | 46 | #### 远端存储名 47 | 对应rclone下remoteName,是使用`rclone config`后自己命名的remoteName,例如这里有三个存储后端,remoteName 分别是 `blog.fengidea.com`,`local`,`storj`。 48 | 所以在remoteName/远程源名 处填写`storj`。 49 | ![](https://link.ap1.storjshare.io/raw/jxl7tkgemjfqomuhhv3epaakfcqq/picgo/picgo/2022/04/bb515414181ea08841aaf07d48745a59.png) 50 | #### backup 51 | 存在三个备份槽,默认不填入,填入后需要确认在rclone config下名称与之一致。 52 | 需要填写remoteName、远程源名。 53 | 54 | #### LocalPostion 55 | 这里填写需要注意: 56 | 1. 位置建议应该填写绝对路径 57 | 2. 对应路径需要有用户权限,如Linux需要能在对应位置创建文件夹。 58 | 3. 参见: `/home/mxuan/` `D:\mxuan\` 59 | 60 | ### rclone config配置 61 | 这里给出了几个配置rclone的config项的示例。 62 | 63 | 网上查找rclone的配置教程,测试成功后再使用插件。。 64 | 65 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import FileType from 'file-type' 3 | import mime from 'mime' 4 | import { IImgInfo } from 'picgo/dist/src/types' 5 | import {execFile,execFileSync} from "child_process" 6 | import * as fs from 'fs' 7 | import path from 'path' 8 | import * as os from 'os' 9 | 10 | class FileNameGenerator { 11 | date: Date 12 | info: IImgInfo 13 | static fields = [ 14 | 'year', 15 | 'month', 16 | 'day', 17 | 'fullName', 18 | 'fileName', 19 | 'extName', 20 | 'md5', 21 | 'sha1', 22 | 'sha256' 23 | ] 24 | 25 | constructor (info: IImgInfo) { 26 | this.date = new Date() 27 | this.info = info 28 | } 29 | 30 | public year (): string { 31 | return `${this.date.getFullYear()}` 32 | } 33 | 34 | public month (): string { 35 | return this.date.getMonth() < 9 36 | ? `0${this.date.getMonth() + 1}` 37 | : `${this.date.getMonth() + 1}` 38 | } 39 | 40 | public day (): string { 41 | return this.date.getDate() < 9 42 | ? `0${this.date.getDate()}` 43 | : `${this.date.getDate()}` 44 | } 45 | 46 | public fullName (): string { 47 | return this.info.fileName 48 | } 49 | 50 | public fileName (): string { 51 | return this.info.fileName.replace(this.info.extname, '') 52 | } 53 | 54 | public extName (): string { 55 | return this.info.extname.replace('.', '') 56 | } 57 | 58 | public md5 (): string { 59 | return crypto.createHash('md5').update(this.imgBuffer()).digest('hex') 60 | } 61 | 62 | public sha1 (): string { 63 | return crypto.createHash('sha1').update(this.imgBuffer()).digest('hex') 64 | } 65 | 66 | public sha256 (): string { 67 | return crypto.createHash('sha256').update(this.imgBuffer()).digest('hex') 68 | } 69 | 70 | private imgBuffer (): string | Buffer { 71 | return this.info.base64Image 72 | ? this.info.base64Image 73 | : this.info.buffer 74 | } 75 | } 76 | 77 | export function formatPath (info: IImgInfo, format?: string): string { 78 | if (!format) { 79 | return info.fileName 80 | } 81 | const fileNameGenerator = new FileNameGenerator(info) 82 | 83 | let formatPath: string = format 84 | 85 | for (let key of FileNameGenerator.fields) { 86 | const re = new RegExp(`{${key}}`, 'g') 87 | formatPath = formatPath.replace(re, fileNameGenerator[key]()) 88 | } 89 | 90 | return formatPath 91 | } 92 | 93 | export async function extractInfo(info: IImgInfo): Promise<{ 94 | body?: Buffer 95 | contentType?: string 96 | contentEncoding?: string 97 | }> { 98 | let result: { 99 | body?: Buffer 100 | contentType?: string 101 | contentEncoding?: string 102 | } = {} 103 | 104 | if (info.base64Image) { 105 | const body = info.base64Image.replace(/^data:[/\w]+;base64,/, '') 106 | result.contentType = info.base64Image.match(/[^:]\w+\/[\w-+\d.]+(?=;|,)/)?.[0] 107 | result.body = Buffer.from(body, 'base64') 108 | result.contentEncoding = 'base64' 109 | } else { 110 | if (info.extname) { 111 | result.contentType = mime.getType(info.extname) 112 | } 113 | result.body = info.buffer 114 | } 115 | 116 | // fallback to detect from buffer 117 | if (!result.contentType) { 118 | const fileType = await FileType.fromBuffer(result.body) 119 | result.contentType = fileType?.mime 120 | } 121 | 122 | return result 123 | } 124 | 125 | export function execFileSyncfunc(command:string, args: string[]):string|boolean{ 126 | try{ 127 | const execProcess = execFileSync(command, args,{ 'encoding': 'utf8' }) 128 | console.log(execProcess) 129 | return execProcess 130 | }catch{ 131 | console.log("remoteName is not exist.") 132 | return false; 133 | } 134 | } 135 | export function execFilefunc(command:string, args: string[]){ 136 | return new Promise(function(resolve, reject) { 137 | execFile(command, args, (error, stdout, stderr) => { 138 | if (error) { 139 | reject(error); 140 | return; 141 | } 142 | resolve(stdout.trim()); 143 | }); 144 | });} 145 | 146 | 147 | export function checkRemoteExistSync(remoteName:string) : boolean{ 148 | try{ 149 | const execProcess = execFileSync('rclone', ['ls',remoteName+':'],{ 'encoding': 'utf8' }) 150 | return true; 151 | }catch{ 152 | return false; 153 | } 154 | } 155 | 156 | export function checkRemoteExist(remoteName:string,remoteBucketName:string):Promise{ 157 | return new Promise(function(resolve, reject) { 158 | execFile("rclone", ["mkdir", remoteName+":"+ remoteBucketName], (error, stdout, stderr) => { 159 | if (error) { 160 | reject(false); 161 | return false; 162 | } 163 | resolve(true); 164 | return true; 165 | }); 166 | });} 167 | 168 | /** 169 | * 备份图片到本地 170 | * @param {ctx} ctx 171 | * @param {图片备份文件夹} imagePath 172 | * @param {ctx.output数组成员对象} imgObject 173 | */ 174 | 175 | //返回路径 176 | export function backupInLocal(ctx, imagePath, imgObject):Promise{ 177 | // 读取图片数据 178 | return new Promise(function(resolve,reject){ 179 | var hashfName = formatPath(imgObject,"{md5}.{extName}") 180 | var ret = `${imagePath}/${hashfName}` 181 | var img = imgObject.buffer 182 | if((!img) && (imgObject.base64Image)){ 183 | img = Buffer.from(imgObject.base64Image, 'base64') 184 | } 185 | //ret = path.resolve(ret) 186 | // 备份图片 187 | console.log("filepath in Local:" + ret) 188 | fs.writeFileSync(ret, img) 189 | resolve(ret) 190 | }) 191 | } 192 | 193 | //返回路径 194 | export function backupInLocalSync(ctx, imagePath, imgObject){ 195 | // 读取图片数据 196 | var hashfName = formatPath(imgObject,"{md5}.{extName}") 197 | var ret = `${imagePath}/${hashfName}` 198 | var img = imgObject.buffer 199 | if((!img) && (imgObject.base64Image)){ 200 | img = Buffer.from(imgObject.base64Image, 'base64') 201 | } 202 | //ret = path.resolve(ret) 203 | // 备份图片 204 | console.log("filepath in Local:" + ret) 205 | fs.writeFileSync(ret, img) 206 | return(ret) 207 | } 208 | 209 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import picgo from 'picgo' 2 | import { formatPath,execFileSyncfunc,checkRemoteExist,execFilefunc,backupInLocalSync} from './utils' 3 | import {execFile,execFileSync} from "child_process" 4 | import * as fs from 'fs' 5 | import path from 'path' 6 | import * as os from 'os' 7 | import { emit } from 'process' 8 | 9 | interface rcloneConfig{ 10 | remoteName: string 11 | remoteBucketName: string 12 | remotePrefix: string 13 | urlPrefix: string //用来生成URL的前缀,必填, 14 | uploadPath: string //上传路径,设定年月日 15 | localPostion: string //必填 16 | backupName1: string 17 | backupName2: string 18 | backupName3: string 19 | 20 | } 21 | //返回false或者stdout 22 | 23 | function precheck(ctx:picgo){ 24 | var userConfig0: rcloneConfig = ctx.getConfig("picBed.rclone") 25 | var lloc = os.homedir() + "/.picgo-rclone-local.json" 26 | 27 | var lJson = { 28 | 'remoteName': userConfig0.remoteName, 29 | 'remoteBucketName': userConfig0.remoteBucketName, 30 | 'remotePrefix': userConfig0.remotePrefix, 31 | 'urlPrefix': userConfig0.urlPrefix,//用来生成URL的前缀,必填, 32 | 'uploadPath': userConfig0.uploadPath, //上传路径,设定年月日 33 | 'localPostion': userConfig0.localPostion,//必填 34 | 'backupName1': userConfig0.backupName1, 35 | 'backupName2': userConfig0.backupName2, 36 | 'backupName3': userConfig0.backupName3 37 | } 38 | try{ 39 | //var fjson = JSON.parse(fs.readFileSync(lloc,'utf-8')) 40 | var fjson = fs.readFileSync(lloc,'utf-8') 41 | if(JSON.stringify(lJson) == fjson){ 42 | console.log("配置没有变更,跳过存储桶检查") 43 | } 44 | }catch{ 45 | console.log("配置变更,重新判断存储桶信息") 46 | fs.writeFileSync(lloc,JSON.stringify(lJson)) 47 | 48 | let checkTasks = [] 49 | if(userConfig0.remoteName){ 50 | const promiseRemote = checkRemoteExist(userConfig0.remoteName,userConfig0.remoteBucketName) 51 | checkTasks.push(promiseRemote) 52 | } 53 | if(userConfig0.backupName1){ 54 | const promise1 = checkRemoteExist(userConfig0.backupName1,userConfig0.remoteBucketName) 55 | checkTasks.push(promise1) 56 | } 57 | if(userConfig0.backupName2){ 58 | const promise2 = checkRemoteExist(userConfig0.backupName2,userConfig0.remoteBucketName) 59 | checkTasks.push(promise2) 60 | } 61 | if(userConfig0.backupName3){ 62 | const promise3 = checkRemoteExist(userConfig0.backupName3,userConfig0.remoteBucketName) 63 | checkTasks.push(promise3) 64 | } 65 | console.log(checkTasks) 66 | Promise.all(checkTasks).catch((err)=>{ctx.log.error("检查存储名称失败"); 67 | ctx.log.error(err) 68 | ctx.emit('notification', { 69 | title: 'rclone上传错误', 70 | body: '请检查存储桶、远程源名字是否正确', 71 | text: '' 72 | }) 73 | throw err 74 | }) 75 | } 76 | 77 | } 78 | 79 | const handle = async (ctx: picgo)=>{ 80 | let ListExec = [] 81 | 82 | let userConfig: rcloneConfig = ctx.getConfig("picBed.rclone") 83 | if(!userConfig){ 84 | throw new Error("RCLONE in Picgo config not exist!") 85 | } 86 | if(userConfig.uploadPath){ 87 | userConfig.uploadPath = userConfig.uploadPath.replace(/\/?$/, '') 88 | } 89 | if(userConfig.urlPrefix){ 90 | userConfig.urlPrefix = userConfig.urlPrefix.replace(/\/$/,'') 91 | } 92 | if(userConfig.localPostion){ 93 | try{fs.mkdirSync(userConfig.localPostion)} 94 | catch(error){console.log("创建文件夹失败,检查位置是否正确")} 95 | } 96 | 97 | //item 属于IImgInfo类型 98 | // 顺序 idx 99 | // 定义返回值,url,index 100 | //通常上传成功之后要给这个数组里每一项加入imgUrl以及url项。可以参 101 | 102 | let rcloneLocalURI = ""// 路径 返回,同时存储到文件 103 | for(let index in ctx.output){ 104 | let item = ctx.output[index] 105 | console.log(item) 106 | var fPath = formatPath(item,userConfig.uploadPath) 107 | // 修改成loc路径 108 | rcloneLocalURI = backupInLocalSync(ctx, os.homedir(), item) 109 | if(userConfig.remotePrefix){ 110 | var rcloneRemoteDir = userConfig.remoteName + ":" + userConfig.remoteBucketName + '/' +userConfig.remotePrefix + '/' + fPath 111 | var rcloneLocalPosition = userConfig.localPostion + "/" + userConfig.remoteBucketName + '/' +userConfig.remotePrefix + '/' + fPath 112 | var rcloneBackupDir1 = userConfig.backupName1 + ":" + userConfig.remoteBucketName + "/" + userConfig.remotePrefix + "/" + fPath 113 | var rcloneBackupDir2 = userConfig.backupName2 + ":" + userConfig.remoteBucketName + "/" + userConfig.remotePrefix + "/" + fPath 114 | var rcloneBackupDir3 = userConfig.backupName3 + ":" + userConfig.remoteBucketName + "/" + userConfig.remotePrefix + "/" + fPath 115 | }else{ 116 | var rcloneRemoteDir = userConfig.remoteName + ":" + userConfig.remoteBucketName + '/' + fPath 117 | var rcloneLocalPosition = userConfig.localPostion + "/" + userConfig.remoteBucketName + '/' + fPath 118 | var rcloneBackupDir1 = userConfig.backupName1 + ":" + userConfig.remoteBucketName + "/" + fPath 119 | var rcloneBackupDir2 = userConfig.backupName2 + ":" + userConfig.remoteBucketName + "/" + fPath 120 | var rcloneBackupDir3 = userConfig.backupName3 + ":" + userConfig.remoteBucketName + "/" + fPath 121 | } 122 | console.log(userConfig.localPostion) 123 | console.log(rcloneLocalURI) 124 | await precheck(ctx) 125 | 126 | // 带URL的远程 127 | var up = execFilefunc("rclone" , ['sync', '-P' ,rcloneLocalURI ,rcloneRemoteDir]) 128 | ListExec.push(up) 129 | 130 | if(userConfig.localPostion){ 131 | var lo = execFilefunc("rclone" , ['sync', '-P' ,rcloneLocalURI ,rcloneLocalPosition]) 132 | ListExec.push(lo) 133 | } 134 | if(userConfig.backupName1){ 135 | var up1 = execFilefunc("rclone" , ['sync', '-P' ,rcloneLocalURI ,rcloneBackupDir1]) 136 | ListExec.push(up1) 137 | } 138 | if(userConfig.backupName2){ 139 | var up2 = execFilefunc("rclone" , ['sync', '-P' ,rcloneLocalURI ,rcloneBackupDir2]) 140 | ListExec.push(up2) 141 | } 142 | if(userConfig.backupName3){ 143 | var up3 = execFilefunc("rclone" , ['sync', '-P' ,rcloneLocalURI ,rcloneBackupDir3]) 144 | ListExec.push(up3) 145 | } 146 | 147 | await Promise.all(ListExec).then(()=>{ 148 | console.log(item) 149 | //if (!ctx.output[index].buffer && !ctx.output[index].base64Image) { 150 | // ctx.log.error(new Error('undefined image')) 151 | //} 152 | if(userConfig.remotePrefix){ 153 | var imgURL= userConfig.remotePrefix + "/" + fPath + "/" + path.basename(rcloneLocalURI) 154 | }else{ 155 | var imgURL= fPath + "/" + path.basename(rcloneLocalURI) 156 | } 157 | delete item.buffer 158 | delete item.base64Image 159 | item.url = `${userConfig.urlPrefix}/${imgURL}` 160 | item.imgUrl = `${userConfig.urlPrefix}/${imgURL}` 161 | ctx.output[index]=item 162 | return ctx 163 | }).catch((err)=>{ 164 | ctx.log.error('rclone上传发生错误,请检查配置是否正确') 165 | ctx.log.error(err) 166 | ctx.emit('notification', { 167 | title: 'rclone上传错误', 168 | body: '请检查存储桶、远程源名字是否正确', 169 | text: '' 170 | }) 171 | throw err 172 | }).then(()=>{ fs.unlinkSync(rcloneLocalURI);ctx.log.info(`rcloneLocalURI:${rcloneLocalURI}`);ctx.log.info("已经删除临时文件")}).catch(()=>{console.log("执行rclone 命令失败");ctx.log.info("执行rclone 命令失败")}) 173 | 174 | }//for 175 | 176 | }//handle 177 | 178 | 179 | 180 | const config = (ctx: picgo) => { 181 | const defaultConfig: rcloneConfig = { 182 | remoteName: '', 183 | remoteBucketName: '', 184 | remotePrefix: '', 185 | urlPrefix: '', //用来生成URL的前缀,必填, 186 | uploadPath: '{year}/{month}/', //上传路径,设定年月日 187 | backupName1: '', 188 | backupName2: '', 189 | backupName3: '', 190 | localPostion: '', 191 | //backupBucketName: '', 192 | //backupPrefix: 'picgo' 193 | } 194 | let userConfig = ctx.getConfig('picBed.rclone') 195 | userConfig = { ...defaultConfig, ...(userConfig || {}) } 196 | return [ 197 | { 198 | name: 'remoteName', 199 | type: 'input', 200 | default: userConfig.remoteName, 201 | required: true, 202 | message: '您设定的远程存储的名称', 203 | alias: '远端存储名' 204 | }, 205 | { 206 | name: 'remoteBucketName', 207 | type: 'input', 208 | default: userConfig.remoteBucketName, 209 | required: true, 210 | message: 'BucketName', 211 | alias: '桶名' 212 | }, 213 | { 214 | name: 'remotePrefix', 215 | type: 'input', 216 | default: userConfig.remotePrefix, 217 | required: false, 218 | message: '桶下前缀文件夹名', 219 | alias: '桶下前缀Prefix' 220 | }, 221 | { 222 | name: 'urlPrefix', 223 | type: 'input', 224 | default: userConfig.urlPrefix, 225 | message: '根据存储后端设定的域名前缀', 226 | required: true, 227 | alias: '域名前缀' 228 | }, 229 | { 230 | name: 'uploadPath', 231 | type: 'input', 232 | default: userConfig.uploadPath, 233 | message: '为空则以原始文件名上传到根目录', 234 | required: false, 235 | alias: '上传路径' 236 | }, 237 | { 238 | name: 'backupName1', 239 | type: 'input', 240 | default: userConfig.backupName1, 241 | required: false, 242 | message: '您设定的远程存储的名称(备份位1)', 243 | alias: '备份存储名1' 244 | }, 245 | { 246 | name: 'backupName2', 247 | type: 'input', 248 | default: userConfig.backupName2, 249 | required: false, 250 | message: '您设定的远程存储的名称(备份位2)', 251 | alias: '备份存储名2' 252 | }, 253 | { 254 | name: 'bucketName3', 255 | type: 'input', 256 | default: userConfig.backupName3, 257 | required: false, 258 | message: '您设定的远程存储的名称(备份位2)', 259 | alias: '备份存储名3' 260 | }, 261 | { 262 | name: 'localPostion', 263 | type: 'input', 264 | default: userConfig.localPostion, 265 | required: false, 266 | message: '/home/picgo-rclone or D:\\picgo-rclone', 267 | alias: '本地备份绝对路径' 268 | } 269 | ] 270 | } 271 | 272 | module.exports = (ctx:picgo) => { 273 | const register = () => { 274 | ctx.helper.uploader.register('rclone', { 275 | config, 276 | handle, 277 | name: "RCLONE" 278 | }) 279 | } 280 | 281 | return { 282 | register, 283 | uploader: 'RCLONE' 284 | } 285 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------