filemayor 2.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/core/categories.js +235 -0
- package/core/cleaner.js +527 -0
- package/core/config.js +562 -0
- package/core/index.js +79 -0
- package/core/organizer.js +528 -0
- package/core/reporter.js +572 -0
- package/core/scanner.js +436 -0
- package/core/security.js +317 -0
- package/core/sop-parser.js +565 -0
- package/core/watcher.js +478 -0
- package/index.js +536 -0
- package/package.json +55 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* FILEMAYOR CORE — ORGANIZER
|
|
6
|
+
* Intelligent file organization with dry-run, rollback journal,
|
|
7
|
+
* naming conventions, duplicate detection, and batch operations.
|
|
8
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { categorize, getCategories } = require('./categories');
|
|
16
|
+
const { scan } = require('./scanner');
|
|
17
|
+
const { validatePath, isFileSafe, sanitizeFilename, canWrite, createSnapshot } = require('./security');
|
|
18
|
+
const { formatBytes } = require('./scanner');
|
|
19
|
+
|
|
20
|
+
// ─── Naming Conventions ───────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const NAMING_CONVENTIONS = {
|
|
23
|
+
/**
|
|
24
|
+
* Category prefix: BIZ_001_Document.pdf
|
|
25
|
+
*/
|
|
26
|
+
category_prefix: (file, category, index) => {
|
|
27
|
+
const prefix = category.toUpperCase().slice(0, 4);
|
|
28
|
+
const padded = String(index + 1).padStart(3, '0');
|
|
29
|
+
const cleanName = cleanFilename(file.name);
|
|
30
|
+
return `${prefix}_${padded}_${cleanName}`;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Date prefix: 2024-01-15_Document.pdf
|
|
35
|
+
*/
|
|
36
|
+
date_prefix: (file) => {
|
|
37
|
+
const date = new Date(file.modified);
|
|
38
|
+
const year = date.getFullYear();
|
|
39
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
40
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
41
|
+
const cleanName = cleanFilename(file.name);
|
|
42
|
+
return `${year}-${month}-${day}_${cleanName}`;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clean title only: Document.pdf (just sanitize)
|
|
47
|
+
*/
|
|
48
|
+
clean: (file) => {
|
|
49
|
+
return cleanFilename(file.name);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keep original: no renaming
|
|
54
|
+
*/
|
|
55
|
+
original: (file) => {
|
|
56
|
+
return file.name;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clean a filename by removing junk patterns
|
|
62
|
+
* @param {string} filename - Original filename
|
|
63
|
+
* @returns {string} Cleaned filename
|
|
64
|
+
*/
|
|
65
|
+
function cleanFilename(filename) {
|
|
66
|
+
const ext = path.extname(filename);
|
|
67
|
+
let base = path.basename(filename, ext);
|
|
68
|
+
|
|
69
|
+
// Remove common junk patterns
|
|
70
|
+
base = base
|
|
71
|
+
.replace(/[\[\(]\s*\d+\s*[\]\)]/g, '') // [1], (2)
|
|
72
|
+
.replace(/\s*-\s*Copy\s*(\(\d+\))?/gi, '') // - Copy, - Copy (2)
|
|
73
|
+
.replace(/\s*\(\d+\)\s*$/g, '') // (1) at end
|
|
74
|
+
.replace(/^\d{10,}_/g, '') // Unix timestamp prefix
|
|
75
|
+
.replace(/^IMG_\d+/gi, 'Photo') // IMG_20240115
|
|
76
|
+
.replace(/^DSC_?\d+/gi, 'Photo') // DSC_0001
|
|
77
|
+
.replace(/^VID_?\d+/gi, 'Video') // VID_20240115
|
|
78
|
+
.replace(/^Screenshot[_ ]\d{4}[-_]\d{2}[-_]\d{2}/gi, 'Screenshot') // Screenshot timestamps
|
|
79
|
+
.replace(/_{2,}/g, '_') // Multiple underscores
|
|
80
|
+
.replace(/\s{2,}/g, ' ') // Multiple spaces
|
|
81
|
+
.replace(/^[_\s]+|[_\s]+$/g, ''); // Trim
|
|
82
|
+
|
|
83
|
+
// Convert to Title Case if all lowercase or all uppercase
|
|
84
|
+
if (base === base.toLowerCase() || base === base.toUpperCase()) {
|
|
85
|
+
base = base.replace(/\b\w/g, c => c.toUpperCase());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return sanitizeFilename(`${base}${ext}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Duplicate Detection ──────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const DUPLICATE_STRATEGIES = {
|
|
94
|
+
skip: 'skip', // Don't move if destination exists
|
|
95
|
+
rename: 'rename', // Add (1), (2), etc.
|
|
96
|
+
overwrite: 'overwrite', // Replace existing
|
|
97
|
+
newest: 'newest', // Keep the newest file
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a destination path with duplicate handling
|
|
102
|
+
* @param {string} destPath - Target destination path
|
|
103
|
+
* @param {string} strategy - Duplicate resolution strategy
|
|
104
|
+
* @returns {{ path: string, action: string }}
|
|
105
|
+
*/
|
|
106
|
+
function resolveDuplicate(destPath, strategy = 'rename') {
|
|
107
|
+
if (!fs.existsSync(destPath)) {
|
|
108
|
+
return { path: destPath, action: 'move' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
switch (strategy) {
|
|
112
|
+
case 'skip':
|
|
113
|
+
return { path: destPath, action: 'skip' };
|
|
114
|
+
|
|
115
|
+
case 'overwrite':
|
|
116
|
+
return { path: destPath, action: 'overwrite' };
|
|
117
|
+
|
|
118
|
+
case 'rename':
|
|
119
|
+
default: {
|
|
120
|
+
const dir = path.dirname(destPath);
|
|
121
|
+
const ext = path.extname(destPath);
|
|
122
|
+
const base = path.basename(destPath, ext);
|
|
123
|
+
let counter = 1;
|
|
124
|
+
let newPath;
|
|
125
|
+
|
|
126
|
+
do {
|
|
127
|
+
newPath = path.join(dir, `${base} (${counter})${ext}`);
|
|
128
|
+
counter++;
|
|
129
|
+
} while (fs.existsSync(newPath) && counter < 1000);
|
|
130
|
+
|
|
131
|
+
return { path: newPath, action: 'rename' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Move Plan ────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @typedef {Object} MovePlanItem
|
|
140
|
+
* @property {string} source - Source file path
|
|
141
|
+
* @property {string} destination - Target destination path
|
|
142
|
+
* @property {string} category - File category
|
|
143
|
+
* @property {string} originalName - Original filename
|
|
144
|
+
* @property {string} newName - New filename (after convention applied)
|
|
145
|
+
* @property {number} size - File size in bytes
|
|
146
|
+
* @property {string} action - Action to take: move, skip, rename, overwrite
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate a move plan without executing (dry-run)
|
|
151
|
+
* @param {string} srcDir - Source directory to organize
|
|
152
|
+
* @param {Object} options - Organization options
|
|
153
|
+
* @returns {{ plan: MovePlanItem[], summary: Object }}
|
|
154
|
+
*/
|
|
155
|
+
function generatePlan(srcDir, options = {}) {
|
|
156
|
+
const {
|
|
157
|
+
naming = 'original',
|
|
158
|
+
outputDir = null, // null = organize in-place (subdirs)
|
|
159
|
+
flat = false, // Flat structure vs nested
|
|
160
|
+
duplicateStrategy = 'rename',
|
|
161
|
+
customCategories = null,
|
|
162
|
+
scanOptions = {},
|
|
163
|
+
} = options;
|
|
164
|
+
|
|
165
|
+
// Scan the directory
|
|
166
|
+
const scanResult = scan(srcDir, {
|
|
167
|
+
...scanOptions,
|
|
168
|
+
includeDirectories: false
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const categories = getCategories(customCategories);
|
|
172
|
+
const plan = [];
|
|
173
|
+
const categoryCounts = {};
|
|
174
|
+
const namingFn = NAMING_CONVENTIONS[naming] || NAMING_CONVENTIONS.original;
|
|
175
|
+
|
|
176
|
+
// Build the plan
|
|
177
|
+
for (const file of scanResult.files) {
|
|
178
|
+
const category = file.category;
|
|
179
|
+
categoryCounts[category] = (categoryCounts[category] || 0);
|
|
180
|
+
|
|
181
|
+
// Determine new filename
|
|
182
|
+
const newName = namingFn(file, category, categoryCounts[category]);
|
|
183
|
+
categoryCounts[category]++;
|
|
184
|
+
|
|
185
|
+
// Determine destination directory
|
|
186
|
+
const catDef = categories[category];
|
|
187
|
+
const catLabel = catDef ? catDef.label : 'Other';
|
|
188
|
+
const destDir = outputDir
|
|
189
|
+
? path.join(outputDir, catLabel)
|
|
190
|
+
: path.join(srcDir, catLabel);
|
|
191
|
+
const destPath = path.join(destDir, newName);
|
|
192
|
+
|
|
193
|
+
// Skip if source and destination are the same
|
|
194
|
+
if (path.resolve(file.path) === path.resolve(destPath)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Handle duplicates
|
|
199
|
+
const resolved = resolveDuplicate(destPath, duplicateStrategy);
|
|
200
|
+
|
|
201
|
+
plan.push({
|
|
202
|
+
source: file.path,
|
|
203
|
+
destination: resolved.path,
|
|
204
|
+
category,
|
|
205
|
+
categoryLabel: catLabel,
|
|
206
|
+
originalName: file.name,
|
|
207
|
+
newName: path.basename(resolved.path),
|
|
208
|
+
size: file.size,
|
|
209
|
+
sizeHuman: file.sizeHuman,
|
|
210
|
+
action: resolved.action,
|
|
211
|
+
ext: file.ext
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Build summary
|
|
216
|
+
const summary = {
|
|
217
|
+
totalFiles: plan.length,
|
|
218
|
+
totalSize: plan.reduce((sum, p) => sum + p.size, 0),
|
|
219
|
+
totalSizeHuman: formatBytes(plan.reduce((sum, p) => sum + p.size, 0)),
|
|
220
|
+
categories: {},
|
|
221
|
+
actions: { move: 0, skip: 0, rename: 0, overwrite: 0 },
|
|
222
|
+
scanStats: scanResult.stats
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
for (const item of plan) {
|
|
226
|
+
summary.actions[item.action]++;
|
|
227
|
+
if (!summary.categories[item.categoryLabel]) {
|
|
228
|
+
summary.categories[item.categoryLabel] = { count: 0, size: 0 };
|
|
229
|
+
}
|
|
230
|
+
summary.categories[item.categoryLabel].count++;
|
|
231
|
+
summary.categories[item.categoryLabel].size += item.size;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Add human-readable size to each category
|
|
235
|
+
for (const cat of Object.values(summary.categories)) {
|
|
236
|
+
cat.sizeHuman = formatBytes(cat.size);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { plan, summary };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Execution ────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Execute a move plan with journal logging for rollback
|
|
246
|
+
* @param {MovePlanItem[]} plan - The move plan to execute
|
|
247
|
+
* @param {Object} options - Execution options
|
|
248
|
+
* @returns {{ results: Object[], journal: Object[], summary: Object }}
|
|
249
|
+
*/
|
|
250
|
+
function executePlan(plan, options = {}) {
|
|
251
|
+
const {
|
|
252
|
+
onProgress = null,
|
|
253
|
+
onError = null,
|
|
254
|
+
journalPath = null, // Path to save rollback journal
|
|
255
|
+
abortOnError = false, // Stop on first error
|
|
256
|
+
} = options;
|
|
257
|
+
|
|
258
|
+
const results = [];
|
|
259
|
+
const journal = [];
|
|
260
|
+
let succeeded = 0;
|
|
261
|
+
let failed = 0;
|
|
262
|
+
let skipped = 0;
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < plan.length; i++) {
|
|
265
|
+
const item = plan[i];
|
|
266
|
+
|
|
267
|
+
// Progress callback
|
|
268
|
+
if (onProgress) {
|
|
269
|
+
onProgress({
|
|
270
|
+
current: i + 1,
|
|
271
|
+
total: plan.length,
|
|
272
|
+
percent: Math.round(((i + 1) / plan.length) * 100),
|
|
273
|
+
file: item.originalName,
|
|
274
|
+
action: item.action,
|
|
275
|
+
category: item.categoryLabel
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Skip items marked as skip
|
|
280
|
+
if (item.action === 'skip') {
|
|
281
|
+
results.push({ ...item, status: 'skipped', error: null });
|
|
282
|
+
skipped++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Safety check
|
|
287
|
+
const safeCheck = isFileSafe(item.source);
|
|
288
|
+
if (!safeCheck.safe) {
|
|
289
|
+
const error = `Safety check failed: ${safeCheck.reason}`;
|
|
290
|
+
results.push({ ...item, status: 'error', error });
|
|
291
|
+
failed++;
|
|
292
|
+
if (onError) onError({ item, error });
|
|
293
|
+
if (abortOnError) break;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Create destination directory
|
|
298
|
+
const destDir = path.dirname(item.destination);
|
|
299
|
+
try {
|
|
300
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
const error = `Failed to create directory: ${err.message}`;
|
|
303
|
+
results.push({ ...item, status: 'error', error });
|
|
304
|
+
failed++;
|
|
305
|
+
if (onError) onError({ item, error });
|
|
306
|
+
if (abortOnError) break;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Create pre-move snapshot for integrity
|
|
311
|
+
const snapshot = createSnapshot(item.source);
|
|
312
|
+
|
|
313
|
+
// Execute the move
|
|
314
|
+
try {
|
|
315
|
+
// Handle overwrite
|
|
316
|
+
if (item.action === 'overwrite' && fs.existsSync(item.destination)) {
|
|
317
|
+
fs.unlinkSync(item.destination);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Perform the move
|
|
321
|
+
fs.renameSync(item.source, item.destination);
|
|
322
|
+
|
|
323
|
+
// Journal entry for rollback
|
|
324
|
+
const journalEntry = {
|
|
325
|
+
source: item.source,
|
|
326
|
+
destination: item.destination,
|
|
327
|
+
originalName: item.originalName,
|
|
328
|
+
newName: item.newName,
|
|
329
|
+
size: item.size,
|
|
330
|
+
timestamp: Date.now(),
|
|
331
|
+
snapshot
|
|
332
|
+
};
|
|
333
|
+
journal.push(journalEntry);
|
|
334
|
+
|
|
335
|
+
results.push({ ...item, status: 'success', error: null });
|
|
336
|
+
succeeded++;
|
|
337
|
+
} catch (err) {
|
|
338
|
+
// If rename fails (cross-device), try copy + delete
|
|
339
|
+
try {
|
|
340
|
+
fs.copyFileSync(item.source, item.destination);
|
|
341
|
+
fs.unlinkSync(item.source);
|
|
342
|
+
|
|
343
|
+
const journalEntry = {
|
|
344
|
+
source: item.source,
|
|
345
|
+
destination: item.destination,
|
|
346
|
+
originalName: item.originalName,
|
|
347
|
+
newName: item.newName,
|
|
348
|
+
size: item.size,
|
|
349
|
+
timestamp: Date.now(),
|
|
350
|
+
snapshot,
|
|
351
|
+
crossDevice: true
|
|
352
|
+
};
|
|
353
|
+
journal.push(journalEntry);
|
|
354
|
+
|
|
355
|
+
results.push({ ...item, status: 'success', error: null });
|
|
356
|
+
succeeded++;
|
|
357
|
+
} catch (copyErr) {
|
|
358
|
+
const error = `Move failed: ${copyErr.message}`;
|
|
359
|
+
results.push({ ...item, status: 'error', error });
|
|
360
|
+
failed++;
|
|
361
|
+
if (onError) onError({ item, error });
|
|
362
|
+
if (abortOnError) break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Save journal to disk if path provided
|
|
368
|
+
if (journalPath && journal.length > 0) {
|
|
369
|
+
try {
|
|
370
|
+
const existing = fs.existsSync(journalPath)
|
|
371
|
+
? JSON.parse(fs.readFileSync(journalPath, 'utf8'))
|
|
372
|
+
: [];
|
|
373
|
+
const merged = [...existing, ...journal];
|
|
374
|
+
fs.writeFileSync(journalPath, JSON.stringify(merged, null, 2), 'utf8');
|
|
375
|
+
} catch (err) {
|
|
376
|
+
// Journal save failed — non-fatal
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const summary = {
|
|
381
|
+
total: plan.length,
|
|
382
|
+
succeeded,
|
|
383
|
+
failed,
|
|
384
|
+
skipped,
|
|
385
|
+
totalSize: plan.reduce((sum, p) => sum + p.size, 0),
|
|
386
|
+
totalSizeHuman: formatBytes(plan.reduce((sum, p) => sum + p.size, 0))
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return { results, journal, summary };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Rollback (Undo) ──────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Rollback operations using a journal
|
|
396
|
+
* @param {Object[]} journal - Journal entries from executePlan
|
|
397
|
+
* @param {Object} options - Rollback options
|
|
398
|
+
* @returns {{ undone: number, failed: number, errors: string[] }}
|
|
399
|
+
*/
|
|
400
|
+
function rollback(journal, options = {}) {
|
|
401
|
+
const { onProgress = null } = options;
|
|
402
|
+
let undone = 0;
|
|
403
|
+
let failed = 0;
|
|
404
|
+
const errors = [];
|
|
405
|
+
|
|
406
|
+
// Process in reverse order (most recent first)
|
|
407
|
+
const reversed = [...journal].reverse();
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < reversed.length; i++) {
|
|
410
|
+
const entry = reversed[i];
|
|
411
|
+
|
|
412
|
+
if (onProgress) {
|
|
413
|
+
onProgress({
|
|
414
|
+
current: i + 1,
|
|
415
|
+
total: reversed.length,
|
|
416
|
+
percent: Math.round(((i + 1) / reversed.length) * 100),
|
|
417
|
+
file: entry.originalName
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
// Ensure source directory exists
|
|
423
|
+
const sourceDir = path.dirname(entry.source);
|
|
424
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
425
|
+
|
|
426
|
+
// Move back
|
|
427
|
+
if (entry.crossDevice) {
|
|
428
|
+
fs.copyFileSync(entry.destination, entry.source);
|
|
429
|
+
fs.unlinkSync(entry.destination);
|
|
430
|
+
} else {
|
|
431
|
+
fs.renameSync(entry.destination, entry.source);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
undone++;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
failed++;
|
|
437
|
+
errors.push(`Failed to undo ${entry.originalName}: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { undone, failed, errors };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Load a journal from disk
|
|
446
|
+
* @param {string} journalPath - Path to journal file
|
|
447
|
+
* @returns {Object[]} Journal entries
|
|
448
|
+
*/
|
|
449
|
+
function loadJournal(journalPath) {
|
|
450
|
+
try {
|
|
451
|
+
if (!fs.existsSync(journalPath)) return [];
|
|
452
|
+
return JSON.parse(fs.readFileSync(journalPath, 'utf8'));
|
|
453
|
+
} catch {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ─── Convenience Functions ────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Organize a directory (scan, plan, execute) in one call
|
|
462
|
+
* @param {string} dirPath - Directory to organize
|
|
463
|
+
* @param {Object} options - Full options
|
|
464
|
+
* @returns {{ results: Object[], journal: Object[], planSummary: Object, execSummary: Object }}
|
|
465
|
+
*/
|
|
466
|
+
function organize(dirPath, options = {}) {
|
|
467
|
+
const {
|
|
468
|
+
dryRun = false,
|
|
469
|
+
naming = 'original',
|
|
470
|
+
outputDir = null,
|
|
471
|
+
duplicateStrategy = 'rename',
|
|
472
|
+
journalPath = null,
|
|
473
|
+
onProgress = null,
|
|
474
|
+
onError = null,
|
|
475
|
+
abortOnError = false,
|
|
476
|
+
scanOptions = {},
|
|
477
|
+
customCategories = null,
|
|
478
|
+
} = options;
|
|
479
|
+
|
|
480
|
+
// Generate plan
|
|
481
|
+
const { plan, summary: planSummary } = generatePlan(dirPath, {
|
|
482
|
+
naming,
|
|
483
|
+
outputDir,
|
|
484
|
+
duplicateStrategy,
|
|
485
|
+
scanOptions,
|
|
486
|
+
customCategories,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
if (dryRun) {
|
|
490
|
+
return {
|
|
491
|
+
dryRun: true,
|
|
492
|
+
plan,
|
|
493
|
+
planSummary,
|
|
494
|
+
results: [],
|
|
495
|
+
journal: [],
|
|
496
|
+
execSummary: null
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Execute
|
|
501
|
+
const { results, journal, summary: execSummary } = executePlan(plan, {
|
|
502
|
+
onProgress,
|
|
503
|
+
onError,
|
|
504
|
+
journalPath: journalPath || path.join(dirPath, '.filemayor-journal.json'),
|
|
505
|
+
abortOnError
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
dryRun: false,
|
|
510
|
+
plan,
|
|
511
|
+
planSummary,
|
|
512
|
+
results,
|
|
513
|
+
journal,
|
|
514
|
+
execSummary
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
module.exports = {
|
|
519
|
+
NAMING_CONVENTIONS,
|
|
520
|
+
DUPLICATE_STRATEGIES,
|
|
521
|
+
cleanFilename,
|
|
522
|
+
resolveDuplicate,
|
|
523
|
+
generatePlan,
|
|
524
|
+
executePlan,
|
|
525
|
+
rollback,
|
|
526
|
+
loadJournal,
|
|
527
|
+
organize
|
|
528
|
+
};
|