文件
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

163 行
4.4 KiB
TypeScript

import { join, resolve } from 'path';
import { HttpMethod, Json, UserData } from '../types';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { Env } from '@joplin/lib/models/Setting';
import execa = require('execa');
import { msleep } from '@joplin/utils/time';
import Logger from '@joplin/utils/Logger';
import { strict as assert } from 'assert';
import { copy, exists } from 'fs-extra';
import { copyFile } from 'fs/promises';
const logger = Logger.create('Server');
const createApi = async (serverUrl: string, adminAuth: UserData) => {
const api = new JoplinServerApi({
baseUrl: () => serverUrl,
userContentBaseUrl: () => serverUrl,
password: () => adminAuth.password,
username: () => adminAuth.email,
session: ()=>null,
apiKey: ()=>'',
env: Env.Dev,
});
await api.loadSession();
return api;
};
interface ServerConfig {
baseUrl: string;
baseDirectory: string;
useRunningServer: boolean;
adminAuth: UserData;
}
interface FromSnapshotOptions extends ServerConfig {
snapshotDirectory: string;
}
export default class Server {
private api_: JoplinServerApi|null = null;
private serverUrl_: string;
private adminAuth_: UserData;
private server_: execa.ExecaChildProcess<string>|null;
private baseDirectory_: string;
public constructor(config: ServerConfig) {
this.adminAuth_ = config.adminAuth;
const serverDir = resolve(config.baseDirectory);
this.baseDirectory_ = serverDir;
this.serverUrl_ = config.baseUrl;
// Code in other places (e.g. the JoplinServerApi) assumes that serverUrl_ ends with a "/"
if (!this.serverUrl_.endsWith('/')) {
this.serverUrl_ = `${this.serverUrl_}/`;
}
const mainEntrypoint = join(serverDir, 'dist', 'index.js');
if (!config.useRunningServer) {
this.server_ = execa.node(mainEntrypoint, [
'--env', 'dev',
], {
env: {
JOPLIN_IS_TESTING: '1',
},
cwd: serverDir,
stdin: 'ignore', // No stdin
// For debugging:
stderr: process.stderr,
// stdout: process.stdout,
});
} else {
this.server_ = null;
}
}
private static assertCanUseSnapshots_() {
if (process.env.SQLITE_DATABASE) {
throw new Error(`Unsupported: Creating snapshots of a non-default database (${JSON.stringify(process.env.SQLITE_DATABASE)}) is not supported. Skipping...`);
}
if ((process.env.DB_CLIENT ?? 'sqlite') !== 'sqlite') {
throw new Error(`Not supported: Creating snapshots of a non-sqlite database is not supported (DB_CLIENT: ${process.env.DB_CLIENT}). Skipping...`);
}
}
public assertCanUseSnapshots() {
Server.assertCanUseSnapshots_();
if (!this.server_) {
throw new Error('Not supported: Creating snapshots using an external server instance.');
}
}
public static async fromSnapshot({
baseDirectory: serverBaseDirectory, snapshotDirectory, ...config
}: FromSnapshotOptions) {
this.assertCanUseSnapshots_();
const serverDatabaseFile = join(serverBaseDirectory, 'db-dev.sqlite');
logger.info('Restoring', serverDatabaseFile, '... Replacing with version from snapshot...');
await copy(join(snapshotDirectory, 'server', 'db-dev.sqlite'), serverDatabaseFile);
return new Server({
baseDirectory: serverBaseDirectory,
...config,
});
}
public async saveSnapshot(outputDirectory: string) {
Server.assertCanUseSnapshots_();
// Note: Assumes that the server is using SQLite!
const databasePath = join(this.baseDirectory_, 'db-dev.sqlite');
logger.info('Creating snapshot of', databasePath, '...');
assert.ok(await exists(outputDirectory));
const destination = join(outputDirectory, 'db-dev.sqlite');
await copyFile(databasePath, destination);
}
public get url() {
return this.serverUrl_;
}
public async checkConnection() {
let lastError;
for (let retry = 0; retry < 30; retry++) {
try {
const response = await fetch(`${this.serverUrl_}api/ping`);
if (response.ok) {
return true;
}
} catch (error) {
lastError = error;
}
await msleep(500);
}
if (lastError) {
throw lastError;
}
return false;
}
public async execApi(method: HttpMethod, route: string, action: Json|undefined): Promise<Json> {
this.api_ ??= await createApi(this.serverUrl_, this.adminAuth_);
logger.debug('API EXEC', method, route, action);
const result = await this.api_.exec(method, route, {}, action);
return result;
}
public async close() {
if (this.server_) {
this.server_.cancel();
logger.info('Closed the server.');
} else {
logger.info('Server not closed: Running using an external server.');
}
}
}