├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── License ├── README.md ├── index.ts ├── lib ├── helper.ts ├── interface.ts └── octokit.ts ├── package.json ├── test ├── __snapshots__ │ └── helper.test.ts.snap ├── helper.test.ts └── index.test.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store 4 | yarn-error.log 5 | temp.js 6 | package-lock.json 7 | data.json 8 | lib/develop.ts 9 | dev 10 | coverage 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.2.0](https://github.com/zWingz/picgo-plugin-github-plus/compare/v1.1.9...v1.2.0) (2019-06-17) 2 | 3 | 4 | ### Features 5 | 6 | * 🎸 support for gitee ([b84ea0d](https://github.com/zWingz/picgo-plugin-github-plus/commit/b84ea0d)) 7 | 8 | 9 | 10 | ## [1.1.9](https://github.com/zWingz/picgo-plugin-github-plus/compare/v1.1.8...v1.1.9) (2019-06-11) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **Remove:** fix remove path, closed [#9](https://github.com/zWingz/picgo-plugin-github-plus/issues/9) ([95fc7aa](https://github.com/zWingz/picgo-plugin-github-plus/commit/95fc7aa)) 16 | 17 | 18 | 19 | ## [1.1.8](https://github.com/zWingz/picgo-plugin-github-plus/compare/v1.1.7...v1.1.8) (2019-05-31) 20 | 21 | 22 | ### Features 23 | 24 | * **Picgo:** add logger ([5973a11](https://github.com/zWingz/picgo-plugin-github-plus/commit/5973a11)) 25 | 26 | 27 | 28 | ## [1.1.7](https://github.com/zWingz/picgo-plugin-github-plus/compare/v1.1.6...v1.1.7) (2019-05-11) 29 | 30 | 31 | ### Features 32 | 33 | * **Config:** create new instance when config changed ([39bcb99](https://github.com/zWingz/picgo-plugin-github-plus/commit/39bcb99)) 34 | 35 | 36 | 37 | ## [1.1.6](https://github.com/zWingz/picgo-plugin-github-plus/compare/v1.1.5...v1.1.6) (2019-03-20) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * 🐛 fix unzip call ([3e3bec1](https://github.com/zWingz/picgo-plugin-github-plus/commit/3e3bec1)) 43 | 44 | 45 | ### Performance Improvements 46 | 47 | * ⚡️ rm zip key-value ([959190d](https://github.com/zWingz/picgo-plugin-github-plus/commit/959190d)) 48 | 49 | 50 | 51 | ## [1.1.5](https://github.com/zWingz/picgo-plugin-github-plus/compare/v1.1.4...v1.1.5) (2019-03-16) 52 | 53 | 54 | ### Features 55 | 56 | * 🎸 skip img when upload error ([e0ebd9b](https://github.com/zWingz/picgo-plugin-github-plus/commit/e0ebd9b)) 57 | 58 | 59 | 60 | ## [1.1.4](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.1.3...v1.1.4) (2019-03-15) 61 | 62 | 63 | 64 | ## [1.1.3](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.1.2...1.1.3) (2019-03-15) 65 | 66 | 67 | ### Features 68 | 69 | * 🎸 bugfix and add test ([43acdbf](https://github.com/zWingz/picgo-plugin-github-plus/commit/43acdbf)) 70 | 71 | 72 | 73 | ## [1.1.2](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.1.1...1.1.2) (2019-03-08) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * 🐛 fix slash in window / fix customUrl generate ([0fd147c](https://github.com/zWingz/picgo-plugin-github-plus/commit/0fd147c)) 79 | 80 | 81 | 82 | ## [1.1.1](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.1.0...1.1.1) (2019-01-21) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * 🐛 fix origin download url ([b6205b2](https://github.com/zWingz/picgo-plugin-github-plus/commit/b6205b2)), closes [#1](https://github.com/zWingz/picgo-plugin-github-plus/issues/1) 88 | 89 | 90 | 91 | # [1.1.0](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.0.3...1.1.0) (2019-01-19) 92 | 93 | 94 | ### Features 95 | 96 | * 🎸 pull from github ([27c4394](https://github.com/zWingz/picgo-plugin-github-plus/commit/27c4394)) 97 | * 🎸 support pull from github ([474945f](https://github.com/zWingz/picgo-plugin-github-plus/commit/474945f)) 98 | * 🎸 support pull github ([2aaf151](https://github.com/zWingz/picgo-plugin-github-plus/commit/2aaf151)) 99 | * 🎸 support sync remove ([325f52c](https://github.com/zWingz/picgo-plugin-github-plus/commit/325f52c)) 100 | * 🎸 support sync remove file ([8110d04](https://github.com/zWingz/picgo-plugin-github-plus/commit/8110d04)) 101 | 102 | 103 | 104 | ## [1.0.3](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.0.2...1.0.3) (2019-01-15) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * 🐛 fix devDependencies ([318c8a1](https://github.com/zWingz/picgo-plugin-github-plus/commit/318c8a1)) 110 | 111 | 112 | 113 | ## [1.0.2](https://github.com/zWingz/picgo-plugin-github-plus/compare/1.0.1...1.0.2) (2019-01-15) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * 🐛 package.json files ([81e8864](https://github.com/zWingz/picgo-plugin-github-plus/commit/81e8864)) 119 | 120 | 121 | 122 | # [1.0.0](https://github.com/zWingz/picgo-plugin-github-plus/compare/e3260c4...1.0.0) (2019-01-15) 123 | 124 | 125 | ### Features 126 | 127 | * 🎸 init project ([e3260c4](https://github.com/zWingz/picgo-plugin-github-plus/commit/e3260c4)) 128 | * 🎸 release ([114dcfa](https://github.com/zWingz/picgo-plugin-github-plus/commit/114dcfa)) 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 zhengzwing@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # picgo-plugin-github-plus 2 | 3 | plugin for [PicGo](https://github.com/Molunerfinn/PicGo) 4 | 5 | - Sync `uploaded` with github use `data.json` 6 | - Sync `remove` action 7 | - Pull `img` info from github 8 | 9 | **Don't edit `lastSync`** 10 | 11 | ## Usage 12 | 13 | ### Config 14 | 15 | - repo: repo name, split by '/', eg: `owner/repoName` 16 | - branch: default `master` 17 | - token: github `access token` 18 | - path: file path 19 | - customUrl: used to insead of `https://raw.githubusercontent.com/:owner/:repo/:branch/:path/:filename`, eg: `${customUrl}/path/filename.jpg` 20 | - origin: `github` or `gitee`, default `github` 21 | 22 | makesure the `customUrl` can access your `repo` 23 | 24 | ![](https://zwing.site/imgur/57566062-a7752000-73fa-11e9-99c1-e3a0562bc41d.png) 25 | 26 | ### Menu 27 | 28 | - Sync origin: Just sync `data.json` (use latest updated) 29 | - Pull origin: Pull all `img` info from origin (**force** and **override** local `data.json`) 30 | 31 | ## Support gitee 32 | 33 | 由于`gitee`文件大小有`1mb`限制, 所以超过`1mb`的文件无法通过外链获取 34 | 35 | ## Related 36 | 37 | - [Hyrule](https://github.com/zWingz/Hyrule): A electron app to manage issues and images from github 38 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import picgo from 'picgo' 2 | import { getIns } from './lib/octokit' 3 | import { PluginConfig } from 'picgo/dist/utils/interfaces' 4 | import { getNow, zip, unzip } from './lib/helper' 5 | import { ImgType, PluginConfig as PlusConfig } from './lib/interface' 6 | const PluginName = 'picgo-plugin-github-plus' 7 | const UploaderName = 'githubPlus' 8 | function initOcto (ctx: picgo) { 9 | const options: PlusConfig = ctx.getConfig('picBed.githubPlus') 10 | if (!options) { 11 | throw new Error("Can't find github-plus config") 12 | } 13 | const ins = getIns(options) 14 | return ins 15 | } 16 | 17 | function notic (showNotification: Function, title: string, body?: string) { 18 | showNotification({ 19 | title: 'GithubPlus: ' + title, 20 | body 21 | }) 22 | } 23 | 24 | const SyncGithubMenu = { 25 | label: 'Sync origin', 26 | async handle (ctx: picgo, { showNotification }) { 27 | const octokit = initOcto(ctx) 28 | notic(showNotification, 'Sync origin...') 29 | const githubDataJson = await octokit.getDataJson().catch(e => { 30 | ctx.log.error(e) 31 | notic(showNotification, 'Error at load dataJson', e.message) 32 | throw e 33 | }) 34 | // FIXME: 新版本可能拿不到uploaded, 加个默认 35 | const uploaded: ImgType[] = ctx.getConfig('uploaded') || [] 36 | const localDataJson = { 37 | data: uploaded.filter(each => each.type === UploaderName).map(zip), 38 | lastSync: (ctx.getConfig(PluginName) || {}).lastSync 39 | } 40 | const { sha, lastSync, data } = githubDataJson 41 | if (localDataJson.lastSync > lastSync) { 42 | try { 43 | if (sha) { 44 | await octokit.updateDataJson({ 45 | data: localDataJson, 46 | sha 47 | }) 48 | } else { 49 | await octokit.createDataJson(localDataJson) 50 | } 51 | } catch (e) { 52 | ctx.log.error(e) 53 | notic(showNotification, 'Error at sync origin', e.message) 54 | throw e 55 | } 56 | } else { 57 | const newUploaded = data 58 | .map(each => { 59 | const obj = unzip(each) 60 | return { 61 | ...obj, 62 | type: UploaderName, 63 | imgUrl: octokit.parseUrl(obj.fileName) 64 | } 65 | }) 66 | .concat(uploaded.filter(each => each.type !== UploaderName)) 67 | ctx.saveConfig({ 68 | uploaded: newUploaded, 69 | [PluginName]: { 70 | lastSync 71 | } 72 | }) 73 | } 74 | notic(showNotification, 'Sync successful', 'Succeed to sync origin. Please reload PicGo') 75 | } 76 | } 77 | 78 | const PullGithubMenu = { 79 | label: 'Pull origin', 80 | handle: async (ctx: picgo, { showNotification }) => { 81 | const octokit = initOcto(ctx) 82 | notic(showNotification, 'Pull img from origin...') 83 | try { 84 | const { tree } = await octokit.getPathTree() 85 | const imgList: ImgType[] = tree 86 | .filter(each => /\.(jpg|png|jpeg|gif|webp)$/.test(each.path)) 87 | .map(each => { 88 | const unzipImg = unzip({ 89 | f: each.path, 90 | s: each.sha 91 | }) 92 | return { 93 | ...unzipImg, 94 | type: UploaderName, 95 | imgUrl: octokit.parseUrl(each.path) 96 | } 97 | }) 98 | const uploaded: ImgType[] = (ctx 99 | .getConfig('uploaded') || []) 100 | .filter(each => each.type !== UploaderName) 101 | uploaded.unshift(...imgList) 102 | ctx.saveConfig({ 103 | uploaded, 104 | [PluginName]: { 105 | lastSync: getNow() 106 | } 107 | }) 108 | notic(showNotification, 'Pull successful', 'Succeed to pull from origin, Please reload PicGo') 109 | } catch (e) { 110 | ctx.log.error(e) 111 | notic(showNotification, 'Error at pull from origin', e.message) 112 | } 113 | } 114 | } 115 | 116 | const guiMenu = ctx => { 117 | return [SyncGithubMenu, PullGithubMenu] 118 | } 119 | 120 | const handle = async (ctx: picgo) => { 121 | let output = ctx.output 122 | const octokit = initOcto(ctx) 123 | const ret = [] 124 | const len = output.length 125 | let index = 0 126 | async function up () { 127 | const img = output[index] 128 | if (index >= len) return 129 | if (!img) { 130 | index++ 131 | return up() 132 | } 133 | return octokit 134 | .upload(img) 135 | .then(({ imgUrl, sha }) => { 136 | img.imgUrl = imgUrl 137 | img.sha = sha 138 | ret.push(img) 139 | index++ 140 | return up() 141 | }) 142 | .catch(e => { 143 | ctx.log.error(e) 144 | ctx.emit('notification', { 145 | title: 'GithubPlus: 上传失败', 146 | body: e.message, 147 | text: '' 148 | }) 149 | index++ 150 | return up() 151 | }) 152 | } 153 | await up() 154 | ctx.saveConfig({ 155 | [PluginName]: { 156 | lastSync: getNow() 157 | } 158 | }) 159 | ctx.output = ret 160 | return ctx 161 | } 162 | 163 | async function onRemove (files: ImgType[], { showNotification }) { 164 | // console.log('1111 =?', this) 165 | const rms = files.filter(each => each.type === UploaderName) 166 | if (rms.length === 0) return 167 | const self: picgo = this 168 | const ins = initOcto(self) 169 | const fail = [] 170 | for (let i = 0; i < rms.length; i++) { 171 | const each = rms[i] 172 | await ins.removeFile(each).catch((e) => { 173 | self.log.error(e) 174 | fail.push(each) 175 | }) 176 | } 177 | if (fail.length) { 178 | // 确保主线程已经把文件从data.json删掉 179 | const uploaded: ImgType[] = self.getConfig('uploaded') || [] 180 | uploaded.unshift(...fail) 181 | self.saveConfig({ 182 | uploaded, 183 | [PluginName]: { 184 | lastSync: getNow() 185 | } 186 | }) 187 | } 188 | notic( 189 | showNotification, 190 | '删除提示', 191 | fail.length === 0 ? '成功同步删除' : `删除失败${fail.length}个` 192 | ) 193 | } 194 | 195 | const config = (ctx: picgo): PluginConfig[] => { 196 | let userConfig = ctx.getConfig(`picBed.${UploaderName}`) 197 | if (!userConfig) { 198 | userConfig = {} 199 | } 200 | const conf = [ 201 | { 202 | name: 'repo', 203 | type: 'input', 204 | default: userConfig.repo || '', 205 | required: true 206 | }, 207 | { 208 | name: 'branch', 209 | type: 'input', 210 | default: userConfig.branch || 'master', 211 | required: false 212 | }, 213 | { 214 | name: 'token', 215 | type: 'input', 216 | default: userConfig.token || '', 217 | required: true 218 | }, 219 | { 220 | name: 'path', 221 | type: 'input', 222 | default: userConfig.path || '', 223 | required: false 224 | }, 225 | { 226 | name: 'customUrl', 227 | type: 'input', 228 | default: userConfig.customUrl || '', 229 | required: false 230 | }, 231 | { 232 | name: 'origin', 233 | type: 'list', 234 | default: userConfig.type || 'github', 235 | required: true, 236 | choices: [{ 237 | name: 'github', 238 | value: 'github' 239 | }, { 240 | name: 'gitee', 241 | value: 'gitee' 242 | }] 243 | } 244 | ] 245 | return conf 246 | } 247 | const syncConfig = (ctx: picgo): PluginConfig[] => { 248 | let userConfig = ctx.getConfig(PluginName) 249 | if (!userConfig) { 250 | userConfig = {} 251 | } 252 | const conf = [ 253 | { 254 | name: 'lastSync', 255 | type: 'input', 256 | default: userConfig.lastSync || '', 257 | required: false 258 | } 259 | ] 260 | return conf 261 | } 262 | 263 | export = (ctx: picgo) => { 264 | const register = () => { 265 | // const { githubPlus } = ctx.getConfig('picBed') 266 | // if (!githubPlus.token) return 267 | // authenticate(githubPlus.token) 268 | ctx.helper.uploader.register(UploaderName, { handle, config }) 269 | ctx.on('remove', onRemove) 270 | } 271 | return { 272 | register, 273 | guiMenu, // <-- 在这里注册 274 | uploader: UploaderName, 275 | config: syncConfig 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /lib/helper.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { ImgType, ImgZipType } from './interface' 3 | import slash from 'normalize-path' 4 | import { join } from 'path' 5 | 6 | export function getNow () { 7 | return dayjs().format('YYYY-MM-DD hh:mm:ss') 8 | } 9 | 10 | export function zip (img: ImgType): ImgZipType { 11 | return { 12 | f: img.fileName, 13 | s: img.sha 14 | } 15 | } 16 | 17 | export function unzip (img: ImgZipType): ImgType { 18 | const { f: fileName, s } = img 19 | const extname = fileName.split('.').slice(-1)[0] 20 | return { 21 | fileName, 22 | id: s, 23 | sha: s, 24 | extname, 25 | imgUrl: '', 26 | type: '' 27 | } 28 | } 29 | 30 | export function pathJoin (...arg) { 31 | return slash(join.apply(null, arg)) 32 | } 33 | -------------------------------------------------------------------------------- /lib/interface.ts: -------------------------------------------------------------------------------- 1 | export interface PluginConfig { 2 | repo: string, 3 | branch?: string, 4 | path?: string, 5 | token: string, 6 | customUrl?: string 7 | origin?: 'github' | 'gitee' 8 | } 9 | 10 | export type ImgType = { 11 | fileName: string; 12 | extname: string; 13 | imgUrl: string; 14 | width?: number; 15 | height?: number; 16 | type: string; 17 | id: string; 18 | sha?: string 19 | } 20 | 21 | export type ImgZipType = { 22 | f: string, 23 | s: string 24 | } 25 | -------------------------------------------------------------------------------- /lib/octokit.ts: -------------------------------------------------------------------------------- 1 | import Octokit from '@octokit/rest' 2 | import { getNow, pathJoin } from './helper' 3 | import { PluginConfig, ImgType } from './interface' 4 | import urlJoin from 'url-join' 5 | import { ImgInfo } from 'picgo/dist/utils/interfaces' 6 | const GithubUrl = 'https://api.github.com' 7 | const GiteeUrl = 'https://gitee.com/api/v5' 8 | 9 | export class Octo { 10 | owner: string = '' 11 | repo: string = '' 12 | branch: string = '' 13 | path: string = '' 14 | token: string = '' 15 | customUrl: string = '' 16 | octokit: Octokit = null 17 | origin: PluginConfig['origin'] 18 | constructor ({ 19 | repo, 20 | branch, 21 | path = '', 22 | token, 23 | customUrl = '', 24 | origin = 'github' 25 | }: PluginConfig) { 26 | const [owner, r] = repo.split('/') 27 | if (!r) throw new Error('Error in repo name') 28 | this.owner = owner 29 | this.repo = r 30 | this.branch = branch || 'master' 31 | this.path = path 32 | this.token = token 33 | this.customUrl = customUrl 34 | this.origin = origin 35 | this.octokit = new Octokit({ 36 | baseUrl: origin === 'github' ? GithubUrl : GiteeUrl, 37 | auth: token ? `token ${token}` : undefined 38 | }) 39 | } 40 | get isGithub() { 41 | return this.origin === 'github' 42 | } 43 | async getTree (sha): Promise<{ path: string; sha: string }[]> { 44 | const { owner, repo } = this 45 | const d = await this.octokit.git.getTree({ 46 | owner, 47 | repo, 48 | tree_sha: sha 49 | }) 50 | const { tree } = d.data 51 | return tree 52 | } 53 | async getPathTree (): Promise<{ sha: string; tree: any[] }> { 54 | const { path } = this 55 | let tree = await this.getTree(this.branch) 56 | const arr = path.split('/').filter(each => each) 57 | let sha = this.branch 58 | for (let i = 0; i < arr.length; i++) { 59 | const item = tree.filter(each => arr[i].endsWith(each.path))[0] 60 | if (!item) return Promise.reject(new Error(`Can\'t find ${path}`)) 61 | sha = item.sha 62 | tree = await this.getTree(sha) 63 | } 64 | return { sha, tree } 65 | } 66 | createFile(params) { 67 | const { isGithub } = this 68 | const request = this.octokit.request(`/repos/:owner/:repo/contents/:path`, { 69 | method: isGithub ? 'PUT' : 'POST', 70 | ...params 71 | }) 72 | return request 73 | } 74 | async getDataJson (): Promise<{ 75 | lastSync: string 76 | data: any[] 77 | sha?: string 78 | }> { 79 | const { owner, repo } = this 80 | const defaultRet = { 81 | lastSync: '', 82 | data: [] 83 | } 84 | const { tree } = await this.getPathTree() 85 | const dataJson = tree.filter(each => each.path === 'data.json')[0] 86 | if (dataJson) { 87 | let content = await this.octokit.git.getBlob({ 88 | owner, 89 | repo, 90 | file_sha: dataJson.sha 91 | }) 92 | const buf = Buffer.from(content.data.content, content.data.encoding) 93 | const json = JSON.parse(buf.toString()) 94 | return { 95 | ...defaultRet, 96 | ...json, 97 | sha: dataJson.sha 98 | } 99 | } 100 | return defaultRet 101 | } 102 | updateDataJson ({ data, sha }) { 103 | const { owner, repo, branch, path } = this 104 | return this.octokit.repos.updateFile({ 105 | owner, 106 | branch, 107 | repo, 108 | path: pathJoin(path, 'data.json'), 109 | sha, 110 | message: `Sync dataJson by PicGo at ${getNow()}`, 111 | content: Buffer.from(JSON.stringify(data)).toString('base64') 112 | }) 113 | } 114 | createDataJson (data) { 115 | const { owner, repo, branch, path } = this 116 | return this.createFile({ 117 | owner, 118 | repo, 119 | branch, 120 | path: pathJoin(path, 'data.json'), 121 | message: `Sync dataJson by PicGo at ${getNow()}`, 122 | content: Buffer.from(JSON.stringify(data)).toString('base64') 123 | }) 124 | } 125 | async upload (img: ImgInfo) { 126 | /* istanbul ignore next */ 127 | const { owner, repo, branch, path = '' } = this 128 | const { fileName } = img 129 | const d = await this.createFile({ 130 | owner, 131 | repo, 132 | path: pathJoin(path, fileName), 133 | message: `Upload ${fileName} by picGo - ${getNow()}`, 134 | content: img.base64Image || Buffer.from(img.buffer).toString('base64'), 135 | branch 136 | }) 137 | if (d) { 138 | return { 139 | imgUrl: this.parseUrl(fileName), 140 | sha: d.data.content.sha 141 | } 142 | } 143 | /* istanbul ignore next */ 144 | throw d 145 | } 146 | removeFile (img: ImgType) { 147 | const { repo, path, owner, branch } = this 148 | return this.octokit.repos.deleteFile({ 149 | repo, 150 | owner, 151 | branch, 152 | path: urlJoin(path, img.fileName), 153 | message: `Deleted ${img.fileName} by PicGo - ${getNow()}`, 154 | sha: img.sha 155 | }) 156 | } 157 | parseUrl (fileName) { 158 | const { origin, owner, repo, path, customUrl, branch } = this 159 | if (customUrl) { 160 | return urlJoin(customUrl, path, fileName) 161 | } 162 | return origin === 'github' ? urlJoin( 163 | `https://raw.githubusercontent.com/`, 164 | owner, 165 | repo, 166 | branch, 167 | path, 168 | fileName 169 | ) : urlJoin(`https://gitee.com`, owner, repo, 'raw', branch, path, fileName) 170 | // https://gitee.com/zwing/test/raw/master/57566062-a7752000-73fa-11e9-99c1-e3a0562bc41d.png 171 | } 172 | } 173 | 174 | let ins: Octo = null 175 | let _cacheOption: string = '' 176 | export function getIns (config: PluginConfig): Octo { 177 | const str = JSON.stringify(config) 178 | if (ins && _cacheOption === str) return ins 179 | _cacheOption = str 180 | ins = new Octo(config) 181 | return ins 182 | } 183 | 184 | /* istanbul ignore next */ 185 | export function clearIns () { 186 | // just for test 187 | ins = null 188 | } 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "picgo-plugin-github-plus", 3 | "version": "1.2.0", 4 | "description": "picgo uploader for github", 5 | "gui": true, 6 | "main": "dist/index.js", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "files": [ 11 | "lib", 12 | "dist" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/zWingz/picgo-plugin-github-plus" 17 | }, 18 | "homepage": "https://github.com/zWingz/picgo-plugin-github-plus", 19 | "scripts": { 20 | "build": "tsc -p .", 21 | "dev": "tsc -w -p .", 22 | "clean": "rm -rf dist", 23 | "test": "jest", 24 | "pub": "yarn clean && yarn build && yarn publish", 25 | "changelog": "npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 26 | }, 27 | "keywords": [ 28 | "picgo", 29 | "picgo-plugin", 30 | "picgo-gui-plugin" 31 | ], 32 | "author": "zzwing ", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@types/jest": "^24.0.11", 36 | "@types/node": "^10.10.1", 37 | "jest": "^24.5.0", 38 | "nock": "^10.0.6", 39 | "picgo": "^1.2.1", 40 | "release-it": "^9.8.1", 41 | "ts-jest": "^24.0.0", 42 | "tslint": "^5.10.0", 43 | "tslint-config-standard": "^7.1.0", 44 | "typescript": "^3.0.3" 45 | }, 46 | "dependencies": { 47 | "@octokit/rest": "^16.18.1", 48 | "dayjs": "^1.8.0", 49 | "normalize-path": "^3.0.0", 50 | "url-join": "^4.0.0" 51 | }, 52 | "jest": { 53 | "moduleFileExtensions": [ 54 | "ts", 55 | "js" 56 | ], 57 | "collectCoverageFrom": [ 58 | "lib/*.ts" 59 | ], 60 | "coverageDirectory": "./coverage/", 61 | "collectCoverage": true, 62 | "transform": { 63 | "^.+\\.(ts|tsx)$": "ts-jest" 64 | }, 65 | "testMatch": [ 66 | "**/test/*.test.(ts|tsx)" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/__snapshots__/helper.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test getNow test getNow: now 1`] = `"2019-02-22 12:10:20"`; 4 | 5 | exports[`test helper test unzip: unzip 1`] = ` 6 | Object { 7 | "extname": "filename", 8 | "fileName": "filename", 9 | "id": "fooiuiouiouio", 10 | "imgUrl": "", 11 | "sha": "fooiuiouiouio", 12 | "type": "", 13 | } 14 | `; 15 | 16 | exports[`test helper test zip: zip 1`] = ` 17 | Object { 18 | "f": "filename", 19 | "s": "fooiuiouiouio", 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /test/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { pathJoin, zip, unzip, getNow } from '../lib/helper' 2 | describe('test helper', () => { 3 | it('test pathJson', () => { 4 | expect(pathJoin('path', 'to', 'dir')).toEqual('path/to/dir') 5 | expect(pathJoin('path', '../to', 'dir')).toEqual('to/dir') 6 | expect(pathJoin('path', '啦啦啦', '中文')).toEqual('path/啦啦啦/中文') 7 | }) 8 | 9 | it('test zip', () => { 10 | const str = zip({ 11 | fileName: 'filename', 12 | width: 1000, 13 | height: 1000, 14 | id: 'fdsafdsafdsafasdf', 15 | sha: 'fooiuiouiouio' 16 | } as any) 17 | expect(str).toMatchSnapshot('zip') 18 | }) 19 | it('test unzip', () => { 20 | const str = unzip({ 21 | f: 'filename', 22 | w: 1000, 23 | h: 1000, 24 | id: 'fdsafdsafdsafasdf', 25 | s: 'fooiuiouiouio' 26 | } as any) 27 | expect(str).toMatchSnapshot('unzip') 28 | }) 29 | }) 30 | describe('test getNow', () => { 31 | const date = global.Date 32 | beforeEach(() => { 33 | const mockedDate = new Date(Date.UTC(2019, 1, 21, 16, 10, 20)) 34 | global.Date = class extends Date { 35 | constructor () { 36 | super() 37 | return mockedDate 38 | } 39 | } as any 40 | }) 41 | afterEach(() => { 42 | global.Date = date 43 | }) 44 | it('test getNow', () => { 45 | const now = getNow() 46 | expect(now).toMatchSnapshot('now') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { getIns, clearIns } from '../lib/octokit' 2 | import { getNow } from '../lib/helper' 3 | import nock from 'nock' 4 | 5 | const date = global.Date 6 | beforeAll(() => { 7 | const mockedDate = new Date(Date.UTC(2019, 1, 21, 16, 10, 20)) 8 | global.Date = class extends Date { 9 | constructor () { 10 | super() 11 | return mockedDate 12 | } 13 | } as any 14 | }) 15 | afterAll(() => { 16 | global.Date = date 17 | }) 18 | 19 | describe('test instance', () => { 20 | const defaultConfig = { 21 | repo: 'zwingz/imgur' 22 | } as any 23 | beforeEach(() => { 24 | clearIns() 25 | }) 26 | it('throw error when repo is empty', () => { 27 | try { 28 | getIns({ repo: 'fdsaf' } as any) 29 | } catch (e) { 30 | expect(e.message).toEqual('Error in repo name') 31 | } 32 | }) 33 | it('singleton pattern', () => { 34 | const ins = getIns(defaultConfig) 35 | expect(ins).toEqual(getIns(defaultConfig)) 36 | }) 37 | it('test config', () => { 38 | const owner = 'fdsa' 39 | const repo = 'f12131' 40 | const branch = 'fdsafdsa' 41 | const token = '12309' 42 | const customUrl = 'qweqwe' 43 | const path = 'oijoi' 44 | const ins = getIns({ 45 | repo: `${owner}/${repo}`, 46 | branch, 47 | token, 48 | customUrl, 49 | path 50 | }) 51 | expect(ins.repo).toEqual(repo) 52 | expect(ins.owner).toEqual(owner) 53 | expect(ins.branch).toEqual(branch) 54 | expect(ins.path).toEqual(path) 55 | expect(ins.token).toEqual(token) 56 | expect(ins.customUrl).toEqual(customUrl) 57 | }) 58 | }) 59 | 60 | describe('test api', () => { 61 | const github = 'https://api.github.com' 62 | const defaultConfig = { 63 | repo: 'zwingz/imgur', 64 | path: 'mock' 65 | } as any 66 | let ins = getIns(defaultConfig) 67 | it('test getTree', async () => { 68 | const tree = { lalal: 123 } 69 | const sha = 'fdsaf' 70 | nock(github) 71 | .get(`/repos/${ins.owner}/${ins.repo}/git/trees/${sha}`) 72 | .reply(200, { tree }) 73 | const data = await ins.getTree(sha) 74 | expect(data).toEqual(tree) 75 | }) 76 | it('test getPathTree', async () => { 77 | const sha = 'fdsfdsafdas' 78 | const retTree = { pathToTree: '12312312' } 79 | nock(github) 80 | .get(`/repos/${ins.owner}/${ins.repo}/git/trees/${ins.branch}`) 81 | .reply(200, { tree: [{ path: 'mock', sha }] }) 82 | nock(github) 83 | .get(`/repos/${ins.owner}/${ins.repo}/git/trees/${sha}`) 84 | .reply(200, { tree: retTree }) 85 | const data = await ins.getPathTree() 86 | expect(data.sha).toEqual(sha) 87 | expect(data.tree).toEqual(retTree) 88 | }) 89 | it('reject when path not exist', () => { 90 | nock(github) 91 | .get(`/repos/${ins.owner}/${ins.repo}/git/trees/${ins.branch}`) 92 | .reply(200, { tree: [] }) 93 | return expect(ins.getPathTree()).rejects.toThrow( 94 | `Can\'t find ${defaultConfig.path}` 95 | ) 96 | }) 97 | it('test getDataJson', async () => { 98 | clearIns() 99 | const sha = 'fdsafa' 100 | const dataJson = JSON.stringify({ 101 | lastSync: 'fdsaf', 102 | data: ['fdsa', 'fdsaf'] 103 | }) 104 | defaultConfig.path = '' 105 | ins = getIns(defaultConfig) 106 | nock(github) 107 | .get(`/repos/${ins.owner}/${ins.repo}/git/trees/${ins.branch}`) 108 | .reply(200, { tree: [{ path: 'data.json', sha }] }) 109 | nock(github) 110 | .get(`/repos/${ins.owner}/${ins.repo}/git/blobs/${sha}`) 111 | .reply(200, { content: dataJson, encoding: 'utf-8' }) 112 | const data = await ins.getDataJson() 113 | expect(data).toMatchObject(JSON.parse(dataJson)) 114 | nock(github) 115 | .get(`/repos/${ins.owner}/${ins.repo}/git/trees/${ins.branch}`) 116 | .reply(200, { tree: [] }) 117 | const data2 = await ins.getDataJson() 118 | expect(data2).toMatchObject({ lastSync: '', data: [] }) 119 | }) 120 | it('test updateDataJson', async () => { 121 | // /repos/:owner/:repo/contents/:path 122 | const data = { lalal: 'fdsa' } 123 | const sha = 'fdsafa' 124 | nock(github) 125 | .put(`/repos/${ins.owner}/${ins.repo}/contents/data.json`, body => { 126 | expect(body.message).toEqual(`Sync dataJson by PicGo at ${getNow()}`) 127 | expect(body.sha).toEqual(sha) 128 | expect(body.content).toEqual( 129 | Buffer.from(JSON.stringify(data)).toString('base64') 130 | ) 131 | expect(body.branch).toEqual(ins.branch) 132 | return true 133 | }) 134 | .reply(200, {}) 135 | await ins.updateDataJson({ data, sha }) 136 | }) 137 | it('test createDataJson', async () => { 138 | const data = { lalal: 'fdsa' } 139 | nock(github) 140 | .put(`/repos/${ins.owner}/${ins.repo}/contents/data.json`, body => { 141 | expect(body.message).toEqual(`Sync dataJson by PicGo at ${getNow()}`) 142 | expect(body.content).toEqual( 143 | Buffer.from(JSON.stringify(data)).toString('base64') 144 | ) 145 | expect(body.branch).toEqual(ins.branch) 146 | return true 147 | }) 148 | .reply(200, {}) 149 | await ins.createDataJson(data) 150 | }) 151 | it('test upload', async () => { 152 | const filename = 'testfilename.jpg' 153 | const base64 = 'fdsafdsafdsa' 154 | const retSha = 'fdsaf' 155 | const url = `/repos/${ins.owner}/${ins.repo}/contents/${filename}` 156 | nock(github) 157 | .put(url, body => { 158 | expect(body.message).toEqual( 159 | `Upload ${filename} by picGo - ${getNow()}` 160 | ) 161 | expect(body.content).toEqual(base64) 162 | expect(body.branch).toEqual(ins.branch) 163 | return true 164 | }) 165 | .reply(200, { content: { sha: retSha } }) 166 | const data = await ins.upload({ 167 | fileName: filename, 168 | base64Image: base64 169 | }) 170 | expect(data.sha).toEqual(retSha) 171 | const buff = 'llalallafdsa' 172 | nock(github) 173 | .put(url, body => { 174 | expect(body.content).toEqual(Buffer.from(buff).toString('base64')) 175 | return true 176 | }) 177 | .reply(200, { content: { sha: retSha } }) 178 | await ins.upload({ 179 | fileName: filename, 180 | buffer: Buffer.from(buff) 181 | }) 182 | nock(github) 183 | .put(url, body => { 184 | return true 185 | }) 186 | .reply(500, { error: { sha: retSha } }) 187 | expect( 188 | ins.upload({ fileName: filename, base64Image: base64 }) 189 | ).rejects.toEqual(expect.anything()) 190 | }) 191 | it('test removeFile', async () => { 192 | const sha = 'fdsaf' 193 | const fileName = 'fdsa' 194 | const url = `/repos/${ins.owner}/${ins.repo}/contents/${fileName}` 195 | nock(github) 196 | .delete(url, body => { 197 | expect(body.sha).toEqual(sha) 198 | expect(body.message).toEqual( 199 | `Deleted ${fileName} by PicGo - ${getNow()}` 200 | ) 201 | return true 202 | }) 203 | .reply(200, {}) 204 | await ins.removeFile({ sha, fileName } as any) 205 | }) 206 | it('test parseUrl', () => { 207 | const customUrl = 'dfsafda' 208 | ins.customUrl = customUrl 209 | const fileName = 'lfdsa' 210 | let ret = ins.parseUrl(fileName) 211 | expect(ret).toEqual(`${customUrl}/${fileName}`) 212 | ins.customUrl = '' 213 | ret = ins.parseUrl(fileName) 214 | expect(ret).toEqual( 215 | `https://raw.githubusercontent.com/${ins.owner}/${ins.repo}/${ 216 | ins.branch 217 | }/${fileName}` 218 | ) 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /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 | "types": ["jest"], 13 | // It's shit. 14 | // "baseUrl": ".", 15 | // "paths": { 16 | // "@core/*": ["core/*"], 17 | // "@lib/*": ["lib/*"], 18 | // "@plugins/*": ["plugins/*"], 19 | // "@utils/*": ["utils/*"] 20 | // }, 21 | "lib": [ 22 | "es7", 23 | "es2015", 24 | "dom" 25 | ] 26 | }, 27 | "exclude": [ 28 | "./test", 29 | "dist" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------