mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-06-11 16:18:57 +08:00
新增PocketBase支持
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"antd": "^6.3.0",
|
||||
"colors": "^1.4.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"pocketbase": "^0.26.8",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
|
||||
@@ -36,13 +36,22 @@ type IndexedDbConfigData = {
|
||||
compress?: boolean;
|
||||
}
|
||||
|
||||
type PocketBaseConfigData = {
|
||||
url?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export enum DbType {
|
||||
IndexedDb,
|
||||
PocketBase,
|
||||
}
|
||||
|
||||
type DbConfigData = {
|
||||
IndexedDb?: IndexedDbConfigData;
|
||||
DbType?: DbType;
|
||||
PocketBase?: PocketBaseConfigData;
|
||||
}
|
||||
|
||||
export class IndexedDbConfig {
|
||||
@@ -58,6 +67,37 @@ export class IndexedDbConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export class PocketBaseConfig {
|
||||
config: PocketBaseConfigData;
|
||||
constructor(config: PocketBaseConfigData) {
|
||||
this.config = config;
|
||||
}
|
||||
get url(): string {
|
||||
return this.config.url ?? 'http://localhost:8090';
|
||||
}
|
||||
set url(value: string) {
|
||||
this.config.url = value;
|
||||
}
|
||||
get username(): string {
|
||||
return this.config.username ?? '';
|
||||
}
|
||||
set username(value: string) {
|
||||
this.config.username = value;
|
||||
}
|
||||
get password(): string {
|
||||
return this.config.password ?? '';
|
||||
}
|
||||
set password(value: string) {
|
||||
this.config.password = value;
|
||||
}
|
||||
get prefix(): string {
|
||||
return this.config.prefix ?? '';
|
||||
}
|
||||
set prefix(value: string) {
|
||||
this.config.prefix = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class DbConfig {
|
||||
static STORAGE_KEY = 'db_config';
|
||||
config?: DbConfigData;
|
||||
@@ -93,4 +133,13 @@ export class DbConfig {
|
||||
}
|
||||
this.config.DbType = value;
|
||||
}
|
||||
get PocketBase(): PocketBaseConfig {
|
||||
if (!this.config) {
|
||||
throw new Error('Config not initialized');
|
||||
}
|
||||
if (!this.config.PocketBase) {
|
||||
this.config.PocketBase = {};
|
||||
}
|
||||
return new PocketBaseConfig(this.config.PocketBase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DbConfig, DbType } from "../config";
|
||||
import { IndexedDb } from "./indexedDb";
|
||||
import { PocketBaseDb } from "./pocketBase";
|
||||
import type { QdChapterInfo } from "../types";
|
||||
|
||||
export interface Db {
|
||||
@@ -17,8 +18,11 @@ export async function createDb(): Promise<Db> {
|
||||
await config.init();
|
||||
switch (config.DbType) {
|
||||
case DbType.IndexedDb:
|
||||
const db = new IndexedDb(config.IndexedDb);
|
||||
return db;
|
||||
const db1 = new IndexedDb(config.IndexedDb);
|
||||
return db1;
|
||||
case DbType.PocketBase:
|
||||
const db2 = new PocketBaseDb(config.PocketBase);
|
||||
return db2;
|
||||
default:
|
||||
throw new Error('Unsupported database type');
|
||||
}
|
||||
|
||||
129
src/db/pocketBase.ts
Normal file
129
src/db/pocketBase.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { Db } from "./interfaces";
|
||||
import PocketBase from "pocketbase";
|
||||
import type { CollectionModel } from "pocketbase";
|
||||
import { PocketBaseConfig } from "../config";
|
||||
import { QdChapterInfo } from "../types";
|
||||
import { hash_qdchapter_info } from "../utils/qd";
|
||||
|
||||
const QD_CHAPTERS_FIELDS = [
|
||||
{
|
||||
'name': 'chapterId',
|
||||
'type': 'number',
|
||||
'required': true,
|
||||
},
|
||||
{
|
||||
'name': 'bookId',
|
||||
'type': 'number',
|
||||
'required': true,
|
||||
},
|
||||
{
|
||||
'name': 'time',
|
||||
'type': 'number',
|
||||
'required': true,
|
||||
},
|
||||
{
|
||||
'name': 'hash',
|
||||
'type': 'text',
|
||||
'required': true,
|
||||
},
|
||||
{
|
||||
'name': 'data',
|
||||
'type': 'json',
|
||||
'required': true,
|
||||
}
|
||||
];
|
||||
const QD_CHAPTERS_INDEXES = [
|
||||
'CREATE INDEX `idx_cid` ON `{name}` (chapterId)',
|
||||
'CREATE INDEX `idx_bid` ON `{name}` (bookId)',
|
||||
'CREATE INDEX `idx_hash` ON `{name}` (chapterId,bookId,hash)',
|
||||
]
|
||||
|
||||
export class PocketBaseDb implements Db {
|
||||
client: PocketBase;
|
||||
cfg: PocketBaseConfig;
|
||||
constructor(cfg: PocketBaseConfig) {
|
||||
this.cfg = cfg;
|
||||
this.client = new PocketBase(cfg.url);
|
||||
}
|
||||
async init() {
|
||||
await this.client.collection('_superusers').authWithPassword(this.cfg.username, this.cfg.password);
|
||||
if (!this.client.authStore.isValid) {
|
||||
throw new Error('Failed to authenticate with PocketBase. Please check your credentials.');
|
||||
}
|
||||
const collections = await this.client.collections.getFullList({filter: this.cfg.prefix ? `name ~ "${this.cfg.prefix}%"` : undefined});
|
||||
const collectionNames = new Set(collections.map(c => c.name));
|
||||
if (!collectionNames.has(`${this.cfg.prefix}qd_chapters`)) {
|
||||
await this.createCollection('qd_chapters', QD_CHAPTERS_FIELDS, QD_CHAPTERS_INDEXES);
|
||||
} else {
|
||||
const target = collections.find(c => c.name === `${this.cfg.prefix}qd_chapters`)!;
|
||||
if (!this.checkCollection(target, QD_CHAPTERS_FIELDS, QD_CHAPTERS_INDEXES)) {
|
||||
await this.updateCollection('qd_chapters', QD_CHAPTERS_FIELDS, QD_CHAPTERS_INDEXES);
|
||||
}
|
||||
}
|
||||
}
|
||||
async createCollection(name: string, fields: Record<string, unknown>[], indexes: string[]) {
|
||||
await this.client.collections.create({
|
||||
name: `${this.cfg.prefix}${name}`,
|
||||
type: 'base',
|
||||
fields: fields,
|
||||
indexes: indexes,
|
||||
});
|
||||
}
|
||||
async updateCollection(name: string, fields: Record<string, unknown>[], indexes: string[]) {
|
||||
const nidexes = indexes.map(i => i.replace('{name}', `${this.cfg.prefix}${name}`));
|
||||
await this.client.collections.update(`${this.cfg.prefix}${name}`, {
|
||||
fields: fields,
|
||||
indexes: nidexes,
|
||||
});
|
||||
}
|
||||
checkCollection(col: CollectionModel, fields: Record<string, unknown>[], indexes: string[]) {
|
||||
for (const field of fields) {
|
||||
const name = field.name;
|
||||
const target = col.fields.find(f => f.name === name);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
for (const key in field) {
|
||||
if (field[key] !== target[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const index of indexes) {
|
||||
const tindex = index.replace('{name}', col.name);
|
||||
if (!col.indexes.includes(tindex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async saveQdChapter(info: QdChapterInfo) {
|
||||
const hash = hash_qdchapter_info(info);
|
||||
const existed = await this.hasQdChapter(info.id, info.bookId, hash);
|
||||
if (existed) {
|
||||
console.log(`Chapter ${info.id} of book ${info.bookId} already exists in database, skipping`);
|
||||
return;
|
||||
}
|
||||
info.hash = undefined;
|
||||
const re = await this.client.collection(`${this.cfg.prefix}qd_chapters`).create({
|
||||
chapterId: info.id,
|
||||
bookId: info.bookId,
|
||||
time: info.time,
|
||||
hash: hash,
|
||||
data: info,
|
||||
});
|
||||
console.log(re);
|
||||
}
|
||||
async hasQdChapter(id: number, bookId: number, hash: string) {
|
||||
const records = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getList(1, 1, {
|
||||
filter: `chapterId = ${id} && bookId = ${bookId} && hash = "${hash}"`,
|
||||
fields: 'id',
|
||||
});
|
||||
return records.totalItems > 0;
|
||||
}
|
||||
close(): void {
|
||||
this.client.cancelAllRequests();
|
||||
this.client.authStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { FloatButton, Affix, Button, Space, Select, Input } from "antd";
|
||||
import { FloatButton, Affix, Button, Space, Select, Input, Typography, Alert } from "antd";
|
||||
import { SaveTwoTone, SaveOutlined, SyncOutlined } from "@ant-design/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DbConfig, DbType, IndexedDbConfig } from "../config";
|
||||
import { DbConfig, DbType, IndexedDbConfig, PocketBaseConfig } from "../config";
|
||||
import AlertWarn from "../components/AlertWarn";
|
||||
import SwitchLabel from "../components/SwitchLabel";
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function IndexedDbSettings({config}: { config: IndexedDbConfig }) {
|
||||
const [compress, setCompress] = useState(false);
|
||||
@@ -19,6 +22,68 @@ function IndexedDbSettings({config}: { config: IndexedDbConfig }) {
|
||||
</>)
|
||||
}
|
||||
|
||||
async function testPocketBaseConnection(config: PocketBaseConfig) {
|
||||
const client = new PocketBase(config.url);
|
||||
const authData = await client.collection('_superusers').authWithPassword(config.username, config.password);
|
||||
return client.authStore.isValid;
|
||||
}
|
||||
|
||||
function PocketBaseSettings({config}: { config: PocketBaseConfig }) {
|
||||
const [url, setUrl] = useState('http://localhost:8090');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [prefix, setPrefix] = useState('');
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
setUrl(config.url);
|
||||
setUsername(config.username);
|
||||
setPassword(config.password);
|
||||
setPrefix(config.prefix);
|
||||
}, [config]);
|
||||
async function handleTestConnection() {
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const result = await testPocketBaseConnection(config);
|
||||
setTestResult(result ? "连接成功!" : "连接失败!");
|
||||
} catch (e) {
|
||||
setTestResult(e instanceof Error ? e.message : "未知错误");
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
}
|
||||
function handleUrlChange(value: string) {
|
||||
setUrl(value);
|
||||
config.url = value;
|
||||
}
|
||||
function handleUsernameChange(value: string) {
|
||||
setUsername(value);
|
||||
config.username = value;
|
||||
}
|
||||
function handlePasswordChange(value: string) {
|
||||
setPassword(value);
|
||||
config.password = value;
|
||||
}
|
||||
function handlePrefixChange(value: string) {
|
||||
setPrefix(value);
|
||||
config.prefix = value;
|
||||
}
|
||||
return (<Space orientation="vertical">
|
||||
<Text>服务器地址</Text>
|
||||
<Input placeholder="服务器地址" value={url} onChange={e => handleUrlChange(e.target.value)} allowClear />
|
||||
<Alert title="用户帐户需要是超级用户(superuser)才能正常工作" type="info" />
|
||||
<Text>用户名</Text>
|
||||
<Input placeholder="用户名" value={username} onChange={e => handleUsernameChange(e.target.value)} allowClear />
|
||||
<Text>密码</Text>
|
||||
<Input.Password placeholder="密码" value={password} onChange={e => handlePasswordChange(e.target.value)} allowClear />
|
||||
<Text>前缀(可选)</Text>
|
||||
<Input placeholder="前缀(可选)" value={prefix} onChange={e => handlePrefixChange(e.target.value)} allowClear />
|
||||
<Button onClick={handleTestConnection} disabled={testing}>{testing ? "测试中..." : "测试连接"}</Button>
|
||||
{testResult && <div>{testResult}</div>}
|
||||
</Space>)
|
||||
}
|
||||
|
||||
export default function DbSettings() {
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
const [config] = useState(new DbConfig());
|
||||
@@ -56,13 +121,19 @@ export default function DbSettings() {
|
||||
</Affix>
|
||||
<br />
|
||||
<Space orientation="vertical">
|
||||
<Text>数据库类型</Text>
|
||||
<Select value={dbType} onChange={value => setDbType(value)} style={{ width: 200 }} options={[
|
||||
{
|
||||
label: "IndexedDb",
|
||||
value: DbType.IndexedDb,
|
||||
},
|
||||
{
|
||||
label: "PocketBase",
|
||||
value: DbType.PocketBase,
|
||||
},
|
||||
]} />
|
||||
{dbType === DbType.IndexedDb && <IndexedDbSettings config={indexedDbConfig} />}
|
||||
{dbType === DbType.PocketBase && <PocketBaseSettings config={config.PocketBase} />}
|
||||
</Space>
|
||||
<FloatButton icon={<SaveTwoTone />} tooltip="保存设置" onClick={saveSettings} />
|
||||
{alert && <AlertWarn title={alert.title} content={alert.content} onClose={() => setAlert(null)} />}
|
||||
|
||||
@@ -796,6 +796,11 @@ json2mq@^0.2.0:
|
||||
dependencies:
|
||||
string-convert "^0.2.0"
|
||||
|
||||
pocketbase@^0.26.8:
|
||||
version "0.26.8"
|
||||
resolved "https://registry.yarnpkg.com/pocketbase/-/pocketbase-0.26.8.tgz#1b800b5f69f2ab08c3e3da45fce25f45c6724c45"
|
||||
integrity sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low==
|
||||
|
||||
react-dom@^19.2.4:
|
||||
version "19.2.4"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591"
|
||||
|
||||
Reference in New Issue
Block a user