mira-app-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/HttpRouter.d.ts +17 -0
- package/dist/HttpRouter.d.ts.map +1 -0
- package/dist/HttpRouter.js +217 -0
- package/dist/HttpRouter.js.map +1 -0
- package/dist/HttpServer.d.ts +24 -0
- package/dist/HttpServer.d.ts.map +1 -0
- package/dist/HttpServer.js +55 -0
- package/dist/HttpServer.js.map +1 -0
- package/dist/ILibraryServerData.d.ts +63 -0
- package/dist/ILibraryServerData.d.ts.map +1 -0
- package/dist/ILibraryServerData.js +3 -0
- package/dist/ILibraryServerData.js.map +1 -0
- package/dist/LibraryList.d.ts +9 -0
- package/dist/LibraryList.d.ts.map +1 -0
- package/dist/LibraryList.js +29 -0
- package/dist/LibraryList.js.map +1 -0
- package/dist/LibraryServerDataSQLite.d.ts +96 -0
- package/dist/LibraryServerDataSQLite.d.ts.map +1 -0
- package/dist/LibraryServerDataSQLite.js +683 -0
- package/dist/LibraryServerDataSQLite.js.map +1 -0
- package/dist/LibraryStorage.d.ts +14 -0
- package/dist/LibraryStorage.d.ts.map +1 -0
- package/dist/LibraryStorage.js +45 -0
- package/dist/LibraryStorage.js.map +1 -0
- package/dist/MessageHandler.d.ts +16 -0
- package/dist/MessageHandler.d.ts.map +1 -0
- package/dist/MessageHandler.js +35 -0
- package/dist/MessageHandler.js.map +1 -0
- package/dist/ServerExample.d.ts +33 -0
- package/dist/ServerExample.d.ts.map +1 -0
- package/dist/ServerExample.js +87 -0
- package/dist/ServerExample.js.map +1 -0
- package/dist/ServerPlugin.d.ts +20 -0
- package/dist/ServerPlugin.d.ts.map +1 -0
- package/dist/ServerPlugin.js +79 -0
- package/dist/ServerPlugin.js.map +1 -0
- package/dist/ServerPluginManager.d.ts +30 -0
- package/dist/ServerPluginManager.d.ts.map +1 -0
- package/dist/ServerPluginManager.js +112 -0
- package/dist/ServerPluginManager.js.map +1 -0
- package/dist/WebSocketRouter.d.ts +18 -0
- package/dist/WebSocketRouter.d.ts.map +1 -0
- package/dist/WebSocketRouter.js +31 -0
- package/dist/WebSocketRouter.js.map +1 -0
- package/dist/WebSocketServer.d.ts +23 -0
- package/dist/WebSocketServer.d.ts.map +1 -0
- package/dist/WebSocketServer.js +162 -0
- package/dist/WebSocketServer.js.map +1 -0
- package/dist/event-manager.d.ts +85 -0
- package/dist/event-manager.d.ts.map +1 -0
- package/dist/event-manager.js +142 -0
- package/dist/event-manager.js.map +1 -0
- package/dist/handlers/FileHandler.d.ts +10 -0
- package/dist/handlers/FileHandler.d.ts.map +1 -0
- package/dist/handlers/FileHandler.js +55 -0
- package/dist/handlers/FileHandler.js.map +1 -0
- package/dist/handlers/FolderHandler.d.ts +10 -0
- package/dist/handlers/FolderHandler.d.ts.map +1 -0
- package/dist/handlers/FolderHandler.js +59 -0
- package/dist/handlers/FolderHandler.js.map +1 -0
- package/dist/handlers/LibraryHandler.d.ts +10 -0
- package/dist/handlers/LibraryHandler.d.ts.map +1 -0
- package/dist/handlers/LibraryHandler.js +49 -0
- package/dist/handlers/LibraryHandler.js.map +1 -0
- package/dist/handlers/MessageHandler.d.ts +15 -0
- package/dist/handlers/MessageHandler.d.ts.map +1 -0
- package/dist/handlers/MessageHandler.js +32 -0
- package/dist/handlers/MessageHandler.js.map +1 -0
- package/dist/handlers/PluginMessageHandler.d.ts +10 -0
- package/dist/handlers/PluginMessageHandler.d.ts.map +1 -0
- package/dist/handlers/PluginMessageHandler.js +21 -0
- package/dist/handlers/PluginMessageHandler.js.map +1 -0
- package/dist/handlers/TagHandler.d.ts +10 -0
- package/dist/handlers/TagHandler.d.ts.map +1 -0
- package/dist/handlers/TagHandler.js +59 -0
- package/dist/handlers/TagHandler.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/HttpRouter.ts +236 -0
- package/src/HttpServer.ts +72 -0
- package/src/ILibraryServerData.ts +70 -0
- package/src/LibraryList.ts +26 -0
- package/src/LibraryServerDataSQLite.ts +778 -0
- package/src/LibraryStorage.ts +55 -0
- package/src/MessageHandler.ts +41 -0
- package/src/ServerExample.ts +72 -0
- package/src/ServerPlugin.ts +56 -0
- package/src/ServerPluginManager.ts +106 -0
- package/src/WebSocketRouter.ts +46 -0
- package/src/WebSocketServer.ts +206 -0
- package/src/event-manager.ts +191 -0
- package/src/handlers/FileHandler.ts +61 -0
- package/src/handlers/FolderHandler.ts +65 -0
- package/src/handlers/LibraryHandler.ts +55 -0
- package/src/handlers/MessageHandler.ts +37 -0
- package/src/handlers/PluginMessageHandler.ts +27 -0
- package/src/handlers/TagHandler.ts +66 -0
- package/src/index.ts +44 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import { Database } from 'sqlite3';
|
|
2
|
+
import { ILibraryServerData } from './ILibraryServerData';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { EventManager } from './event-manager';
|
|
6
|
+
import { ServerPluginManager } from './ServerPluginManager';
|
|
7
|
+
import { MiraWebsocketServer } from './WebSocketServer';
|
|
8
|
+
import { MiraHttpServer } from './HttpServer';
|
|
9
|
+
|
|
10
|
+
export class LibraryServerDataSQLite implements ILibraryServerData {
|
|
11
|
+
private db: Database | null = null;
|
|
12
|
+
private inTransaction = false;
|
|
13
|
+
private enableHash: boolean;
|
|
14
|
+
private readonly websocketServer: MiraWebsocketServer | undefined;
|
|
15
|
+
eventManager: EventManager | undefined;
|
|
16
|
+
readonly config: Record<string, any>;
|
|
17
|
+
pluginManager: ServerPluginManager | undefined;
|
|
18
|
+
httpServer: MiraHttpServer | undefined;
|
|
19
|
+
|
|
20
|
+
private async initializePlugins(): Promise<void> {
|
|
21
|
+
if (this.pluginManager) await this.pluginManager.loadPlugins();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
constructor(config: Record<string, any>, opts: any) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
if (opts.websocketServer || opts.httpServer) {
|
|
27
|
+
this.websocketServer = opts.websocketServer;
|
|
28
|
+
this.httpServer = opts.httpServer;
|
|
29
|
+
this.eventManager = new EventManager();
|
|
30
|
+
this.pluginManager = new ServerPluginManager(
|
|
31
|
+
{ server: opts.webSocketServer, dbService: this as unknown as ILibraryServerData, httpServer: opts.httpServer, pluginsDir: config.pluginsDir }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
this.initializePlugins();
|
|
35
|
+
this.enableHash = config.customFields?.enableHash ?? false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async initialize(): Promise<void> {
|
|
40
|
+
// 初始化数据库连接和表结构
|
|
41
|
+
const dbPath = path.join(await this.getLibraryPath(), 'library_data.db');
|
|
42
|
+
this.db = new Database(dbPath);
|
|
43
|
+
// 创建文件表
|
|
44
|
+
await this.executeSql(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS files(
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
name TEXT NOT NULL,
|
|
48
|
+
created_at INTEGER NOT NULL,
|
|
49
|
+
imported_at INTEGER NOT NULL,
|
|
50
|
+
size INTEGER NOT NULL,
|
|
51
|
+
hash TEXT NOT NULL,
|
|
52
|
+
custom_fields TEXT,
|
|
53
|
+
notes TEXT,
|
|
54
|
+
stars INTEGER DEFAULT 0,
|
|
55
|
+
folder_id INTEGER,
|
|
56
|
+
reference TEXT,
|
|
57
|
+
path TEXT,
|
|
58
|
+
thumb INTEGER DEFAULT 0,
|
|
59
|
+
recycled INTEGER DEFAULT 0,
|
|
60
|
+
tags TEXT,
|
|
61
|
+
FOREIGN KEY(folder_id) REFERENCES folders(id)
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
// 创建文件夹表
|
|
66
|
+
await this.executeSql(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS folders(
|
|
68
|
+
id INTEGER PRIMARY KEY,
|
|
69
|
+
title TEXT NOT NULL,
|
|
70
|
+
parent_id INTEGER,
|
|
71
|
+
color INTEGER,
|
|
72
|
+
icon TEXT,
|
|
73
|
+
FOREIGN KEY(parent_id) REFERENCES folders(id)
|
|
74
|
+
)
|
|
75
|
+
`);
|
|
76
|
+
|
|
77
|
+
// 创建标签表
|
|
78
|
+
await this.executeSql(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS tags(
|
|
80
|
+
id INTEGER PRIMARY KEY,
|
|
81
|
+
title TEXT NOT NULL,
|
|
82
|
+
parent_id INTEGER,
|
|
83
|
+
color INTEGER,
|
|
84
|
+
icon INTEGER,
|
|
85
|
+
FOREIGN KEY(parent_id) REFERENCES tags(id)
|
|
86
|
+
)
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 文件操作方法实现
|
|
91
|
+
async createFile(fileData: Record<string, any>): Promise<Record<string, any>> {
|
|
92
|
+
const result = await this.runSql(
|
|
93
|
+
`INSERT INTO files(
|
|
94
|
+
name, created_at, imported_at, size, hash,
|
|
95
|
+
custom_fields, notes, stars, folder_id,
|
|
96
|
+
reference, path, thumb, recycled, tags
|
|
97
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
98
|
+
[
|
|
99
|
+
fileData.name,
|
|
100
|
+
fileData.created_at,
|
|
101
|
+
fileData.imported_at,
|
|
102
|
+
fileData.size,
|
|
103
|
+
fileData.hash,
|
|
104
|
+
fileData.custom_fields,
|
|
105
|
+
fileData.notes,
|
|
106
|
+
fileData.stars ?? 0,
|
|
107
|
+
fileData.folder_id,
|
|
108
|
+
fileData.reference,
|
|
109
|
+
fileData.path,
|
|
110
|
+
fileData.thumb ?? 0,
|
|
111
|
+
fileData.recycled ?? 0,
|
|
112
|
+
fileData.tags,
|
|
113
|
+
]
|
|
114
|
+
);
|
|
115
|
+
return { id: result.lastID, ...fileData };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async updateFile(id: number, fileData: Record<string, any>): Promise<boolean> {
|
|
120
|
+
const fields: string[] = [];
|
|
121
|
+
const params: any[] = [];
|
|
122
|
+
|
|
123
|
+
const addField = (key: string, value: any) => {
|
|
124
|
+
if (fileData[key] !== undefined) {
|
|
125
|
+
fields.push(`${key} = ?`);
|
|
126
|
+
params.push(value);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
addField('name', fileData.name);
|
|
131
|
+
addField('created_at', fileData.created_at);
|
|
132
|
+
addField('imported_at', fileData.imported_at);
|
|
133
|
+
addField('size', fileData.size);
|
|
134
|
+
addField('hash', fileData.hash);
|
|
135
|
+
addField('custom_fields', fileData.custom_fields);
|
|
136
|
+
addField('notes', fileData.notes);
|
|
137
|
+
addField('stars', fileData.stars ?? 0);
|
|
138
|
+
addField('tags', fileData.tags);
|
|
139
|
+
addField('folder_id', fileData.folder_id);
|
|
140
|
+
addField('reference', fileData.reference);
|
|
141
|
+
addField('path', fileData.path);
|
|
142
|
+
addField('thumb', fileData.thumb ?? 0);
|
|
143
|
+
addField('recycled', fileData.recycled ?? 0);
|
|
144
|
+
|
|
145
|
+
if (fields.length === 0) return false;
|
|
146
|
+
|
|
147
|
+
const query = `UPDATE files SET ${fields.join(', ')} WHERE id = ?`;
|
|
148
|
+
params.push(id);
|
|
149
|
+
|
|
150
|
+
const result = await this.runSql(query, params);
|
|
151
|
+
return result.changes > 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async deleteFile(id: number, options?: { moveToRecycleBin: boolean }): Promise<boolean> {
|
|
155
|
+
const query = options?.moveToRecycleBin
|
|
156
|
+
? 'UPDATE files SET recycled = 1 WHERE id = ?'
|
|
157
|
+
: 'DELETE FROM files WHERE id = ?';
|
|
158
|
+
const result = await this.runSql(query, [id]);
|
|
159
|
+
return result.changes > 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async recoverFile(id: number): Promise<boolean> {
|
|
163
|
+
const result = await this.runSql('UPDATE files SET recycled = 0 WHERE id = ?', [id]);
|
|
164
|
+
return result.changes > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getFile(id: number): Promise<Record<string, any> | null> {
|
|
168
|
+
const rows = await this.getSql('SELECT * FROM files WHERE id = ? LIMIT 1', [id]);
|
|
169
|
+
return rows.length > 0 ? this.rowToMap(rows[0]) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getFiles(options?: {
|
|
173
|
+
select?: string;
|
|
174
|
+
filters?: Record<string, any>;
|
|
175
|
+
isUrlFile?: boolean;
|
|
176
|
+
}): Promise<{
|
|
177
|
+
result: Record<string, any>[];
|
|
178
|
+
limit: number;
|
|
179
|
+
offset: number;
|
|
180
|
+
total: number;
|
|
181
|
+
}> {
|
|
182
|
+
const select = options?.select || '*';
|
|
183
|
+
const filters = options?.filters || {};
|
|
184
|
+
const whereClauses: string[] = [];
|
|
185
|
+
const params: any[] = [];
|
|
186
|
+
const folderId = parseInt(filters.folder?.toString() || '0') || 0;
|
|
187
|
+
const tagIds = Array.isArray(filters.tags) ? filters.tags : [];
|
|
188
|
+
const limit = parseInt(filters.limit?.toString() || '100') || 100;
|
|
189
|
+
const offset = parseInt(filters.offset?.toString() || '0') || 0;
|
|
190
|
+
|
|
191
|
+
// 构建查询条件
|
|
192
|
+
if (filters.recycled !== undefined) {
|
|
193
|
+
whereClauses.push('recycled = ?');
|
|
194
|
+
params.push(filters.recycled ? 1 : 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (filters.star !== undefined) {
|
|
198
|
+
whereClauses.push('stars >= ?');
|
|
199
|
+
params.push(filters.star);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (filters.name) {
|
|
203
|
+
whereClauses.push('name LIKE ?');
|
|
204
|
+
params.push(`%${filters.name}%`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (filters.dateRange) {
|
|
208
|
+
whereClauses.push('created_at BETWEEN ? AND ?');
|
|
209
|
+
params.push(filters.dateRange.start.getTime(), filters.dateRange.end.getTime());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (filters.minSize !== undefined) {
|
|
213
|
+
whereClauses.push('size >= ?');
|
|
214
|
+
params.push(filters.minSize * 1024);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (filters.maxSize !== undefined) {
|
|
218
|
+
whereClauses.push('size <= ?');
|
|
219
|
+
params.push(filters.maxSize * 1024);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (filters.minRating !== undefined) {
|
|
223
|
+
whereClauses.push('stars >= ?');
|
|
224
|
+
params.push(filters.minRating);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (folderId !== 0) {
|
|
228
|
+
whereClauses.push('folder_id = ?');
|
|
229
|
+
params.push(folderId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (tagIds.length > 0) {
|
|
233
|
+
whereClauses.push(`(
|
|
234
|
+
SELECT COUNT(*) FROM json_each(tags)
|
|
235
|
+
WHERE value IN (${tagIds.map(() => '?').join(',')})
|
|
236
|
+
) = ${tagIds.length}`);
|
|
237
|
+
params.push(...tagIds);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (filters.custom_fields) {
|
|
241
|
+
const customFields = filters.custom_fields;
|
|
242
|
+
const convertValue = (value: any) => {
|
|
243
|
+
if (value == 'null') {
|
|
244
|
+
value = null;
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
for (const [key, value] of Object.entries(customFields)) {
|
|
249
|
+
if (typeof value === 'string' && value.startsWith('!=')) {
|
|
250
|
+
let actualValue: string | null = value.substring(2).trim();
|
|
251
|
+
whereClauses.push(`(json_extract(custom_fields, '$.${key}') IS NOT NULL OR json_extract(custom_fields, '$.${key}') != ?)`);
|
|
252
|
+
params.push(convertValue(actualValue));
|
|
253
|
+
} else if (typeof value === 'string' && value.startsWith('>')) {
|
|
254
|
+
whereClauses.push(`json_extract(custom_fields, '$.${key}') > ?`);
|
|
255
|
+
params.push(convertValue(value.substring(1).trim()));
|
|
256
|
+
} else if (typeof value === 'string' && value.startsWith('<')) {
|
|
257
|
+
whereClauses.push(`json_extract(custom_fields, '$.${key}') < ?`);
|
|
258
|
+
params.push(convertValue(value.substring(1).trim()));
|
|
259
|
+
} else {
|
|
260
|
+
whereClauses.push(`json_extract(custom_fields, '$.${key}') = ?`);
|
|
261
|
+
params.push(convertValue(value));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const where = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
|
267
|
+
// 处理排序
|
|
268
|
+
let orderBy = '';
|
|
269
|
+
// sort?: 'imported_at' | 'id' | 'size' | 'stars' | 'folder_id' | 'tags' | 'name' | 'custom_fields';
|
|
270
|
+
// order?: 'asc' | 'desc';
|
|
271
|
+
if (filters?.sort) {
|
|
272
|
+
const order = filters?.order || 'asc';
|
|
273
|
+
if (filters.sort === 'custom_fields') {
|
|
274
|
+
// 自定义字段排序需要特殊处理
|
|
275
|
+
orderBy = ` ORDER BY json_extract(custom_fields, '$') ${order}`;
|
|
276
|
+
} else {
|
|
277
|
+
orderBy = ` ORDER BY ${filters.sort} ${order}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const query = `SELECT ${select} FROM files ${where}${orderBy} LIMIT ? OFFSET ?`;
|
|
282
|
+
const countQuery = `SELECT COUNT(*) as total FROM files ${where}`;
|
|
283
|
+
|
|
284
|
+
const [rows, countRows] = await Promise.all([
|
|
285
|
+
this.getSql(query, [...params, limit, offset]),
|
|
286
|
+
this.getSql(countQuery, params),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
result: await this.processingFiles(rows.map(row => this.rowToMap(row)), options?.isUrlFile),
|
|
291
|
+
limit,
|
|
292
|
+
offset,
|
|
293
|
+
total: countRows[0].total,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 文件夹操作方法
|
|
298
|
+
async createFolder(folderData: Record<string, any>): Promise<number> {
|
|
299
|
+
const result = await this.runSql(
|
|
300
|
+
'INSERT INTO folders(id, title, parent_id, color, icon) VALUES (?, ?, ?, ?, ?)',
|
|
301
|
+
[
|
|
302
|
+
folderData.id,
|
|
303
|
+
folderData.title,
|
|
304
|
+
folderData.parent_id,
|
|
305
|
+
folderData.color,
|
|
306
|
+
folderData.icon,
|
|
307
|
+
]
|
|
308
|
+
);
|
|
309
|
+
return result.lastID;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async updateFolder(id: number, folderData: Record<string, any>): Promise<boolean> {
|
|
313
|
+
const result = await this.runSql(
|
|
314
|
+
'UPDATE folders SET title = ?, parent_id = ?, color = ?, icon = ? WHERE id = ?',
|
|
315
|
+
[
|
|
316
|
+
folderData.title,
|
|
317
|
+
folderData.parent_id,
|
|
318
|
+
folderData.color,
|
|
319
|
+
folderData.icon,
|
|
320
|
+
id,
|
|
321
|
+
]
|
|
322
|
+
);
|
|
323
|
+
return result.changes > 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async deleteFolder(id: number): Promise<boolean> {
|
|
327
|
+
await this.beginTransaction();
|
|
328
|
+
try {
|
|
329
|
+
// 递归删除子文件夹
|
|
330
|
+
const children = await this.getFolders({ parentId: id });
|
|
331
|
+
for (const child of children) {
|
|
332
|
+
await this.deleteFolder(child.id);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 更新文件的folder_id为null
|
|
336
|
+
await this.runSql('UPDATE files SET folder_id = NULL WHERE folder_id = ?', [id]);
|
|
337
|
+
|
|
338
|
+
// 删除文件夹
|
|
339
|
+
const result = await this.runSql('DELETE FROM folders WHERE id = ?', [id]);
|
|
340
|
+
await this.commitTransaction();
|
|
341
|
+
return result.changes > 0;
|
|
342
|
+
} catch (err) {
|
|
343
|
+
await this.rollbackTransaction();
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async getFolder(id: number): Promise<Record<string, any> | null> {
|
|
349
|
+
const rows = await this.getSql('SELECT * FROM folders WHERE id = ? LIMIT 1', [id]);
|
|
350
|
+
return rows.length > 0 ? this.rowToMap(rows[0]) : null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async findFolderByName(name: string, parentId?: number | null): Promise<Record<string, any> | null> {
|
|
354
|
+
const query = parentId !== undefined && parentId !== null
|
|
355
|
+
? 'SELECT * FROM folders WHERE title = ? AND parent_id = ? LIMIT 1'
|
|
356
|
+
: 'SELECT * FROM folders WHERE title = ? AND parent_id IS NULL LIMIT 1';
|
|
357
|
+
|
|
358
|
+
const params = parentId !== undefined && parentId !== null
|
|
359
|
+
? [name, parentId]
|
|
360
|
+
: [name];
|
|
361
|
+
|
|
362
|
+
const rows = await this.getSql(query, params);
|
|
363
|
+
return rows.length > 0 ? this.rowToMap(rows[0]) : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async getFolders(options?: {
|
|
367
|
+
parentId?: number;
|
|
368
|
+
limit?: number;
|
|
369
|
+
offset?: number;
|
|
370
|
+
}): Promise<Record<string, any>[]> {
|
|
371
|
+
const parentId = options?.parentId;
|
|
372
|
+
const limit = options?.limit || 100;
|
|
373
|
+
const offset = options?.offset || 0;
|
|
374
|
+
|
|
375
|
+
const where = parentId !== undefined ? 'WHERE parent_id = ?' : 'WHERE parent_id IS NULL';
|
|
376
|
+
const params = parentId !== undefined ? [parentId, limit, offset] : [limit, offset];
|
|
377
|
+
const query = `SELECT * FROM folders ${where} LIMIT ? OFFSET ?`;
|
|
378
|
+
|
|
379
|
+
const rows = await this.getSql(query, params);
|
|
380
|
+
return rows.map(row => this.rowToMap(row));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 标签操作方法
|
|
384
|
+
async createTag(tagData: Record<string, any>): Promise<number> {
|
|
385
|
+
const result = await this.runSql(
|
|
386
|
+
'INSERT INTO tags(id, title, parent_id, color, icon) VALUES (?, ?, ?, ?, ?)',
|
|
387
|
+
[
|
|
388
|
+
tagData.id,
|
|
389
|
+
tagData.title,
|
|
390
|
+
tagData.parent_id,
|
|
391
|
+
tagData.color,
|
|
392
|
+
tagData.icon,
|
|
393
|
+
]
|
|
394
|
+
);
|
|
395
|
+
return result.lastID;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async updateTag(id: number, tagData: Record<string, any>): Promise<boolean> {
|
|
399
|
+
const result = await this.runSql(
|
|
400
|
+
'UPDATE tags SET title = ?, parent_id = ?, color = ?, icon = ? WHERE id = ?',
|
|
401
|
+
[
|
|
402
|
+
tagData.title,
|
|
403
|
+
tagData.parent_id,
|
|
404
|
+
tagData.color,
|
|
405
|
+
tagData.icon,
|
|
406
|
+
id,
|
|
407
|
+
]
|
|
408
|
+
);
|
|
409
|
+
return result.changes > 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async deleteTag(id: number): Promise<boolean> {
|
|
413
|
+
await this.beginTransaction();
|
|
414
|
+
try {
|
|
415
|
+
// 递归删除子标签
|
|
416
|
+
const children = await this.getTags({ parentId: id });
|
|
417
|
+
for (const child of children) {
|
|
418
|
+
await this.deleteTag(child.id);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 删除标签
|
|
422
|
+
const result = await this.runSql('DELETE FROM tags WHERE id = ?', [id]);
|
|
423
|
+
await this.commitTransaction();
|
|
424
|
+
return result.changes > 0;
|
|
425
|
+
} catch (err) {
|
|
426
|
+
await this.rollbackTransaction();
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async getTag(id: number): Promise<Record<string, any> | null> {
|
|
432
|
+
const rows = await this.getSql('SELECT * FROM tags WHERE id = ? LIMIT 1', [id]);
|
|
433
|
+
return rows.length > 0 ? this.rowToMap(rows[0]) : null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async getTags(options?: {
|
|
437
|
+
parentId?: number;
|
|
438
|
+
limit?: number;
|
|
439
|
+
offset?: number;
|
|
440
|
+
}): Promise<Record<string, any>[]> {
|
|
441
|
+
const parentId = options?.parentId;
|
|
442
|
+
const limit = options?.limit || 100;
|
|
443
|
+
const offset = options?.offset || 0;
|
|
444
|
+
|
|
445
|
+
const where = parentId !== undefined ? 'WHERE parent_id = ?' : 'WHERE parent_id IS NULL';
|
|
446
|
+
const params = parentId !== undefined ? [parentId, limit, offset] : [limit, offset];
|
|
447
|
+
const query = `SELECT * FROM tags ${where} LIMIT ? OFFSET ?`;
|
|
448
|
+
|
|
449
|
+
const rows = await this.getSql(query, params);
|
|
450
|
+
return rows.map(row => this.rowToMap(row));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 事务管理
|
|
454
|
+
async beginTransaction(): Promise<void> {
|
|
455
|
+
if (!this.inTransaction) {
|
|
456
|
+
await this.executeSql('BEGIN TRANSACTION');
|
|
457
|
+
this.inTransaction = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async commitTransaction(): Promise<void> {
|
|
462
|
+
if (this.inTransaction) {
|
|
463
|
+
await this.executeSql('COMMIT');
|
|
464
|
+
this.inTransaction = false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async rollbackTransaction(): Promise<void> {
|
|
469
|
+
if (this.inTransaction) {
|
|
470
|
+
await this.executeSql('ROLLBACK');
|
|
471
|
+
this.inTransaction = false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async close(): Promise<void> {
|
|
476
|
+
if (this.db) {
|
|
477
|
+
this.db.close();
|
|
478
|
+
this.db = null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async createFileFromPath(
|
|
483
|
+
filePath: string,
|
|
484
|
+
fileMeta: Record<string, any>,
|
|
485
|
+
options?: { importType: string }
|
|
486
|
+
): Promise<Record<string, any>> {
|
|
487
|
+
if (!fs.existsSync(filePath)) {
|
|
488
|
+
throw new Error(`File does not exist: ${filePath}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const stat = fs.statSync(filePath);
|
|
492
|
+
const hash = this.enableHash ? this.calculateFileHashSync(filePath) : '';
|
|
493
|
+
|
|
494
|
+
const fileData = {
|
|
495
|
+
path: filePath,
|
|
496
|
+
name: path.basename(filePath),
|
|
497
|
+
created_at: stat.mtime.getTime(),
|
|
498
|
+
imported_at: Date.now(),
|
|
499
|
+
size: stat.size,
|
|
500
|
+
hash,
|
|
501
|
+
...fileMeta,
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
await this.handleFile(filePath, fileData, options?.importType || 'copy');
|
|
505
|
+
return this.createFile(fileData);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async getFileFolder(fileId: number): Promise<Record<string, any>[]> {
|
|
509
|
+
const rows = await this.getSql(
|
|
510
|
+
'SELECT f.* FROM folders f JOIN files fi ON fi.folder_id = f.id WHERE fi.id = ?',
|
|
511
|
+
[fileId]
|
|
512
|
+
);
|
|
513
|
+
return rows.map(row => this.rowToMap(row));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async getFileTags(fileId: number): Promise<Record<string, any>[]> {
|
|
517
|
+
const rows = await this.getSql('SELECT tags FROM files WHERE id = ?', [fileId]);
|
|
518
|
+
if (rows.length === 0) return [];
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const tagsStr = rows[0].tags;
|
|
522
|
+
if (!tagsStr) return [];
|
|
523
|
+
|
|
524
|
+
const tagIds = JSON.parse(tagsStr).filter((id: any) => id);
|
|
525
|
+
if (tagIds.length === 0) return [];
|
|
526
|
+
|
|
527
|
+
const tagRows = await this.getSql(
|
|
528
|
+
`SELECT * FROM tags WHERE id IN (${tagIds.map(() => '?').join(',')})`,
|
|
529
|
+
tagIds
|
|
530
|
+
);
|
|
531
|
+
return tagRows.map(row => this.rowToMap(row));
|
|
532
|
+
} catch (err) {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async setFileFolder(fileId: number, folderId: string): Promise<boolean> {
|
|
538
|
+
if (!folderId) return false;
|
|
539
|
+
|
|
540
|
+
await this.beginTransaction();
|
|
541
|
+
try {
|
|
542
|
+
const result = await this.runSql('UPDATE files SET folder_id = ? WHERE id = ?', [
|
|
543
|
+
folderId,
|
|
544
|
+
fileId,
|
|
545
|
+
]);
|
|
546
|
+
await this.commitTransaction();
|
|
547
|
+
return result.changes > 0;
|
|
548
|
+
} catch (err) {
|
|
549
|
+
await this.rollbackTransaction();
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async setFileTags(fileId: number, tagIds: string[]): Promise<boolean> {
|
|
555
|
+
await this.beginTransaction();
|
|
556
|
+
try {
|
|
557
|
+
const result = await this.runSql('UPDATE files SET tags = ? WHERE id = ?', [
|
|
558
|
+
JSON.stringify(tagIds),
|
|
559
|
+
fileId,
|
|
560
|
+
]);
|
|
561
|
+
await this.commitTransaction();
|
|
562
|
+
return result.changes > 0;
|
|
563
|
+
} catch (err) {
|
|
564
|
+
await this.rollbackTransaction();
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async getAllTags(): Promise<Record<string, any>[]> {
|
|
570
|
+
const rows = await this.getSql('SELECT * FROM tags', []);
|
|
571
|
+
return rows.map(row => this.rowToMap(row));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async getAllFolders(): Promise<Record<string, any>[]> {
|
|
575
|
+
const rows = await this.getSql('SELECT * FROM folders', []);
|
|
576
|
+
return rows.map(row => this.rowToMap(row));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
getLibraryId(): string {
|
|
580
|
+
return this.config.id;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async getItemPath(item: Record<string, any>): Promise<string> {
|
|
584
|
+
const libraryPath = await this.getLibraryPath();
|
|
585
|
+
const folderName = await this.getFolderName(item.folder_id);
|
|
586
|
+
return path.join(libraryPath, folderName);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async getItemFilePath(item: Record<string, any>, options?: { isUrlFile: boolean }): Promise<string> {
|
|
590
|
+
const libraryPath = await this.getLibraryPath();
|
|
591
|
+
const folderName = await this.getFolderName(item.folder_id);
|
|
592
|
+
const filePath = path.join(libraryPath, folderName, item.name);
|
|
593
|
+
return options?.isUrlFile && this.httpServer ? this.httpServer.getPublicURL(`api/file/${this.getLibraryId()}/${item.id}`) : filePath
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async getItemThumbPath(
|
|
597
|
+
item: Record<string, any>,
|
|
598
|
+
options?: { isUrlFile: boolean }
|
|
599
|
+
): Promise<string> {
|
|
600
|
+
const libraryPath = await this.getLibraryPath();
|
|
601
|
+
const fileName = item.hash ? `${item.hash}.png` : `${item.id}.png`;
|
|
602
|
+
const thumbFile = path.join(libraryPath, 'thumbs', fileName);
|
|
603
|
+
return options?.isUrlFile && this.httpServer ? this.httpServer.getPublicURL(`api/thumb/${this.getLibraryId()}/${item.id}`) : thumbFile
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
getEventManager(): EventManager | undefined {
|
|
607
|
+
return this.eventManager;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private rowToMap(row: any): Record<string, any> {
|
|
611
|
+
const map: Record<string, any> = {};
|
|
612
|
+
for (const key in row) {
|
|
613
|
+
map[key] = row[key];
|
|
614
|
+
}
|
|
615
|
+
return map;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private calculateFileHashSync(filePath: string): string {
|
|
619
|
+
const buffer = fs.readFileSync(filePath);
|
|
620
|
+
// 这里应该使用实际的哈希算法实现
|
|
621
|
+
return buffer.toString('hex').substring(0, 32); // 简化示例
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private async handleFile(
|
|
625
|
+
filePath: string,
|
|
626
|
+
fileData: Record<string, any>,
|
|
627
|
+
importType: string
|
|
628
|
+
): Promise<void> {
|
|
629
|
+
const destPath = path.join(await this.getItemPath(fileData), fileData.name);
|
|
630
|
+
switch (importType) {
|
|
631
|
+
case 'link':
|
|
632
|
+
// 保持原文件位置不变
|
|
633
|
+
break;
|
|
634
|
+
case 'copy':
|
|
635
|
+
const destDir = path.dirname(destPath);
|
|
636
|
+
if (!fs.existsSync(destDir)) {
|
|
637
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
638
|
+
}
|
|
639
|
+
fs.copyFileSync(filePath, destPath);
|
|
640
|
+
fileData.path = destPath;
|
|
641
|
+
break;
|
|
642
|
+
case 'move':
|
|
643
|
+
const destDir2 = path.dirname(destPath);
|
|
644
|
+
if (!fs.existsSync(destDir2)) {
|
|
645
|
+
fs.mkdirSync(destDir2, { recursive: true });
|
|
646
|
+
}
|
|
647
|
+
// 如果不同是跨盘符操作,则单独复制一份,再删除源文件
|
|
648
|
+
if (path.parse(filePath).root !== path.parse(destPath).root) {
|
|
649
|
+
fs.copyFileSync(filePath, destPath);
|
|
650
|
+
fs.unlinkSync(filePath);
|
|
651
|
+
} else {
|
|
652
|
+
fs.renameSync(filePath, destPath);
|
|
653
|
+
}
|
|
654
|
+
fileData.path = destPath;
|
|
655
|
+
break;
|
|
656
|
+
default:
|
|
657
|
+
throw new Error(`Unknown import type: ${importType}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private async getFolderName(folderId?: number): Promise<string> {
|
|
662
|
+
if (folderId) {
|
|
663
|
+
const folder = await this.getFolder(folderId);
|
|
664
|
+
if (folder) return folder.title;
|
|
665
|
+
}
|
|
666
|
+
return '未分类';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private executeSql(sql: string, params?: any[]): Promise<void> {
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
671
|
+
if (!this.db) {
|
|
672
|
+
reject(new Error('Database not initialized'));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
this.db.run(sql, params, (err) => {
|
|
677
|
+
if (err) reject(err);
|
|
678
|
+
else resolve();
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private runSql(sql: string, params?: any[]): Promise<{ lastID: number; changes: number }> {
|
|
684
|
+
return new Promise((resolve, reject) => {
|
|
685
|
+
if (!this.db) {
|
|
686
|
+
reject(new Error('Database not initialized'));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
this.db.run(sql, params, function (err) {
|
|
691
|
+
if (err) reject(err);
|
|
692
|
+
else resolve({ lastID: this.lastID, changes: this.changes });
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private getSql(sql: string, params?: any[]): Promise<any[]> {
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
if (!this.db) {
|
|
700
|
+
reject(new Error('Database not initialized'));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
this.db.all(sql, params, (err, rows) => {
|
|
705
|
+
if (err) reject(err);
|
|
706
|
+
else resolve(rows);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async getLibraryPath(): Promise<string> {
|
|
712
|
+
return this.config.customFields?.path || '';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async query(sql: string, params?: any[]): Promise<any[]> {
|
|
716
|
+
return this.getSql(sql, params);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async getLibraryInfo(): Promise<Record<string, any>> {
|
|
720
|
+
const tags = await this.getAllTags();
|
|
721
|
+
const folders = await this.getAllFolders();
|
|
722
|
+
return {
|
|
723
|
+
libraryId: this.getLibraryId(),
|
|
724
|
+
status: 'connected',
|
|
725
|
+
tags, folders,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 查询方法
|
|
730
|
+
async queryFile(query: Record<string, any>, isUrlFile: boolean = true): Promise<Record<string, any>[]> {
|
|
731
|
+
const { result } = await this.getFiles({ filters: query });
|
|
732
|
+
return this.processingFiles(result, isUrlFile);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async processingFiles(files: Record<string, any>[], isUrlFile: boolean = true) {
|
|
736
|
+
return Promise.all(files.map(async (file) => {
|
|
737
|
+
return {
|
|
738
|
+
...file, ...{
|
|
739
|
+
thumb: await this.getItemThumbPath(file, { isUrlFile }),
|
|
740
|
+
path: await this.getItemFilePath(file, { isUrlFile }),
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
}))
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async queryFolder(query: Record<string, any>): Promise<Record<string, any>[]> {
|
|
747
|
+
const folders = await this.getFolders();
|
|
748
|
+
return folders.filter(folder => {
|
|
749
|
+
return Object.entries(query).every(([key, value]) => {
|
|
750
|
+
return folder[key] === value;
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async queryLibrary(query: Record<string, any>): Promise<Record<string, any>> {
|
|
756
|
+
return this.getLibraryInfo();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async createLibrary(data: Record<string, any>): Promise<Record<string, any>> {
|
|
760
|
+
this.config.id = data.id || this.config.id;
|
|
761
|
+
this.config.customFields = { ...this.config.customFields, ...data };
|
|
762
|
+
return this.getLibraryInfo();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async closeLibrary(): Promise<boolean> {
|
|
766
|
+
await this.close();
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async queryTag(query: Record<string, any>): Promise<Record<string, any>[]> {
|
|
771
|
+
const tags = await this.getTags();
|
|
772
|
+
return tags.filter(tag => {
|
|
773
|
+
return Object.entries(query).every(([key, value]) => {
|
|
774
|
+
return tag[key] === value;
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|