From 687ed49f9dd4f10dc6256466f4c869beef492d9d Mon Sep 17 00:00:00 2001 From: lidong Date: Tue, 24 Feb 2026 14:22:26 +0800 Subject: [PATCH] Add year-month segmented folders for the Github image hosting. --- index.ts | 17 +++++++++++++---- lib/interface.ts | 2 ++ lib/octokit.ts | 42 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/index.ts b/index.ts index b443042..e1cf20a 100644 --- a/index.ts +++ b/index.ts @@ -81,9 +81,11 @@ const PullGithubMenu = { const octokit = initOcto(ctx) notic(showNotification, 'Pull img from origin...') try { - const { tree } = await octokit.getPathTree() + const { tree } = octokit.pathByDate + ? await octokit.getPathTreeRecursive() + : await octokit.getPathTree() const imgList: ImgType[] = tree - .filter(each => /\.(jpg|png|jpeg|gif|webp)$/.test(each.path)) + .filter(each => /\.(jpg|png|jpeg|gif|webp)$/i.test(each.path)) .map(each => { const unzipImg = unzip({ f: each.path, @@ -132,9 +134,10 @@ const handle = async (ctx: picgo) => { } return octokit .upload(img) - .then(({ imgUrl, sha }) => { + .then(({ imgUrl, sha, fileName: effectiveFileName }) => { img.imgUrl = imgUrl img.sha = sha + if (effectiveFileName) img.fileName = effectiveFileName ret.push(img) index++ return up() @@ -222,6 +225,12 @@ const config = (ctx: picgo): PluginConfig[] => { default: userConfig.path || '', required: false }, + { + name: 'pathByDate', + type: 'confirm', + default: userConfig.pathByDate ?? false, + required: false + }, { name: 'customUrl', type: 'input', @@ -231,7 +240,7 @@ const config = (ctx: picgo): PluginConfig[] => { { name: 'origin', type: 'list', - default: userConfig.type || 'github', + default: userConfig.origin || 'github', required: true, choices: [{ name: 'github', diff --git a/lib/interface.ts b/lib/interface.ts index 04c377d..37cb668 100644 --- a/lib/interface.ts +++ b/lib/interface.ts @@ -5,6 +5,8 @@ export interface PluginConfig { token: string, customUrl?: string origin?: 'github' | 'gitee' + /** When true, upload to path/year/month (e.g. images/2026/01) */ + pathByDate?: boolean } export type ImgType = { diff --git a/lib/octokit.ts b/lib/octokit.ts index cdde5bc..70ea109 100644 --- a/lib/octokit.ts +++ b/lib/octokit.ts @@ -1,4 +1,5 @@ import Octokit from '@octokit/rest' +import dayjs from 'dayjs' import { getNow, pathJoin } from './helper' import { PluginConfig, ImgType } from './interface' import urlJoin from 'url-join' @@ -15,13 +16,15 @@ export class Octo { customUrl: string = '' octokit: Octokit = null origin: PluginConfig['origin'] + pathByDate: boolean = false constructor ({ repo, branch, path = '', token, customUrl = '', - origin = 'github' + origin = 'github', + pathByDate = false }: PluginConfig) { const [owner, r] = repo.split('/') if (!r) throw new Error('Error in repo name') @@ -32,6 +35,7 @@ export class Octo { this.token = token this.customUrl = customUrl this.origin = origin + this.pathByDate = pathByDate this.octokit = new Octokit({ baseUrl: origin === 'github' ? GithubUrl : GiteeUrl, auth: token ? `token ${token}` : undefined @@ -63,6 +67,29 @@ export class Octo { } return { sha, tree } } + /** Recursively get all files under path (for Pull when pathByDate). Returns flat list with paths relative to this.path. */ + async getPathTreeRecursive (): Promise<{ tree: { path: string; sha: string }[] }> { + const { tree } = await this.getPathTree() + const flat: { path: string; sha: string }[] = [] + const collect = async (sha: string, prefix: string): Promise => { + const list = await this.getTree(sha) + for (const item of list) { + if (item.type === 'tree') { + await collect(item.sha, prefix + item.path + '/') + } else { + flat.push({ path: prefix + item.path, sha: item.sha }) + } + } + } + for (const item of tree) { + if (item.type === 'tree') { + await collect(item.sha, item.path + '/') + } else { + flat.push({ path: item.path, sha: item.sha }) + } + } + return { tree: flat } + } createFile(params) { const { isGithub } = this const request = this.octokit.request(`/repos/:owner/:repo/contents/:path`, { @@ -122,22 +149,27 @@ export class Octo { content: Buffer.from(JSON.stringify(data)).toString('base64') }) } - async upload (img: ImgInfo) { + async upload (img: ImgInfo): Promise<{ imgUrl: string; sha: string; fileName?: string }> { /* istanbul ignore next */ const { owner, repo, branch, path = '' } = this const { fileName } = img + const effectiveRelativePath = this.pathByDate + ? pathJoin(dayjs().format('YYYY'), dayjs().format('MM'), fileName) + : fileName + const filePath = pathJoin(path, effectiveRelativePath) const d = await this.createFile({ owner, repo, - path: pathJoin(path, fileName), + path: filePath, message: `Upload ${fileName} by picGo - ${getNow()}`, content: img.base64Image || Buffer.from(img.buffer).toString('base64'), branch }) if (d) { return { - imgUrl: this.parseUrl(fileName), - sha: d.data.content.sha + imgUrl: this.parseUrl(effectiveRelativePath), + sha: d.data.content.sha, + fileName: this.pathByDate ? effectiveRelativePath : undefined } } /* istanbul ignore next */