hub-repo-tracker 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/app.d.ts +2 -0
- package/dist/app.js +149 -0
- package/dist/features/backup/repository.d.ts +24 -0
- package/dist/features/backup/repository.js +236 -0
- package/dist/features/backup/routes.d.ts +2 -0
- package/dist/features/backup/routes.js +329 -0
- package/dist/features/backup/schema.d.ts +108 -0
- package/dist/features/backup/schema.js +75 -0
- package/dist/features/backup/service.d.ts +7 -0
- package/dist/features/backup/service.js +67 -0
- package/dist/features/backup/types.d.ts +72 -0
- package/dist/features/backup/types.js +1 -0
- package/dist/features/categories/repository.d.ts +11 -0
- package/dist/features/categories/repository.js +68 -0
- package/dist/features/categories/routes.d.ts +2 -0
- package/dist/features/categories/routes.js +113 -0
- package/dist/features/categories/schema.d.ts +105 -0
- package/dist/features/categories/schema.js +64 -0
- package/dist/features/categories/service.d.ts +8 -0
- package/dist/features/categories/service.js +46 -0
- package/dist/features/categories/types.d.ts +24 -0
- package/dist/features/categories/types.js +1 -0
- package/dist/features/dashboard/routes.d.ts +2 -0
- package/dist/features/dashboard/routes.js +117 -0
- package/dist/features/import/routes.d.ts +2 -0
- package/dist/features/import/routes.js +18 -0
- package/dist/features/import/schema.d.ts +89 -0
- package/dist/features/import/schema.js +58 -0
- package/dist/features/import/service.d.ts +7 -0
- package/dist/features/import/service.js +197 -0
- package/dist/features/import/types.d.ts +21 -0
- package/dist/features/import/types.js +1 -0
- package/dist/features/repos/repository.d.ts +23 -0
- package/dist/features/repos/repository.js +236 -0
- package/dist/features/repos/routes.d.ts +2 -0
- package/dist/features/repos/routes.js +311 -0
- package/dist/features/repos/schema.d.ts +310 -0
- package/dist/features/repos/schema.js +212 -0
- package/dist/features/repos/service.d.ts +21 -0
- package/dist/features/repos/service.js +171 -0
- package/dist/features/repos/types.d.ts +97 -0
- package/dist/features/repos/types.js +1 -0
- package/dist/features/sync/github-client.d.ts +39 -0
- package/dist/features/sync/github-client.js +163 -0
- package/dist/features/sync/routes.d.ts +2 -0
- package/dist/features/sync/routes.js +74 -0
- package/dist/features/sync/service.d.ts +28 -0
- package/dist/features/sync/service.js +206 -0
- package/dist/features/system/routes.d.ts +2 -0
- package/dist/features/system/routes.js +43 -0
- package/dist/public/assets/index-BXnOED59.css +1 -0
- package/dist/public/assets/index-DIAgE4hq.js +52 -0
- package/dist/public/index.html +20 -0
- package/dist/shared/config/index.d.ts +18 -0
- package/dist/shared/config/index.js +29 -0
- package/dist/shared/db/index.d.ts +4 -0
- package/dist/shared/db/index.js +126 -0
- package/dist/shared/jobs/sync-job.d.ts +3 -0
- package/dist/shared/jobs/sync-job.js +42 -0
- package/dist/shared/logger.d.ts +2 -0
- package/dist/shared/logger.js +29 -0
- package/dist/shared/middleware/error.d.ts +33 -0
- package/dist/shared/middleware/error.js +78 -0
- package/dist/shared/utils/semver.d.ts +19 -0
- package/dist/shared/utils/semver.js +59 -0
- package/package.json +54 -0
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import cors from '@fastify/cors';
|
|
4
|
+
import multipart from '@fastify/multipart';
|
|
5
|
+
import fastifyStatic from '@fastify/static';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
import { config, validateConfig } from './shared/config/index.js';
|
|
12
|
+
import { initializeDatabase, closeDatabase } from './shared/db/index.js';
|
|
13
|
+
import { errorHandler, errorSchema } from './shared/middleware/error.js';
|
|
14
|
+
import { repoRoutes } from './features/repos/routes.js';
|
|
15
|
+
import { syncRoutes } from './features/sync/routes.js';
|
|
16
|
+
import { dashboardRoutes } from './features/dashboard/routes.js';
|
|
17
|
+
import { categoryRoutes } from './features/categories/routes.js';
|
|
18
|
+
import { importRoutes } from './features/import/routes.js';
|
|
19
|
+
import { systemRoutes } from './features/system/routes.js';
|
|
20
|
+
import { backupRoutes } from './features/backup/routes.js';
|
|
21
|
+
import { startSyncJob, stopSyncJob } from './shared/jobs/sync-job.js';
|
|
22
|
+
import { repoSchemas } from './features/repos/schema.js';
|
|
23
|
+
import { categorySchemas } from './features/categories/schema.js';
|
|
24
|
+
// Validate config on startup
|
|
25
|
+
try {
|
|
26
|
+
validateConfig();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error('Configuration error:', error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Initialize database
|
|
33
|
+
initializeDatabase();
|
|
34
|
+
// Create Fastify instance
|
|
35
|
+
const app = Fastify({
|
|
36
|
+
logger: {
|
|
37
|
+
level: config.nodeEnv === 'development' ? 'info' : 'warn',
|
|
38
|
+
transport: config.nodeEnv === 'development'
|
|
39
|
+
? { target: 'pino-pretty', options: { colorize: true } }
|
|
40
|
+
: undefined,
|
|
41
|
+
},
|
|
42
|
+
ajv: {
|
|
43
|
+
customOptions: {
|
|
44
|
+
coerceTypes: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
// Register plugins
|
|
49
|
+
await app.register(cors, {
|
|
50
|
+
origin: true, // Allow all origins in development
|
|
51
|
+
});
|
|
52
|
+
// Register multipart for file uploads
|
|
53
|
+
await app.register(multipart, {
|
|
54
|
+
limits: {
|
|
55
|
+
fileSize: 100 * 1024 * 1024, // 100MB limit for SQLite backups
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
// Set custom error handler
|
|
59
|
+
app.setErrorHandler(errorHandler);
|
|
60
|
+
// Add response schema for errors
|
|
61
|
+
app.addSchema({
|
|
62
|
+
$id: 'error',
|
|
63
|
+
...errorSchema,
|
|
64
|
+
});
|
|
65
|
+
// Add shared schemas for repos and categories
|
|
66
|
+
app.addSchema({
|
|
67
|
+
$id: 'repo',
|
|
68
|
+
...repoSchemas.repo,
|
|
69
|
+
});
|
|
70
|
+
app.addSchema({
|
|
71
|
+
$id: 'category',
|
|
72
|
+
...categorySchemas.category,
|
|
73
|
+
});
|
|
74
|
+
// Health check
|
|
75
|
+
app.get('/health', async () => ({
|
|
76
|
+
status: 'ok',
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
}));
|
|
79
|
+
// Register routes
|
|
80
|
+
await app.register(repoRoutes);
|
|
81
|
+
await app.register(syncRoutes);
|
|
82
|
+
await app.register(dashboardRoutes);
|
|
83
|
+
await app.register(categoryRoutes);
|
|
84
|
+
await app.register(importRoutes);
|
|
85
|
+
await app.register(systemRoutes);
|
|
86
|
+
await app.register(backupRoutes);
|
|
87
|
+
// Serve static files (Frontend) - MUST be after API routes to avoid conflicts (or use prefix)
|
|
88
|
+
// configured to serve from 'public' directory which will be in 'dist/public'
|
|
89
|
+
await app.register(fastifyStatic, {
|
|
90
|
+
root: path.join(__dirname, 'public'),
|
|
91
|
+
prefix: '/',
|
|
92
|
+
constraints: {} // optional
|
|
93
|
+
});
|
|
94
|
+
// SPA Fallback: Serve index.html for any non-API route not handled above
|
|
95
|
+
app.setNotFoundHandler(async (req, reply) => {
|
|
96
|
+
if (req.raw.url?.startsWith('/api')) {
|
|
97
|
+
return reply.status(404).send({
|
|
98
|
+
error: 'Not Found',
|
|
99
|
+
message: 'Route not found',
|
|
100
|
+
statusCode: 404
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return reply.sendFile('index.html');
|
|
104
|
+
});
|
|
105
|
+
// Start scheduled sync job
|
|
106
|
+
startSyncJob();
|
|
107
|
+
// Graceful shutdown
|
|
108
|
+
const gracefulShutdown = async (signal) => {
|
|
109
|
+
app.log.info(`Received ${signal}, shutting down gracefully...`);
|
|
110
|
+
try {
|
|
111
|
+
stopSyncJob();
|
|
112
|
+
await app.close();
|
|
113
|
+
closeDatabase();
|
|
114
|
+
app.log.info('Server closed');
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
app.log.error(error, 'Error during shutdown');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
123
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
124
|
+
// Start server
|
|
125
|
+
const start = async () => {
|
|
126
|
+
try {
|
|
127
|
+
const address = await app.listen({ port: config.port, host: '0.0.0.0' });
|
|
128
|
+
app.log.info(`Server listening at ${address}`);
|
|
129
|
+
// Auto-open browser in production/CLI mode (not in Docker)
|
|
130
|
+
if (config.nodeEnv !== 'development' && !process.env.DOCKER) {
|
|
131
|
+
try {
|
|
132
|
+
const url = `http://localhost:${config.port}`;
|
|
133
|
+
app.log.info(`Opening browser at ${url}`);
|
|
134
|
+
await open(url);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
app.log.warn('Failed to open browser automatically');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
app.log.error(error, 'Failed to start server');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
// Only start server if run directly
|
|
147
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
148
|
+
start();
|
|
149
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BackupData, BackupRepo, BackupCategory, BackupSyncState, BackupVersionHistory, RestoreResult } from './types.js';
|
|
2
|
+
export declare const backupRepository: {
|
|
3
|
+
exportRepos(): BackupRepo[];
|
|
4
|
+
exportCategories(): BackupCategory[];
|
|
5
|
+
exportSyncStates(): BackupSyncState[];
|
|
6
|
+
exportVersionHistory(): BackupVersionHistory[];
|
|
7
|
+
clearAllData(): void;
|
|
8
|
+
importCategory(category: BackupCategory, idMapping: Map<number, number>): number | null;
|
|
9
|
+
importRepo(repo: BackupRepo, categoryIdMapping: Map<number, number>, repoIdMapping: Map<number, number>): number | null;
|
|
10
|
+
importSyncState(syncState: BackupSyncState, repoIdMapping: Map<number, number>): boolean;
|
|
11
|
+
importVersionHistory(history: BackupVersionHistory, repoIdMapping: Map<number, number>): boolean;
|
|
12
|
+
_clearDataIfReplace(mode: "merge" | "replace"): void;
|
|
13
|
+
_importCategories(categories: BackupCategory[], categoryIdMapping: Map<number, number>): {
|
|
14
|
+
imported: number;
|
|
15
|
+
skipped: number;
|
|
16
|
+
};
|
|
17
|
+
_importRepos(repos: BackupRepo[], categoryIdMapping: Map<number, number>, repoIdMapping: Map<number, number>): {
|
|
18
|
+
imported: number;
|
|
19
|
+
skipped: number;
|
|
20
|
+
};
|
|
21
|
+
_importSyncStates(syncStates: BackupSyncState[], repoIdMapping: Map<number, number>): number;
|
|
22
|
+
_importVersionHistoryFromRepos(repos: BackupRepo[], repoIdMapping: Map<number, number>): number;
|
|
23
|
+
restoreFromBackup(data: BackupData, mode: "merge" | "replace"): RestoreResult;
|
|
24
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { db } from '../../shared/db/index.js';
|
|
2
|
+
export const backupRepository = {
|
|
3
|
+
// Export all repos with version history
|
|
4
|
+
exportRepos() {
|
|
5
|
+
const repos = db.prepare(`
|
|
6
|
+
SELECT * FROM repos ORDER BY created_at ASC
|
|
7
|
+
`).all();
|
|
8
|
+
// Get version history for each repo
|
|
9
|
+
const historyStmt = db.prepare(`
|
|
10
|
+
SELECT * FROM version_history WHERE repo_id = ? ORDER BY detected_at DESC
|
|
11
|
+
`);
|
|
12
|
+
return repos.map(repo => ({
|
|
13
|
+
...repo,
|
|
14
|
+
version_history: historyStmt.all(repo.id),
|
|
15
|
+
}));
|
|
16
|
+
},
|
|
17
|
+
// Export all categories
|
|
18
|
+
exportCategories() {
|
|
19
|
+
return db.prepare(`
|
|
20
|
+
SELECT * FROM categories ORDER BY created_at ASC
|
|
21
|
+
`).all();
|
|
22
|
+
},
|
|
23
|
+
// Export all sync states
|
|
24
|
+
exportSyncStates() {
|
|
25
|
+
return db.prepare(`
|
|
26
|
+
SELECT * FROM sync_state
|
|
27
|
+
`).all();
|
|
28
|
+
},
|
|
29
|
+
// Export all version history
|
|
30
|
+
exportVersionHistory() {
|
|
31
|
+
return db.prepare(`
|
|
32
|
+
SELECT * FROM version_history ORDER BY detected_at DESC
|
|
33
|
+
`).all();
|
|
34
|
+
},
|
|
35
|
+
// Clear all data (for replace mode)
|
|
36
|
+
clearAllData() {
|
|
37
|
+
db.exec(`
|
|
38
|
+
DELETE FROM version_history;
|
|
39
|
+
DELETE FROM sync_state;
|
|
40
|
+
DELETE FROM repos;
|
|
41
|
+
DELETE FROM categories;
|
|
42
|
+
`);
|
|
43
|
+
},
|
|
44
|
+
// Import category
|
|
45
|
+
importCategory(category, idMapping) {
|
|
46
|
+
// Check if category already exists by name
|
|
47
|
+
const existing = db.prepare('SELECT id FROM categories WHERE name = ?').get(category.name);
|
|
48
|
+
if (existing) {
|
|
49
|
+
idMapping.set(category.id, existing.id);
|
|
50
|
+
return null; // Skipped
|
|
51
|
+
}
|
|
52
|
+
const result = db.prepare(`
|
|
53
|
+
INSERT INTO categories (name, type, color, icon, owner_name, created_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
55
|
+
`).run(category.name, category.type || 'custom', category.color || '#6366f1', category.icon || null, category.owner_name || null, category.created_at);
|
|
56
|
+
const newId = result.lastInsertRowid;
|
|
57
|
+
idMapping.set(category.id, newId);
|
|
58
|
+
return newId;
|
|
59
|
+
},
|
|
60
|
+
// Import repo
|
|
61
|
+
importRepo(repo, categoryIdMapping, repoIdMapping) {
|
|
62
|
+
// Check if repo already exists by full_name
|
|
63
|
+
const existing = db.prepare('SELECT id FROM repos WHERE full_name = ?').get(repo.full_name);
|
|
64
|
+
if (existing) {
|
|
65
|
+
repoIdMapping.set(repo.id, existing.id);
|
|
66
|
+
return null; // Skipped
|
|
67
|
+
}
|
|
68
|
+
// Map old category_id to new one
|
|
69
|
+
const newCategoryId = repo.category_id ? categoryIdMapping.get(repo.category_id) ?? null : null;
|
|
70
|
+
const result = db.prepare(`
|
|
71
|
+
INSERT INTO repos (github_id, owner, name, full_name, url, description, notes, category_id, installed_version, local_path, is_favorite, created_at, updated_at)
|
|
72
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
+
`).run(repo.github_id || null, repo.owner, repo.name, repo.full_name, repo.url, repo.description || null, repo.notes || null, newCategoryId, repo.installed_version || null, repo.local_path || null, repo.is_favorite || 0, repo.created_at, repo.updated_at);
|
|
74
|
+
const newId = result.lastInsertRowid;
|
|
75
|
+
repoIdMapping.set(repo.id, newId);
|
|
76
|
+
return newId;
|
|
77
|
+
},
|
|
78
|
+
// Import sync state
|
|
79
|
+
importSyncState(syncState, repoIdMapping) {
|
|
80
|
+
const newRepoId = repoIdMapping.get(syncState.repo_id);
|
|
81
|
+
if (!newRepoId)
|
|
82
|
+
return false;
|
|
83
|
+
// Check if sync state already exists
|
|
84
|
+
const existing = db.prepare('SELECT repo_id FROM sync_state WHERE repo_id = ?').get(newRepoId);
|
|
85
|
+
if (existing) {
|
|
86
|
+
// Update existing
|
|
87
|
+
db.prepare(`
|
|
88
|
+
UPDATE sync_state SET
|
|
89
|
+
last_commit_sha = ?,
|
|
90
|
+
last_commit_date = ?,
|
|
91
|
+
last_commit_message = ?,
|
|
92
|
+
last_commit_author = ?,
|
|
93
|
+
last_release_tag = ?,
|
|
94
|
+
last_release_date = ?,
|
|
95
|
+
last_release_notes = ?,
|
|
96
|
+
last_tag = ?,
|
|
97
|
+
last_tag_date = ?,
|
|
98
|
+
acknowledged_release = ?,
|
|
99
|
+
release_notification_active = ?,
|
|
100
|
+
last_sync_at = ?,
|
|
101
|
+
has_updates = ?
|
|
102
|
+
WHERE repo_id = ?
|
|
103
|
+
`).run(syncState.last_commit_sha, syncState.last_commit_date, syncState.last_commit_message, syncState.last_commit_author, syncState.last_release_tag, syncState.last_release_date, syncState.last_release_notes, syncState.last_tag, syncState.last_tag_date, syncState.acknowledged_release, syncState.release_notification_active, syncState.last_sync_at, syncState.has_updates, newRepoId);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Insert new
|
|
107
|
+
db.prepare(`
|
|
108
|
+
INSERT INTO sync_state (
|
|
109
|
+
repo_id, last_commit_sha, last_commit_date, last_commit_message, last_commit_author,
|
|
110
|
+
last_release_tag, last_release_date, last_release_notes, last_tag, last_tag_date,
|
|
111
|
+
acknowledged_release, release_notification_active, last_sync_at, has_updates
|
|
112
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
113
|
+
`).run(newRepoId, syncState.last_commit_sha, syncState.last_commit_date, syncState.last_commit_message, syncState.last_commit_author, syncState.last_release_tag, syncState.last_release_date, syncState.last_release_notes, syncState.last_tag, syncState.last_tag_date, syncState.acknowledged_release, syncState.release_notification_active, syncState.last_sync_at, syncState.has_updates);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
// Import version history
|
|
118
|
+
importVersionHistory(history, repoIdMapping) {
|
|
119
|
+
const newRepoId = repoIdMapping.get(history.repo_id);
|
|
120
|
+
if (!newRepoId)
|
|
121
|
+
return false;
|
|
122
|
+
// Check if this version history entry already exists
|
|
123
|
+
const existing = db.prepare(`
|
|
124
|
+
SELECT id FROM version_history
|
|
125
|
+
WHERE repo_id = ? AND version_type = ? AND version_value = ? AND detected_at = ?
|
|
126
|
+
`).get(newRepoId, history.version_type, history.version_value, history.detected_at);
|
|
127
|
+
if (existing)
|
|
128
|
+
return false;
|
|
129
|
+
db.prepare(`
|
|
130
|
+
INSERT INTO version_history (repo_id, version_type, version_value, release_notes, detected_at, acknowledged_at)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
132
|
+
`).run(newRepoId, history.version_type, history.version_value, history.release_notes, history.detected_at, history.acknowledged_at);
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
// Clear data if replace mode (internal)
|
|
136
|
+
_clearDataIfReplace(mode) {
|
|
137
|
+
if (mode === 'replace') {
|
|
138
|
+
this.clearAllData();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
// Import all categories from backup (internal)
|
|
142
|
+
_importCategories(categories, categoryIdMapping) {
|
|
143
|
+
let imported = 0;
|
|
144
|
+
let skipped = 0;
|
|
145
|
+
for (const category of categories) {
|
|
146
|
+
const result = this.importCategory(category, categoryIdMapping);
|
|
147
|
+
if (result !== null) {
|
|
148
|
+
imported++;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
skipped++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { imported, skipped };
|
|
155
|
+
},
|
|
156
|
+
// Import all repos from backup (internal)
|
|
157
|
+
_importRepos(repos, categoryIdMapping, repoIdMapping) {
|
|
158
|
+
let imported = 0;
|
|
159
|
+
let skipped = 0;
|
|
160
|
+
for (const repo of repos) {
|
|
161
|
+
const result = this.importRepo(repo, categoryIdMapping, repoIdMapping);
|
|
162
|
+
if (result !== null) {
|
|
163
|
+
imported++;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
skipped++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { imported, skipped };
|
|
170
|
+
},
|
|
171
|
+
// Import all sync states from backup (internal)
|
|
172
|
+
_importSyncStates(syncStates, repoIdMapping) {
|
|
173
|
+
let restored = 0;
|
|
174
|
+
for (const syncState of syncStates) {
|
|
175
|
+
if (this.importSyncState(syncState, repoIdMapping)) {
|
|
176
|
+
restored++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return restored;
|
|
180
|
+
},
|
|
181
|
+
// Import all version history from backup repos (internal)
|
|
182
|
+
_importVersionHistoryFromRepos(repos, repoIdMapping) {
|
|
183
|
+
let restored = 0;
|
|
184
|
+
for (const repo of repos) {
|
|
185
|
+
if (repo.version_history) {
|
|
186
|
+
for (const history of repo.version_history) {
|
|
187
|
+
if (this.importVersionHistory(history, repoIdMapping)) {
|
|
188
|
+
restored++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return restored;
|
|
194
|
+
},
|
|
195
|
+
// Full restore from backup data
|
|
196
|
+
restoreFromBackup(data, mode) {
|
|
197
|
+
const result = {
|
|
198
|
+
success: true,
|
|
199
|
+
message: '',
|
|
200
|
+
stats: {
|
|
201
|
+
repos_imported: 0,
|
|
202
|
+
repos_skipped: 0,
|
|
203
|
+
categories_imported: 0,
|
|
204
|
+
categories_skipped: 0,
|
|
205
|
+
sync_states_restored: 0,
|
|
206
|
+
version_history_restored: 0,
|
|
207
|
+
},
|
|
208
|
+
errors: [],
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
const restoreTransaction = db.transaction(() => {
|
|
212
|
+
const categoryIdMapping = new Map();
|
|
213
|
+
const repoIdMapping = new Map();
|
|
214
|
+
this._clearDataIfReplace(mode);
|
|
215
|
+
const categoryResult = this._importCategories(data.data.categories, categoryIdMapping);
|
|
216
|
+
result.stats.categories_imported = categoryResult.imported;
|
|
217
|
+
result.stats.categories_skipped = categoryResult.skipped;
|
|
218
|
+
const repoResult = this._importRepos(data.data.repos, categoryIdMapping, repoIdMapping);
|
|
219
|
+
result.stats.repos_imported = repoResult.imported;
|
|
220
|
+
result.stats.repos_skipped = repoResult.skipped;
|
|
221
|
+
result.stats.sync_states_restored = this._importSyncStates(data.data.sync_state, repoIdMapping);
|
|
222
|
+
result.stats.version_history_restored = this._importVersionHistoryFromRepos(data.data.repos, repoIdMapping);
|
|
223
|
+
});
|
|
224
|
+
restoreTransaction();
|
|
225
|
+
result.message = mode === 'replace'
|
|
226
|
+
? 'Data replaced successfully'
|
|
227
|
+
: 'Data merged successfully';
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
result.success = false;
|
|
231
|
+
result.message = 'Restore failed';
|
|
232
|
+
result.errors.push(error instanceof Error ? error.message : 'Unknown error');
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
},
|
|
236
|
+
};
|