From b3de7a4bc56c1731eb21a80020f3acf4b5a25c16 Mon Sep 17 00:00:00 2001 From: xream Date: Sat, 20 Jan 2024 05:33:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=B0=83=E6=95=B4=20?= =?UTF-8?q?Gist=20=E5=90=8C=E6=AD=A5=E9=80=BB=E8=BE=91;=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20GitLab=20Snippet=20=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/src/products/cron-sync-artifacts.js | 15 +- backend/src/restful/artifacts.js | 6 +- backend/src/restful/miscs.js | 7 +- backend/src/restful/settings.js | 83 ++++--- backend/src/restful/sync.js | 28 ++- backend/src/utils/gist.js | 230 ++++++++++++++++---- backend/src/vendor/open-api.js | 11 + 8 files changed, 302 insertions(+), 80 deletions(-) diff --git a/backend/package.json b/backend/package.json index 37dcf88..0ec1eb4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.186", + "version": "2.14.187", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "main": "src/main.js", "scripts": { diff --git a/backend/src/products/cron-sync-artifacts.js b/backend/src/products/cron-sync-artifacts.js index 8ea95dc..c1dc261 100644 --- a/backend/src/products/cron-sync-artifacts.js +++ b/backend/src/products/cron-sync-artifacts.js @@ -54,9 +54,18 @@ async function doSync() { if (artifact.sync) { artifact.updated = new Date().getTime(); // extract real url from gist - artifact.url = body.files[ - encodeURIComponent(artifact.name) - ]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); + let files = body.files; + let isGitLab; + if (Array.isArray(files)) { + isGitLab = true; + files = Object.fromEntries( + files.map((item) => [item.path, item]), + ); + } + const url = files[encodeURIComponent(artifact.name)]?.raw_url; + artifact.url = isGitLab + ? url + : url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); } } diff --git a/backend/src/restful/artifacts.js b/backend/src/restful/artifacts.js index 8f2b75d..ef6a740 100644 --- a/backend/src/restful/artifacts.js +++ b/backend/src/restful/artifacts.js @@ -35,13 +35,14 @@ export default function register($app) { async function restoreArtifacts(_, res) { $.info('开始恢复远程配置...'); try { - const { gistToken } = $.read(SETTINGS_KEY); + const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); if (!gistToken) { return Promise.reject('未设置 GitHub Token!'); } const manager = new Gist({ token: gistToken, key: ARTIFACT_REPOSITORY_KEY, + syncPlatform, }); try { @@ -243,13 +244,14 @@ function validateArtifactName(name) { } async function syncToGist(files) { - const { gistToken } = $.read(SETTINGS_KEY); + const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); if (!gistToken) { return Promise.reject('未设置 GitHub Token!'); } const manager = new Gist({ token: gistToken, key: ARTIFACT_REPOSITORY_KEY, + syncPlatform, }); return manager.upload(files); } diff --git a/backend/src/restful/miscs.js b/backend/src/restful/miscs.js index 6b62c48..30a0ffa 100644 --- a/backend/src/restful/miscs.js +++ b/backend/src/restful/miscs.js @@ -1,7 +1,7 @@ import $ from '@/core/app'; import { ENV } from '@/vendor/open-api'; import { failed, success } from '@/restful/response'; -import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings'; +import { updateArtifactStore, updateAvatar } from '@/restful/settings'; import resourceCache from '@/utils/resource-cache'; import { GIST_BACKUP_FILE_NAME, @@ -68,7 +68,7 @@ function getEnv(req, res) { async function refresh(_, res) { // 1. get GitHub avatar and artifact store - await updateGitHubAvatar(); + await updateAvatar(); await updateArtifactStore(); // 2. clear resource cache @@ -79,7 +79,7 @@ async function refresh(_, res) { async function gistBackup(req, res) { const { action } = req.query; // read token - const { gistToken } = $.read(SETTINGS_KEY); + const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); if (!gistToken) { failed( res, @@ -92,6 +92,7 @@ async function gistBackup(req, res) { const gist = new Gist({ token: gistToken, key: GIST_BACKUP_KEY, + syncPlatform, }); try { let content; diff --git a/backend/src/restful/settings.js b/backend/src/restful/settings.js index 13d60e5..c880e43 100644 --- a/backend/src/restful/settings.js +++ b/backend/src/restful/settings.js @@ -18,7 +18,7 @@ async function getSettings(req, res) { $.write(settings, SETTINGS_KEY); } - if (!settings.avatarUrl) await updateGitHubAvatar(); + if (!settings.avatarUrl) await updateAvatar(); if (!settings.artifactStore) await updateArtifactStore(); success(res, settings); @@ -43,7 +43,7 @@ async function updateSettings(req, res) { ...req.body, }; $.write(newSettings, SETTINGS_KEY); - await updateGitHubAvatar(); + await updateAvatar(); await updateArtifactStore(); success(res, newSettings); } catch (e) { @@ -59,28 +59,57 @@ async function updateSettings(req, res) { } } -export async function updateGitHubAvatar() { +export async function updateAvatar() { const settings = $.read(SETTINGS_KEY); - const username = settings.githubUser; + const { githubUser: username, syncPlatform } = settings; if (username) { - try { - const data = await $.http - .get({ - url: `https://api.github.com/users/${username}`, - headers: { - 'User-Agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', - }, - }) - .then((resp) => JSON.parse(resp.body)); - settings.avatarUrl = data['avatar_url']; - $.write(settings, SETTINGS_KEY); - } catch (err) { - $.error( - `Failed to fetch GitHub avatar for User: ${username}. Reason: ${ - err.message ?? err - }`, - ); + if (syncPlatform === 'gitlab') { + try { + const data = await $.http + .get({ + url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent( + username, + )}`, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', + }, + }) + .then((resp) => JSON.parse(resp.body)); + settings.avatarUrl = data[0]['avatar_url'].replace( + /(\?|&)s=\d+(&|$)/, + '$1s=160$2', + ); + $.write(settings, SETTINGS_KEY); + } catch (err) { + $.error( + `Failed to fetch GitLab avatar for User: ${username}. Reason: ${ + err.message ?? err + }`, + ); + } + } else { + try { + const data = await $.http + .get({ + url: `https://api.github.com/users/${encodeURIComponent( + username, + )}`, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', + }, + }) + .then((resp) => JSON.parse(resp.body)); + settings.avatarUrl = data['avatar_url']; + $.write(settings, SETTINGS_KEY); + } catch (err) { + $.error( + `Failed to fetch GitHub avatar for User: ${username}. Reason: ${ + err.message ?? err + }`, + ); + } } } } @@ -88,19 +117,21 @@ export async function updateGitHubAvatar() { export async function updateArtifactStore() { $.log('Updating artifact store'); const settings = $.read(SETTINGS_KEY); - const { gistToken } = settings; + const { gistToken, syncPlatform } = settings; if (gistToken) { const manager = new Gist({ token: gistToken, key: ARTIFACT_REPOSITORY_KEY, + syncPlatform, }); try { const gist = await manager.locate(); - if (gist?.html_url) { - $.log(`找到 Sub-Store Gist: ${gist.html_url}`); + const url = gist?.html_url ?? gist?.web_url; + if (url) { + $.log(`找到 Sub-Store Gist: ${url}`); // 只需要保证 token 是对的, 现在 username 错误只会导致头像错误 - settings.artifactStore = gist.html_url; + settings.artifactStore = url; settings.artifactStoreStatus = 'VALID'; } else { $.error(`找不到 Sub-Store Gist`); diff --git a/backend/src/restful/sync.js b/backend/src/restful/sync.js index 378aa8c..1256d95 100644 --- a/backend/src/restful/sync.js +++ b/backend/src/restful/sync.js @@ -492,9 +492,18 @@ async function syncArtifacts() { if (artifact.sync) { artifact.updated = new Date().getTime(); // extract real url from gist - artifact.url = body.files[ - encodeURIComponent(artifact.name) - ]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); + let files = body.files; + let isGitLab; + if (Array.isArray(files)) { + isGitLab = true; + files = Object.fromEntries( + files.map((item) => [item.path, item]), + ); + } + const url = files[encodeURIComponent(artifact.name)]?.raw_url; + artifact.url = isGitLab + ? url + : url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); } } @@ -582,9 +591,16 @@ async function syncArtifact(req, res) { }); artifact.updated = new Date().getTime(); const body = JSON.parse(resp.body); - artifact.url = body.files[ - encodeURIComponent(artifact.name) - ]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); + let files = body.files; + let isGitLab; + if (Array.isArray(files)) { + isGitLab = true; + files = Object.fromEntries(files.map((item) => [item.path, item])); + } + const url = files[encodeURIComponent(artifact.name)]?.raw_url; + artifact.url = isGitLab + ? url + : url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); $.write(allArtifacts, ARTIFACTS_KEY); success(res, artifact); } catch (err) { diff --git a/backend/src/utils/gist.js b/backend/src/utils/gist.js index cea9706..20e4cd1 100644 --- a/backend/src/utils/gist.js +++ b/backend/src/utils/gist.js @@ -4,64 +4,216 @@ import { HTTP } from '@/vendor/open-api'; * Gist backup */ export default class Gist { - constructor({ token, key }) { - this.http = HTTP({ - baseURL: 'https://api.github.com', - headers: { + constructor({ token, key, syncPlatform }) { + if (syncPlatform === 'gitlab') { + this.headers = { + 'PRIVATE-TOKEN': `${token}`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', + }; + this.http = HTTP({ + baseURL: 'https://gitlab.com/api/v4', + headers: { ...this.headers }, + events: { + onResponse: (resp) => { + if (/^[45]/.test(String(resp.statusCode))) { + const body = JSON.parse(resp.body); + return Promise.reject( + `ERROR: ${body.message?.error ?? body.message}`, + ); + } else { + return resp; + } + }, + }, + }); + } else { + this.headers = { Authorization: `token ${token}`, 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', - }, - events: { - onResponse: (resp) => { - if (/^[45]/.test(String(resp.statusCode))) { - return Promise.reject( - `ERROR: ${JSON.parse(resp.body).message}`, - ); - } else { - return resp; - } + }; + this.http = HTTP({ + baseURL: 'https://api.github.com', + headers: { ...this.headers }, + events: { + onResponse: (resp) => { + if (/^[45]/.test(String(resp.statusCode))) { + return Promise.reject( + `ERROR: ${JSON.parse(resp.body).message}`, + ); + } else { + return resp; + } + }, }, - }, - }); + }); + } + this.key = key; + this.syncPlatform = syncPlatform; } async locate() { - return this.http.get('/gists').then((response) => { - const gists = JSON.parse(response.body); - for (let g of gists) { - if (g.description === this.key) { - return g; + if (this.syncPlatform === 'gitlab') { + return this.http.get('/snippets').then((response) => { + const gists = JSON.parse(response.body); + + for (let g of gists) { + if (g.title === this.key) { + return g; + } } - } - return; - }); + return; + }); + } else { + return this.http.get('/gists').then((response) => { + const gists = JSON.parse(response.body); + for (let g of gists) { + if (g.description === this.key) { + return g; + } + } + return; + }); + } } - async upload(files) { - if (Object.keys(files).length === 0) { + async upload(input) { + if (Object.keys(input).length === 0) { return Promise.reject('未提供需上传的文件'); } const gist = await this.locate(); + let files = input; + if (gist?.id) { - // update an existing gist - return this.http.patch({ - url: `/gists/${gist.id}`, - body: JSON.stringify({ files }), + if (this.syncPlatform === 'gitlab') { + gist.files = gist.files.reduce((acc, item) => { + acc[item.path] = item; + return acc; + }, {}); + } + // console.log(`files`, files); + // console.log(`gist`, gist.files); + let actions = []; + const result = { ...gist.files }; + Object.keys(files).map((key) => { + if (result[key]) { + if ( + files[key].content == null || + files[key].content === '' + ) { + delete result[key]; + actions.push({ + action: 'delete', + file_path: key, + }); + } else { + result[key] = files[key]; + actions.push({ + action: 'update', + file_path: key, + content: files[key].content, + }); + } + } else { + if ( + files[key].content == null || + files[key].content === '' + ) { + delete result[key]; + delete files[key]; + } else { + result[key] = files[key]; + actions.push({ + action: 'create', + file_path: key, + content: files[key].content, + }); + } + } }); + console.log(`result`, result); + console.log(`files`, files); + console.log(`actions`, actions); + + if (this.syncPlatform === 'gitlab') { + if (Object.keys(result).length === 0) { + return Promise.reject( + '本次操作将导致所有文件的内容都为空, 无法更新 snippet', + ); + } + if (Object.keys(result).length > 10) { + return Promise.reject( + '本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet', + ); + } + files = actions; + return this.http.put({ + headers: { + ...this.headers, + 'Content-Type': 'application/json', + }, + url: `/snippets/${gist.id}`, + body: JSON.stringify({ files }), + }); + } else { + if (Object.keys(result).length === 0) { + return Promise.reject( + '本次操作将导致所有文件的内容都为空, 无法更新 gist', + ); + } + return this.http.patch({ + url: `/gists/${gist.id}`, + body: JSON.stringify({ files }), + }); + } } else { - // create a new gist for backup - return this.http.post({ - url: '/gists', - body: JSON.stringify({ - description: this.key, - public: false, - files, - }), - }); + files = Object.entries(files).reduce((acc, [key, file]) => { + if (file.content !== null && file.content !== '') { + acc[key] = file; + } + return acc; + }, {}); + if (this.syncPlatform === 'gitlab') { + if (Object.keys(files).length === 0) { + return Promise.reject( + '所有文件的内容都为空, 无法创建 snippet', + ); + } + files = Object.keys(files).map((key) => ({ + file_path: key, + content: files[key].content, + })); + return this.http.post({ + headers: { + ...this.headers, + 'Content-Type': 'application/json', + }, + url: '/snippets', + body: JSON.stringify({ + title: this.key, + visibility: 'private', + files, + }), + }); + } else { + if (Object.keys(files).length === 0) { + return Promise.reject( + '所有文件的内容都为空, 无法创建 gist', + ); + } + return this.http.post({ + url: '/gists', + body: JSON.stringify({ + description: this.key, + public: false, + files, + }), + }); + } } } diff --git a/backend/src/vendor/open-api.js b/backend/src/vendor/open-api.js index 6f30858..0d19678 100644 --- a/backend/src/vendor/open-api.js +++ b/backend/src/vendor/open-api.js @@ -314,6 +314,17 @@ export function HTTP(defaultOptions = { baseURL: '' }) { request[method.toLowerCase()]( options, (err, response, body) => { + // if (err) { + // console.log(err); + // } else { + // console.log({ + // statusCode: + // response.status || response.statusCode, + // headers: response.headers, + // body, + // }); + // } + if (err) reject(err); else resolve({