mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-08 04:03:41 +08:00
Use MikroORM's official Node SQLite dialect with bounded database isolation, preserve legacy schema and data for rollback, and require Node 22.17 or newer. Update Debian runtimes and retain ARMv6 musl compatibility without a SQLite addon. Complete short archive writes, stop orphaned SQL workers after owner death, preserve buffered startup requests, and prestart the archived backend on every platform. Cover real package installation, migration, failure recovery, and legacy database compatibility. Keep authentication storage failures distinct from expired sessions with a pinned Keyv runtime, propagated cache errors, and retryable HTTP 503 responses. Preserve genuine 401 failures and verify legacy cache bytes, initialization safety, and session recovery.
236 lines
7.2 KiB
TypeScript
236 lines
7.2 KiB
TypeScript
import { textSearch } from '../data/queries';
|
|
import { Query, QuerySpec, orderBy } from '../data/queries';
|
|
import groupBy from 'lodash/groupBy';
|
|
import { Inject, Service } from 'typedi';
|
|
import winston from 'winston';
|
|
import config from '../config';
|
|
import {
|
|
Env,
|
|
EnvModel,
|
|
EnvStatus,
|
|
initPosition,
|
|
maxPosition,
|
|
minPosition,
|
|
stepPosition,
|
|
} from '../data/env';
|
|
import { writeFileWithLock } from '../shared/utils';
|
|
import { database } from '../data';
|
|
@Service()
|
|
export default class EnvService {
|
|
constructor(
|
|
@Inject('logger')
|
|
private logger: winston.Logger,
|
|
) {}
|
|
public async create(payloads: Env[]): Promise<Env[]> {
|
|
const envs = await this.envs();
|
|
let position = initPosition;
|
|
if (
|
|
envs &&
|
|
envs.length > 0 &&
|
|
typeof envs[envs.length - 1].position === 'number'
|
|
) {
|
|
position = this.getPrecisionPosition(envs[envs.length - 1].position!);
|
|
}
|
|
const tabs = payloads.map((x) => {
|
|
position = position - stepPosition;
|
|
const tab = new Env({ ...x, position });
|
|
return tab;
|
|
});
|
|
const docs = await this.insert(tabs);
|
|
await this.set_envs();
|
|
await this.checkPosition(tabs[tabs.length - 1].position!);
|
|
return docs;
|
|
}
|
|
public async insert(payloads: Env[]): Promise<Env[]> {
|
|
const result: Env[] = [];
|
|
for (const env of payloads) {
|
|
const doc = await EnvModel.insert(env);
|
|
result.push(doc);
|
|
}
|
|
return result;
|
|
}
|
|
public async update(payload: Env): Promise<Env> {
|
|
const doc = await this.getDb({ id: payload.id });
|
|
const tab = new Env({ ...doc, ...payload });
|
|
const newDoc = await this.updateDb(tab);
|
|
await this.set_envs();
|
|
return newDoc;
|
|
}
|
|
private async updateDb(payload: Env): Promise<Env> {
|
|
await EnvModel.nativeUpdate({ id: payload.id }, { ...payload });
|
|
return await this.getDb({ id: payload.id });
|
|
}
|
|
public async remove(ids: number[]) {
|
|
await EnvModel.nativeDelete({ id: ids });
|
|
await this.set_envs();
|
|
}
|
|
public async move(
|
|
id: number,
|
|
{
|
|
fromIndex,
|
|
toIndex,
|
|
}: {
|
|
fromIndex: number;
|
|
toIndex: number;
|
|
},
|
|
): Promise<Env> {
|
|
let targetPosition: number;
|
|
const isUpward = fromIndex > toIndex;
|
|
const envs = await this.envs();
|
|
if (toIndex === 0 || toIndex === envs.length - 1) {
|
|
targetPosition = isUpward
|
|
? envs[0].position! + stepPosition
|
|
: envs[toIndex].position! - stepPosition;
|
|
} else {
|
|
targetPosition = isUpward
|
|
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
|
|
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
|
|
}
|
|
const newDoc = await this.update({
|
|
id,
|
|
position: this.getPrecisionPosition(targetPosition),
|
|
});
|
|
await this.checkPosition(targetPosition, envs[toIndex].position!);
|
|
return newDoc;
|
|
}
|
|
private async checkPosition(position: number, edge: number = 0) {
|
|
const precisionPosition = this.getPrecisionPosition(position);
|
|
if (
|
|
precisionPosition < minPosition ||
|
|
precisionPosition > maxPosition ||
|
|
Math.abs(precisionPosition - edge) < minPosition
|
|
) {
|
|
const envs = await this.envs();
|
|
let position = initPosition;
|
|
for (const env of envs) {
|
|
position = position - stepPosition;
|
|
await this.updateDb({ id: env.id, position });
|
|
}
|
|
}
|
|
}
|
|
private getPrecisionPosition(position: number): number {
|
|
return Math.trunc(parseFloat(position.toPrecision(16)));
|
|
}
|
|
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
|
let condition = { ...query };
|
|
if (searchText) {
|
|
condition = {
|
|
...condition,
|
|
...textSearch(['name', 'value', 'remarks'], searchText),
|
|
};
|
|
}
|
|
try {
|
|
const result = await this.find(condition, [
|
|
['isPinned', 'DESC'],
|
|
['position', 'DESC'],
|
|
['createdAt', 'ASC'],
|
|
]);
|
|
return result;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
private async find(query: any, sort: any = []): Promise<Env[]> {
|
|
const docs = await EnvModel.find(
|
|
{ ...query },
|
|
{
|
|
orderBy: orderBy([...sort]),
|
|
},
|
|
);
|
|
return docs.map((x) => x);
|
|
}
|
|
public async getDb(query: QuerySpec['where']): Promise<Env> {
|
|
const doc: any = await EnvModel.findOne({ ...query }, {});
|
|
if (!doc) {
|
|
throw new Error(`Env ${JSON.stringify(query)} not found`);
|
|
}
|
|
return doc;
|
|
}
|
|
public async disabled(ids: number[]) {
|
|
await EnvModel.nativeUpdate({ id: ids }, { status: EnvStatus.disabled });
|
|
await this.set_envs();
|
|
}
|
|
public async enabled(ids: number[]) {
|
|
await EnvModel.nativeUpdate({ id: ids }, { status: EnvStatus.normal });
|
|
await this.set_envs();
|
|
}
|
|
public async updateNames({ ids, name }: { ids: number[]; name: string }) {
|
|
await EnvModel.nativeUpdate({ id: ids }, { name });
|
|
await this.set_envs();
|
|
}
|
|
public async pin(ids: number[]) {
|
|
await EnvModel.nativeUpdate({ id: ids }, { isPinned: 1 });
|
|
}
|
|
public async unPin(ids: number[]) {
|
|
await EnvModel.nativeUpdate({ id: ids }, { isPinned: 0 });
|
|
}
|
|
public async addLabels(ids: number[], labels: string[]) {
|
|
await database.transaction(async () => {
|
|
const docs = await EnvModel.find({ id: ids }, {});
|
|
for (const doc of docs) {
|
|
const env = doc;
|
|
await EnvModel.nativeUpdate(
|
|
{ id: env.id },
|
|
{ labels: Array.from(new Set([...(env.labels || []), ...labels])) },
|
|
);
|
|
}
|
|
});
|
|
return await this.find({ id: ids });
|
|
}
|
|
public async removeLabels(ids: number[], labels: string[]) {
|
|
await database.transaction(async () => {
|
|
const docs = await EnvModel.find({ id: ids }, {});
|
|
for (const doc of docs) {
|
|
const env = doc;
|
|
await EnvModel.nativeUpdate(
|
|
{ id: env.id },
|
|
{
|
|
labels: (env.labels || []).filter(
|
|
(label: string) => !labels.includes(label),
|
|
),
|
|
},
|
|
);
|
|
}
|
|
});
|
|
return await this.find({ id: ids });
|
|
}
|
|
public async set_envs() {
|
|
const envs = await this.envs('', {
|
|
name: { ['$ne']: null },
|
|
status: EnvStatus.normal,
|
|
});
|
|
const groups = groupBy(envs, 'name');
|
|
let env_string = '';
|
|
let js_env_string = '';
|
|
let py_env_string = 'import os\n';
|
|
for (const key in groups) {
|
|
if (Object.prototype.hasOwnProperty.call(groups, key)) {
|
|
const group = groups[key];
|
|
// 忽略不符合bash要求的环境变量名称
|
|
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
|
|
let value = group
|
|
.map((x) => x.value)
|
|
.join('&')
|
|
.replace(/'/g, "'\\''")
|
|
.trim();
|
|
env_string += `export ${key}='${value}'\n`;
|
|
const _env_value = `${group
|
|
.map((x) => x.value)
|
|
.join('&')
|
|
.replace(/\\/g, '\\\\')}`;
|
|
js_env_string += `process.env[${JSON.stringify(
|
|
key,
|
|
)}]=${JSON.stringify(group.map((x) => x.value).join('&'))};\n`;
|
|
py_env_string += `os.environ['${key}']='''${_env_value.replace(
|
|
/\'/g,
|
|
"\\'",
|
|
)}'''\n`;
|
|
}
|
|
}
|
|
}
|
|
await writeFileWithLock(config.envFile, env_string);
|
|
await writeFileWithLock(config.jsEnvFile, js_env_string);
|
|
await writeFileWithLock(config.pyEnvFile, py_env_string);
|
|
}
|
|
}
|