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",
"version": "2.14.186",
"version": "2.14.187",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {

View File

@ -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');
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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`);

View File

@ -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) {

View File

@ -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,
}),
});
}
}
}

View File

@ -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({