@tb.p/dd 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/controllers/getExtensionsController.js +27 -0
- package/controllers/newController.js +72 -0
- package/controllers/resumeController.js +233 -0
- package/database/README.md +262 -0
- package/database/dbConnection.js +314 -0
- package/database/dbOperations.js +332 -0
- package/database/dbUtils.js +125 -0
- package/database/dbValidator.js +325 -0
- package/database/index.js +102 -0
- package/index.js +75 -0
- package/package.json +32 -0
- package/processors/optionETL.js +82 -0
- package/utils/README.md +261 -0
- package/utils/candidateDetection.js +541 -0
- package/utils/duplicateMover.js +140 -0
- package/utils/duplicateReporter.js +91 -0
- package/utils/fileHasher.js +195 -0
- package/utils/fileMover.js +180 -0
- package/utils/fileScanner.js +128 -0
- package/utils/fileSystemUtils.js +192 -0
- package/utils/index.js +5 -0
- package/validators/optionValidator.js +103 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import sqlite3 from 'sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database connection manager for the file deduplication tool
|
|
7
|
+
* Handles SQLite database initialization, connection, and cleanup
|
|
8
|
+
*/
|
|
9
|
+
class DatabaseConnection {
|
|
10
|
+
constructor(dbPath) {
|
|
11
|
+
this.dbPath = dbPath;
|
|
12
|
+
this.db = null;
|
|
13
|
+
this.isConnected = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize database connection and create tables if needed
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
async connect() {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
try {
|
|
23
|
+
// Ensure directory exists
|
|
24
|
+
const dir = path.dirname(this.dbPath);
|
|
25
|
+
if (!fs.existsSync(dir)) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
|
30
|
+
if (err) {
|
|
31
|
+
reject(new Error(`Failed to connect to database: ${err.message}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.isConnected = true;
|
|
36
|
+
resolve();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Enable foreign key constraints
|
|
40
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
41
|
+
|
|
42
|
+
// Create tables if they don't exist
|
|
43
|
+
this.createTables()
|
|
44
|
+
.then(() => resolve())
|
|
45
|
+
.catch(reject);
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
reject(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create database tables if they don't exist
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async createTables() {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const createTablesSQL = `
|
|
60
|
+
-- Meta table for storing configuration parameters
|
|
61
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
62
|
+
key TEXT PRIMARY KEY,
|
|
63
|
+
value TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Copies table for storing file paths and their metadata
|
|
67
|
+
CREATE TABLE IF NOT EXISTS copies (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
dir_group TEXT,
|
|
70
|
+
file_path TEXT NOT NULL,
|
|
71
|
+
file_name TEXT NOT NULL,
|
|
72
|
+
file_extension TEXT,
|
|
73
|
+
file_size INTEGER NOT NULL,
|
|
74
|
+
file_hash TEXT,
|
|
75
|
+
active BOOLEAN DEFAULT 1,
|
|
76
|
+
priority INTEGER DEFAULT 0
|
|
77
|
+
);
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
this.db.exec(createTablesSQL, (err) => {
|
|
81
|
+
if (err) {
|
|
82
|
+
reject(new Error(`Failed to create tables: ${err.message}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create indexes for better performance
|
|
87
|
+
this.createIndexes()
|
|
88
|
+
.then(() => resolve())
|
|
89
|
+
.catch(reject);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create database indexes for better performance
|
|
96
|
+
* @returns {Promise<void>}
|
|
97
|
+
*/
|
|
98
|
+
async createIndexes() {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const createIndexesSQL = `
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_copies_file_hash ON copies(file_hash);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_copies_file_path ON copies(file_path);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_copies_file_name ON copies(file_name);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_copies_dir_group ON copies(dir_group);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_copies_extension ON copies(file_extension);
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
this.db.exec(createIndexesSQL, (err) => {
|
|
109
|
+
if (err) {
|
|
110
|
+
reject(new Error(`Failed to create indexes: ${err.message}`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the database instance
|
|
120
|
+
* @returns {sqlite3.Database|null}
|
|
121
|
+
*/
|
|
122
|
+
getDatabase() {
|
|
123
|
+
if (!this.isConnected) {
|
|
124
|
+
throw new Error('Database not connected. Call connect() first.');
|
|
125
|
+
}
|
|
126
|
+
return this.db;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Execute a query with parameters
|
|
131
|
+
* @param {string} sql - SQL query
|
|
132
|
+
* @param {Array} params - Query parameters
|
|
133
|
+
* @returns {Promise<Array>} Query results
|
|
134
|
+
*/
|
|
135
|
+
async query(sql, params = []) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
this.db.all(sql, params, (err, rows) => {
|
|
138
|
+
if (err) {
|
|
139
|
+
reject(new Error(`Query failed: ${err.message}`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
resolve(rows);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute a single query with parameters
|
|
149
|
+
* @param {string} sql - SQL query
|
|
150
|
+
* @param {Array} params - Query parameters
|
|
151
|
+
* @returns {Promise<Object>} Single query result
|
|
152
|
+
*/
|
|
153
|
+
async queryOne(sql, params = []) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
this.db.get(sql, params, (err, row) => {
|
|
156
|
+
if (err) {
|
|
157
|
+
reject(new Error(`Query failed: ${err.message}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolve(row);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Execute a statement (INSERT, UPDATE, DELETE)
|
|
167
|
+
* @param {string} sql - SQL statement
|
|
168
|
+
* @param {Array} params - Statement parameters
|
|
169
|
+
* @returns {Promise<Object>} Statement result with lastID and changes
|
|
170
|
+
*/
|
|
171
|
+
async run(sql, params = []) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
this.db.run(sql, params, function(err) {
|
|
174
|
+
if (err) {
|
|
175
|
+
reject(new Error(`Statement failed: ${err.message}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
resolve({
|
|
179
|
+
lastID: this.lastID,
|
|
180
|
+
changes: this.changes
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Begin a transaction
|
|
188
|
+
* @returns {Promise<void>}
|
|
189
|
+
*/
|
|
190
|
+
async beginTransaction() {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
this.db.run('BEGIN TRANSACTION', (err) => {
|
|
193
|
+
if (err) {
|
|
194
|
+
reject(new Error(`Failed to begin transaction: ${err.message}`));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
resolve();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Commit a transaction
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async commit() {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
this.db.run('COMMIT', (err) => {
|
|
209
|
+
if (err) {
|
|
210
|
+
reject(new Error(`Failed to commit transaction: ${err.message}`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
resolve();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Rollback a transaction
|
|
220
|
+
* @returns {Promise<void>}
|
|
221
|
+
*/
|
|
222
|
+
async rollback() {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
this.db.run('ROLLBACK', (err) => {
|
|
225
|
+
if (err) {
|
|
226
|
+
reject(new Error(`Failed to rollback transaction: ${err.message}`));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
resolve();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Close database connection
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async close() {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
if (!this.db) {
|
|
241
|
+
resolve();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.db.close((err) => {
|
|
246
|
+
if (err) {
|
|
247
|
+
reject(new Error(`Failed to close database: ${err.message}`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.isConnected = false;
|
|
252
|
+
this.db = null;
|
|
253
|
+
resolve();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check if database file exists
|
|
260
|
+
* @returns {boolean}
|
|
261
|
+
*/
|
|
262
|
+
exists() {
|
|
263
|
+
return fs.existsSync(this.dbPath);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get database file size
|
|
268
|
+
* @returns {number} File size in bytes
|
|
269
|
+
*/
|
|
270
|
+
getFileSize() {
|
|
271
|
+
if (!this.exists()) {
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
const stats = fs.statSync(this.dbPath);
|
|
275
|
+
return stats.size;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get database information
|
|
280
|
+
* @returns {Promise<Object>} Database info
|
|
281
|
+
*/
|
|
282
|
+
async getInfo() {
|
|
283
|
+
if (!this.isConnected) {
|
|
284
|
+
throw new Error('Database not connected');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const [metaCount, copiesCount] = await Promise.all([
|
|
288
|
+
this.queryOne('SELECT COUNT(*) as count FROM meta'),
|
|
289
|
+
this.queryOne('SELECT COUNT(*) as count FROM copies')
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
path: this.dbPath,
|
|
294
|
+
fileSize: this.getFileSize(),
|
|
295
|
+
exists: this.exists(),
|
|
296
|
+
connected: this.isConnected,
|
|
297
|
+
tables: {
|
|
298
|
+
meta: metaCount.count,
|
|
299
|
+
copies: copiesCount.count
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create a new database connection
|
|
307
|
+
* @param {string} dbPath - Path to database file
|
|
308
|
+
* @returns {DatabaseConnection} Database connection instance
|
|
309
|
+
*/
|
|
310
|
+
function createConnection(dbPath) {
|
|
311
|
+
return new DatabaseConnection(dbPath);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { DatabaseConnection, createConnection };
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
class DatabaseOperations {
|
|
4
|
+
constructor(dbConnection) {
|
|
5
|
+
this.db = dbConnection;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async setMeta(key, value) {
|
|
9
|
+
try {
|
|
10
|
+
const result = await this.db.run(
|
|
11
|
+
'INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)',
|
|
12
|
+
[key, value]
|
|
13
|
+
);
|
|
14
|
+
return { success: true, changes: result.changes };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return { success: false, error: error.message };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async getAllMeta() {
|
|
22
|
+
try {
|
|
23
|
+
const results = await this.db.query('SELECT key, value FROM meta');
|
|
24
|
+
const meta = {};
|
|
25
|
+
results.forEach(row => {
|
|
26
|
+
meta[row.key] = row.value;
|
|
27
|
+
});
|
|
28
|
+
return meta;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to get all meta values: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async addCopy(fileInfo) {
|
|
41
|
+
try {
|
|
42
|
+
const {
|
|
43
|
+
path,
|
|
44
|
+
name,
|
|
45
|
+
extension,
|
|
46
|
+
size,
|
|
47
|
+
hash = null,
|
|
48
|
+
active = true,
|
|
49
|
+
priority = 0,
|
|
50
|
+
dirGroup = null
|
|
51
|
+
} = fileInfo;
|
|
52
|
+
|
|
53
|
+
const result = await this.db.run(
|
|
54
|
+
'INSERT INTO copies (dir_group, file_path, file_name, file_extension, file_size, file_hash, active, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
55
|
+
[dirGroup, path, name, extension, size, hash, active, priority]
|
|
56
|
+
);
|
|
57
|
+
return { success: true, id: result.lastID, changes: result.changes };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return { success: false, error: error.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async getUnhashedCopies(options = {}) {
|
|
67
|
+
try {
|
|
68
|
+
const { limit, offset } = options;
|
|
69
|
+
let sql = 'SELECT * FROM copies WHERE file_hash IS NULL ORDER BY file_path';
|
|
70
|
+
|
|
71
|
+
if (limit) {
|
|
72
|
+
sql += ` LIMIT ${limit}`;
|
|
73
|
+
if (offset) {
|
|
74
|
+
sql += ` OFFSET ${offset}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const results = await this.db.query(sql);
|
|
79
|
+
return results;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Failed to get unhashed copies: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async updateFileHash(copyId, hash) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await this.db.run(
|
|
88
|
+
'UPDATE copies SET file_hash = ? WHERE id = ?',
|
|
89
|
+
[hash, copyId]
|
|
90
|
+
);
|
|
91
|
+
return { success: true, changes: result.changes };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { success: false, error: error.message };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async getDuplicateGroups(options = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const sql = `
|
|
103
|
+
SELECT
|
|
104
|
+
file_hash,
|
|
105
|
+
file_size,
|
|
106
|
+
COUNT(id) as duplicate_count,
|
|
107
|
+
GROUP_CONCAT(file_path, ';') as file_paths
|
|
108
|
+
FROM copies
|
|
109
|
+
WHERE file_hash IS NOT NULL AND active = 1
|
|
110
|
+
GROUP BY file_hash
|
|
111
|
+
HAVING COUNT(id) > 1
|
|
112
|
+
ORDER BY COUNT(id) DESC, file_size DESC
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const results = await this.db.query(sql);
|
|
116
|
+
return results;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`Failed to get duplicate groups: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async getStatistics() {
|
|
124
|
+
try {
|
|
125
|
+
const [metaCount, copiesCount, unhashedCount, duplicateGroups, uniqueFiles] = await Promise.all([
|
|
126
|
+
this.db.queryOne('SELECT COUNT(*) as count FROM meta'),
|
|
127
|
+
this.db.queryOne('SELECT COUNT(*) as count FROM copies'),
|
|
128
|
+
this.db.queryOne('SELECT COUNT(*) as count FROM copies WHERE file_hash IS NULL'),
|
|
129
|
+
this.db.queryOne(`
|
|
130
|
+
SELECT COUNT(*) as count FROM (
|
|
131
|
+
SELECT file_hash FROM copies
|
|
132
|
+
WHERE file_hash IS NOT NULL
|
|
133
|
+
GROUP BY file_hash
|
|
134
|
+
HAVING COUNT(*) > 1
|
|
135
|
+
)
|
|
136
|
+
`),
|
|
137
|
+
this.db.queryOne('SELECT COUNT(DISTINCT file_size) as count FROM copies WHERE file_hash IS NULL')
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const totalSize = await this.db.queryOne(`
|
|
141
|
+
SELECT SUM(file_size) as total_size FROM copies
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
const duplicateSize = await this.db.queryOne(`
|
|
145
|
+
SELECT SUM(file_size * (count - 1)) as duplicate_size FROM (
|
|
146
|
+
SELECT file_hash, file_size, COUNT(*) as count FROM copies
|
|
147
|
+
WHERE file_hash IS NOT NULL
|
|
148
|
+
GROUP BY file_hash
|
|
149
|
+
HAVING COUNT(*) > 1
|
|
150
|
+
)
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
meta: metaCount.count,
|
|
155
|
+
copies: copiesCount.count,
|
|
156
|
+
unhashed: unhashedCount.count,
|
|
157
|
+
uniqueFiles: uniqueFiles.count,
|
|
158
|
+
duplicateGroups: duplicateGroups.count,
|
|
159
|
+
totalSize: totalSize.total_size || 0,
|
|
160
|
+
duplicateSize: duplicateSize.duplicate_size || 0,
|
|
161
|
+
spaceSavings: duplicateSize.duplicate_size || 0
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error(`Failed to get statistics: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async getAllCopies() {
|
|
169
|
+
try {
|
|
170
|
+
const results = await this.db.query('SELECT * FROM copies ORDER BY file_path');
|
|
171
|
+
return results;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(`Failed to get all copies: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async getExistingFilePaths() {
|
|
178
|
+
try {
|
|
179
|
+
const results = await this.db.query('SELECT file_path FROM copies');
|
|
180
|
+
return new Set(results.map(row => row.file_path));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Failed to get existing file paths: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async setAllActive(active) {
|
|
188
|
+
try {
|
|
189
|
+
const result = await this.db.run(
|
|
190
|
+
'UPDATE copies SET active = ?',
|
|
191
|
+
[active]
|
|
192
|
+
);
|
|
193
|
+
return { success: true, changes: result.changes };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return { success: false, error: error.message };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async setActiveByPath(filePath, active) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await this.db.run(
|
|
202
|
+
'UPDATE copies SET active = ? WHERE file_path = ?',
|
|
203
|
+
[active, filePath]
|
|
204
|
+
);
|
|
205
|
+
return { success: true, changes: result.changes };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return { success: false, error: error.message };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async getActiveFiles() {
|
|
213
|
+
try {
|
|
214
|
+
const results = await this.db.query('SELECT * FROM copies WHERE active = 1 ORDER BY file_path');
|
|
215
|
+
return results;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
throw new Error(`Failed to get active files: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Step 1: Get files with duplicates based on file size, excluding inactive and already-hashed files
|
|
223
|
+
* @returns {Promise<Array>} Array of file size groups with potential duplicates
|
|
224
|
+
*/
|
|
225
|
+
async getFilesWithSizeDuplicates() {
|
|
226
|
+
try {
|
|
227
|
+
const sql = `
|
|
228
|
+
SELECT
|
|
229
|
+
file_size,
|
|
230
|
+
COUNT(id) as count,
|
|
231
|
+
GROUP_CONCAT(id, ',') as file_ids,
|
|
232
|
+
GROUP_CONCAT(file_path, '|') as file_paths
|
|
233
|
+
FROM copies
|
|
234
|
+
WHERE active = 1 AND file_hash IS NULL
|
|
235
|
+
GROUP BY file_size
|
|
236
|
+
HAVING COUNT(id) > 1
|
|
237
|
+
ORDER BY file_size DESC
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const results = await this.db.query(sql);
|
|
241
|
+
return results.map(row => ({
|
|
242
|
+
size: row.file_size,
|
|
243
|
+
count: parseInt(row.count),
|
|
244
|
+
fileIds: row.file_ids ? row.file_ids.split(',').map(id => parseInt(id)) : [],
|
|
245
|
+
filePaths: row.file_paths ? row.file_paths.split('|') : []
|
|
246
|
+
}));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(`Failed to get files with size duplicates: ${error.message}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Step 2: Get active files grouped by hash to find actual duplicates
|
|
254
|
+
* @returns {Promise<Array>} Array of hash groups with duplicates
|
|
255
|
+
*/
|
|
256
|
+
async getActiveFilesGroupedByHash() {
|
|
257
|
+
try {
|
|
258
|
+
const sql = `
|
|
259
|
+
SELECT
|
|
260
|
+
file_hash,
|
|
261
|
+
file_size,
|
|
262
|
+
COUNT(id) as count,
|
|
263
|
+
GROUP_CONCAT(id, ',') as file_ids,
|
|
264
|
+
GROUP_CONCAT(file_path, '|') as file_paths
|
|
265
|
+
FROM copies
|
|
266
|
+
WHERE active = 1 AND file_hash IS NOT NULL
|
|
267
|
+
GROUP BY file_hash
|
|
268
|
+
HAVING COUNT(id) > 1
|
|
269
|
+
ORDER BY file_size DESC, COUNT(id) DESC
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
const results = await this.db.query(sql);
|
|
273
|
+
return results.map(row => ({
|
|
274
|
+
hash: row.file_hash,
|
|
275
|
+
size: row.file_size,
|
|
276
|
+
count: parseInt(row.count),
|
|
277
|
+
fileIds: row.file_ids ? row.file_ids.split(',').map(id => parseInt(id)) : [],
|
|
278
|
+
filePaths: row.file_paths ? row.file_paths.split('|') : []
|
|
279
|
+
}));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new Error(`Failed to get active files grouped by hash: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get statistics for resume process
|
|
287
|
+
* @returns {Promise<Object>} Resume statistics
|
|
288
|
+
*/
|
|
289
|
+
async getResumeStatistics() {
|
|
290
|
+
try {
|
|
291
|
+
const [totalFiles, activeFiles, unhashedActiveFiles, sizeDuplicates, hashDuplicates] = await Promise.all([
|
|
292
|
+
this.db.queryOne('SELECT COUNT(*) as count FROM copies'),
|
|
293
|
+
this.db.queryOne('SELECT COUNT(*) as count FROM copies WHERE active = 1'),
|
|
294
|
+
this.db.queryOne('SELECT COUNT(*) as count FROM copies WHERE active = 1 AND file_hash IS NULL'),
|
|
295
|
+
this.db.queryOne(`
|
|
296
|
+
SELECT COUNT(*) as count FROM (
|
|
297
|
+
SELECT file_size FROM copies
|
|
298
|
+
WHERE active = 1 AND file_hash IS NULL
|
|
299
|
+
GROUP BY file_size
|
|
300
|
+
HAVING COUNT(*) > 1
|
|
301
|
+
)
|
|
302
|
+
`),
|
|
303
|
+
this.db.queryOne(`
|
|
304
|
+
SELECT COUNT(*) as count FROM (
|
|
305
|
+
SELECT file_hash FROM copies
|
|
306
|
+
WHERE active = 1 AND file_hash IS NOT NULL
|
|
307
|
+
GROUP BY file_hash
|
|
308
|
+
HAVING COUNT(*) > 1
|
|
309
|
+
)
|
|
310
|
+
`)
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
totalFiles: totalFiles.count,
|
|
315
|
+
activeFiles: activeFiles.count,
|
|
316
|
+
unhashedActiveFiles: unhashedActiveFiles.count,
|
|
317
|
+
sizeDuplicateGroups: sizeDuplicates.count,
|
|
318
|
+
hashDuplicateGroups: hashDuplicates.count
|
|
319
|
+
};
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw new Error(`Failed to get resume statistics: ${error.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createOperations(dbConnection) {
|
|
329
|
+
return new DatabaseOperations(dbConnection);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export { DatabaseOperations, createOperations };
|