mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 03:09:03 +08:00
feat: 优化调整 Gist 同步逻辑; 增加 GitLab Snippet 同步
This commit is contained in:
parent
099ae5ad83
commit
b3de7a4bc5
@ -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": {
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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`);
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
11
backend/src/vendor/open-api.js
vendored
11
backend/src/vendor/open-api.js
vendored
@ -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({
|
||||
|
Loading…
x
Reference in New Issue
Block a user