feat: 优化调整 Gist 同步逻辑; 增加 GitLab Snippet 同步

This commit is contained in:
xream 2024-01-20 05:33:31 +08:00
parent 099ae5ad83
commit b3de7a4bc5
No known key found for this signature in database
GPG Key ID: 1D2C5225471789F9
8 changed files with 302 additions and 80 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.186", "version": "2.14.187",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {

View File

@ -54,9 +54,18 @@ async function doSync() {
if (artifact.sync) { if (artifact.sync) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[ let files = body.files;
encodeURIComponent(artifact.name) let isGitLab;
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); 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');
} }
} }

View File

@ -35,13 +35,14 @@ export default function register($app) {
async function restoreArtifacts(_, res) { async function restoreArtifacts(_, res) {
$.info('开始恢复远程配置...'); $.info('开始恢复远程配置...');
try { try {
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
return Promise.reject('未设置 GitHub Token'); return Promise.reject('未设置 GitHub Token');
} }
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
}); });
try { try {
@ -243,13 +244,14 @@ function validateArtifactName(name) {
} }
async function syncToGist(files) { async function syncToGist(files) {
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
return Promise.reject('未设置 GitHub Token'); return Promise.reject('未设置 GitHub Token');
} }
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
}); });
return manager.upload(files); return manager.upload(files);
} }

View File

@ -1,7 +1,7 @@
import $ from '@/core/app'; import $ from '@/core/app';
import { ENV } from '@/vendor/open-api'; import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response'; 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 resourceCache from '@/utils/resource-cache';
import { import {
GIST_BACKUP_FILE_NAME, GIST_BACKUP_FILE_NAME,
@ -68,7 +68,7 @@ function getEnv(req, res) {
async function refresh(_, res) { async function refresh(_, res) {
// 1. get GitHub avatar and artifact store // 1. get GitHub avatar and artifact store
await updateGitHubAvatar(); await updateAvatar();
await updateArtifactStore(); await updateArtifactStore();
// 2. clear resource cache // 2. clear resource cache
@ -79,7 +79,7 @@ async function refresh(_, res) {
async function gistBackup(req, res) { async function gistBackup(req, res) {
const { action } = req.query; const { action } = req.query;
// read token // read token
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
failed( failed(
res, res,
@ -92,6 +92,7 @@ async function gistBackup(req, res) {
const gist = new Gist({ const gist = new Gist({
token: gistToken, token: gistToken,
key: GIST_BACKUP_KEY, key: GIST_BACKUP_KEY,
syncPlatform,
}); });
try { try {
let content; let content;

View File

@ -18,7 +18,7 @@ async function getSettings(req, res) {
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} }
if (!settings.avatarUrl) await updateGitHubAvatar(); if (!settings.avatarUrl) await updateAvatar();
if (!settings.artifactStore) await updateArtifactStore(); if (!settings.artifactStore) await updateArtifactStore();
success(res, settings); success(res, settings);
@ -43,7 +43,7 @@ async function updateSettings(req, res) {
...req.body, ...req.body,
}; };
$.write(newSettings, SETTINGS_KEY); $.write(newSettings, SETTINGS_KEY);
await updateGitHubAvatar(); await updateAvatar();
await updateArtifactStore(); await updateArtifactStore();
success(res, newSettings); success(res, newSettings);
} catch (e) { } catch (e) {
@ -59,14 +59,42 @@ async function updateSettings(req, res) {
} }
} }
export async function updateGitHubAvatar() { export async function updateAvatar() {
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
const username = settings.githubUser; const { githubUser: username, syncPlatform } = settings;
if (username) { if (username) {
if (syncPlatform === 'gitlab') {
try { try {
const data = await $.http const data = await $.http
.get({ .get({
url: `https://api.github.com/users/${username}`, 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: { headers: {
'User-Agent': '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', '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',
@ -84,23 +112,26 @@ export async function updateGitHubAvatar() {
} }
} }
} }
}
export async function updateArtifactStore() { export async function updateArtifactStore() {
$.log('Updating artifact store'); $.log('Updating artifact store');
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
const { gistToken } = settings; const { gistToken, syncPlatform } = settings;
if (gistToken) { if (gistToken) {
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY, key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
}); });
try { try {
const gist = await manager.locate(); const gist = await manager.locate();
if (gist?.html_url) { const url = gist?.html_url ?? gist?.web_url;
$.log(`找到 Sub-Store Gist: ${gist.html_url}`); if (url) {
$.log(`找到 Sub-Store Gist: ${url}`);
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误 // 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
settings.artifactStore = gist.html_url; settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID'; settings.artifactStoreStatus = 'VALID';
} else { } else {
$.error(`找不到 Sub-Store Gist`); $.error(`找不到 Sub-Store Gist`);

View File

@ -492,9 +492,18 @@ async function syncArtifacts() {
if (artifact.sync) { if (artifact.sync) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[ let files = body.files;
encodeURIComponent(artifact.name) let isGitLab;
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); 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(); artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
artifact.url = body.files[ let files = body.files;
encodeURIComponent(artifact.name) let isGitLab;
]?.raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); 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); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact); success(res, artifact);
} catch (err) { } catch (err) {

View File

@ -4,14 +4,38 @@ import { HTTP } from '@/vendor/open-api';
* Gist backup * Gist backup
*/ */
export default class Gist { export default class Gist {
constructor({ token, key }) { 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({ this.http = HTTP({
baseURL: 'https://api.github.com', baseURL: 'https://gitlab.com/api/v4',
headers: { 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}`, Authorization: `token ${token}`,
'User-Agent': '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', '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://api.github.com',
headers: { ...this.headers },
events: { events: {
onResponse: (resp) => { onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) { if (/^[45]/.test(String(resp.statusCode))) {
@ -24,10 +48,25 @@ export default class Gist {
}, },
}, },
}); });
}
this.key = key; this.key = key;
this.syncPlatform = syncPlatform;
} }
async locate() { async locate() {
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;
});
} else {
return this.http.get('/gists').then((response) => { return this.http.get('/gists').then((response) => {
const gists = JSON.parse(response.body); const gists = JSON.parse(response.body);
for (let g of gists) { for (let g of gists) {
@ -38,22 +77,134 @@ export default class Gist {
return; return;
}); });
} }
}
async upload(files) { async upload(input) {
if (Object.keys(files).length === 0) { if (Object.keys(input).length === 0) {
return Promise.reject('未提供需上传的文件'); return Promise.reject('未提供需上传的文件');
} }
const gist = await this.locate(); const gist = await this.locate();
let files = input;
if (gist?.id) { if (gist?.id) {
// update an existing gist 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({ return this.http.patch({
url: `/gists/${gist.id}`, url: `/gists/${gist.id}`,
body: JSON.stringify({ files }), body: JSON.stringify({ files }),
}); });
}
} else { } else {
// create a new gist for backup 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({ return this.http.post({
url: '/gists', url: '/gists',
body: JSON.stringify({ body: JSON.stringify({
@ -64,6 +215,7 @@ export default class Gist {
}); });
} }
} }
}
async download(filename) { async download(filename) {
const gist = await this.locate(); const gist = await this.locate();

View File

@ -314,6 +314,17 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
request[method.toLowerCase()]( request[method.toLowerCase()](
options, options,
(err, response, body) => { (err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
if (err) reject(err); if (err) reject(err);
else else
resolve({ resolve({