filemayor 3.6.0 → 4.0.7

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/analyzer.js CHANGED
@@ -1,163 +1,163 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * ═══════════════════════════════════════════════════════════════════
5
- * FILEMAYOR CORE — ANALYZER
6
- * Deep filesystem insights: duplicate detection, bloat mapping,
7
- * and potential savings calculation.
8
- * ═══════════════════════════════════════════════════════════════════
9
- */
10
-
11
- 'use strict';
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const crypto = require('crypto');
16
- const { scan } = require('./scanner');
17
- const { findJunk } = require('./cleaner');
18
- const { formatBytes } = require('./scanner');
19
-
20
- /**
21
- * Analyze a directory for duplicates, bloat, and junk
22
- * @param {string} dirPath - Root directory to analyze
23
- * @param {Object} options - Analysis options
24
- * @returns {Object} Deep analysis results
25
- */
26
- function analyzeDirectory(dirPath, options = {}) {
27
- const {
28
- maxDepth = 10,
29
- minSize = 1024, // Ignore files smaller than 1KB for duplicate analysis
30
- } = options;
31
-
32
- // 1. Initial Scan
33
- const scanResult = scan(dirPath, { maxDepth, includeDirectories: true });
34
- const { files, stats } = scanResult;
35
-
36
- // 2. Duplicate Detection (Size-first hashing)
37
- const sizeMap = new Map();
38
- const duplicates = [];
39
-
40
- // Group by size first (fast)
41
- for (const file of files) {
42
- if (file.size < minSize) continue;
43
- if (!sizeMap.has(file.size)) {
44
- sizeMap.set(file.size, []);
45
- }
46
- sizeMap.get(file.size).push(file);
47
- }
48
-
49
- // Hash only those with identical sizes
50
- for (const [size, candidateFiles] of sizeMap.entries()) {
51
- if (candidateFiles.length < 2) continue;
52
-
53
- const hashMap = new Map();
54
- for (const file of candidateFiles) {
55
- try {
56
- // Partial hash (first 16KB) for efficiency
57
- const hash = getFileHash(file.path, 16384);
58
- if (!hashMap.has(hash)) {
59
- hashMap.set(hash, []);
60
- }
61
- hashMap.get(hash).push(file);
62
- } catch (err) {
63
- // Skip files we can't read
64
- }
65
- }
66
-
67
- for (const [hash, dupeFiles] of hashMap.entries()) {
68
- if (dupeFiles.length > 1) {
69
- duplicates.push({
70
- size,
71
- sizeHuman: formatBytes(size),
72
- files: dupeFiles.map(f => ({ name: f.name, path: f.path, relativePath: f.relativePath })),
73
- wastedSpace: size * (dupeFiles.length - 1),
74
- wastedSpaceHuman: formatBytes(size * (dupeFiles.length - 1))
75
- });
76
- }
77
- }
78
- }
79
- // 3. Bloat Mapping (Top Directories) & Progress Reporting
80
- const dirMap = new Map();
81
- for (let i = 0; i < files.length; i++) {
82
- const file = files[i];
83
-
84
- // Progress callback
85
- if (options.onProgress) {
86
- options.onProgress({
87
- current: i + 1,
88
- total: files.length,
89
- percent: Math.round(((i + 1) / files.length) * 100),
90
- file: file.name
91
- });
92
- }
93
-
94
- const parts = file.relativePath.split(path.sep);
95
- let currentPath = '';
96
- // Only track top-level and second-level folders for bloat mapping
97
- for (let j = 0; j < Math.min(parts.length - 1, 2); j++) {
98
- currentPath = currentPath ? path.join(currentPath, parts[j]) : parts[j];
99
- if (!dirMap.has(currentPath)) {
100
- dirMap.set(currentPath, { size: 0, count: 0 });
101
- }
102
- const info = dirMap.get(currentPath);
103
- info.size += file.size;
104
- info.count += 1;
105
- }
106
- }
107
-
108
- const largestDirs = Array.from(dirMap.entries())
109
- .map(([name, info]) => ({ name, ...info, sizeHuman: formatBytes(info.size) }))
110
- .sort((a, b) => b.size - a.size)
111
- .slice(0, 5);
112
-
113
- // 4. Junk Detection Integration
114
- const junkResult = findJunk(dirPath, { maxDepth });
115
- const totalJunkSize = junkResult.stats.totalSize;
116
- const totalDuplicateWasted = duplicates.reduce((sum, d) => sum + d.wastedSpace, 0);
117
-
118
- return {
119
- root: dirPath,
120
- timestamp: Date.now(),
121
- summary: {
122
- totalFiles: stats.filesFound,
123
- totalSize: stats.totalSize,
124
- totalSizeHuman: formatBytes(stats.totalSize),
125
- potentialSavings: totalJunkSize + totalDuplicateWasted,
126
- potentialSavingsHuman: formatBytes(totalJunkSize + totalDuplicateWasted)
127
- },
128
- duplicates: {
129
- sets: duplicates.length,
130
- totalWasted: totalDuplicateWasted,
131
- totalWastedHuman: formatBytes(totalDuplicateWasted),
132
- details: duplicates.sort((a, b) => b.wastedSpace - a.wastedSpace).slice(0, 10)
133
- },
134
- largestDirs,
135
- junk: {
136
- count: junkResult.junk.length,
137
- totalSize: totalJunkSize,
138
- totalSizeHuman: formatBytes(totalJunkSize),
139
- categories: junkResult.stats.byCategory
140
- }
141
- };
142
- }
143
-
144
- /**
145
- * Get hash of file content (partial or full)
146
- */
147
- function getFileHash(filePath, limit = null) {
148
- const buffer = limit
149
- ? Buffer.alloc(limit)
150
- : fs.readFileSync(filePath);
151
-
152
- if (limit) {
153
- const fd = fs.openSync(filePath, 'r');
154
- fs.readSync(fd, buffer, 0, limit, 0);
155
- fs.closeSync(fd);
156
- }
157
-
158
- return crypto.createHash('md5').update(buffer).digest('hex');
159
- }
160
-
161
- module.exports = {
162
- analyzeDirectory
163
- };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — ANALYZER
6
+ * Deep filesystem insights: duplicate detection, bloat mapping,
7
+ * and potential savings calculation.
8
+ * ═══════════════════════════════════════════════════════════════════
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const crypto = require('crypto');
16
+ const { scan } = require('./scanner');
17
+ const { findJunk } = require('./cleaner');
18
+ const { formatBytes } = require('./scanner');
19
+
20
+ /**
21
+ * Analyze a directory for duplicates, bloat, and junk
22
+ * @param {string} dirPath - Root directory to analyze
23
+ * @param {Object} options - Analysis options
24
+ * @returns {Object} Deep analysis results
25
+ */
26
+ function analyzeDirectory(dirPath, options = {}) {
27
+ const {
28
+ maxDepth = 10,
29
+ minSize = 1024, // Ignore files smaller than 1KB for duplicate analysis
30
+ } = options;
31
+
32
+ // 1. Initial Scan
33
+ const scanResult = scan(dirPath, { maxDepth, includeDirectories: true });
34
+ const { files, stats } = scanResult;
35
+
36
+ // 2. Duplicate Detection (Size-first hashing)
37
+ const sizeMap = new Map();
38
+ const duplicates = [];
39
+
40
+ // Group by size first (fast)
41
+ for (const file of files) {
42
+ if (file.size < minSize) continue;
43
+ if (!sizeMap.has(file.size)) {
44
+ sizeMap.set(file.size, []);
45
+ }
46
+ sizeMap.get(file.size).push(file);
47
+ }
48
+
49
+ // Hash only those with identical sizes
50
+ for (const [size, candidateFiles] of sizeMap.entries()) {
51
+ if (candidateFiles.length < 2) continue;
52
+
53
+ const hashMap = new Map();
54
+ for (const file of candidateFiles) {
55
+ try {
56
+ // Partial hash (first 16KB) for efficiency
57
+ const hash = getFileHash(file.path, 16384);
58
+ if (!hashMap.has(hash)) {
59
+ hashMap.set(hash, []);
60
+ }
61
+ hashMap.get(hash).push(file);
62
+ } catch (err) {
63
+ // Skip files we can't read
64
+ }
65
+ }
66
+
67
+ for (const [hash, dupeFiles] of hashMap.entries()) {
68
+ if (dupeFiles.length > 1) {
69
+ duplicates.push({
70
+ size,
71
+ sizeHuman: formatBytes(size),
72
+ files: dupeFiles.map(f => ({ name: f.name, path: f.path, relativePath: f.relativePath })),
73
+ wastedSpace: size * (dupeFiles.length - 1),
74
+ wastedSpaceHuman: formatBytes(size * (dupeFiles.length - 1))
75
+ });
76
+ }
77
+ }
78
+ }
79
+ // 3. Bloat Mapping (Top Directories) & Progress Reporting
80
+ const dirMap = new Map();
81
+ for (let i = 0; i < files.length; i++) {
82
+ const file = files[i];
83
+
84
+ // Progress callback
85
+ if (options.onProgress) {
86
+ options.onProgress({
87
+ current: i + 1,
88
+ total: files.length,
89
+ percent: Math.round(((i + 1) / files.length) * 100),
90
+ file: file.name
91
+ });
92
+ }
93
+
94
+ const parts = file.relativePath.split(path.sep);
95
+ let currentPath = '';
96
+ // Only track top-level and second-level folders for bloat mapping
97
+ for (let j = 0; j < Math.min(parts.length - 1, 2); j++) {
98
+ currentPath = currentPath ? path.join(currentPath, parts[j]) : parts[j];
99
+ if (!dirMap.has(currentPath)) {
100
+ dirMap.set(currentPath, { size: 0, count: 0 });
101
+ }
102
+ const info = dirMap.get(currentPath);
103
+ info.size += file.size;
104
+ info.count += 1;
105
+ }
106
+ }
107
+
108
+ const largestDirs = Array.from(dirMap.entries())
109
+ .map(([name, info]) => ({ name, ...info, sizeHuman: formatBytes(info.size) }))
110
+ .sort((a, b) => b.size - a.size)
111
+ .slice(0, 5);
112
+
113
+ // 4. Junk Detection Integration
114
+ const junkResult = findJunk(dirPath, { maxDepth });
115
+ const totalJunkSize = junkResult.stats.totalSize;
116
+ const totalDuplicateWasted = duplicates.reduce((sum, d) => sum + d.wastedSpace, 0);
117
+
118
+ return {
119
+ root: dirPath,
120
+ timestamp: Date.now(),
121
+ summary: {
122
+ totalFiles: stats.filesFound,
123
+ totalSize: stats.totalSize,
124
+ totalSizeHuman: formatBytes(stats.totalSize),
125
+ potentialSavings: totalJunkSize + totalDuplicateWasted,
126
+ potentialSavingsHuman: formatBytes(totalJunkSize + totalDuplicateWasted)
127
+ },
128
+ duplicates: {
129
+ sets: duplicates.length,
130
+ totalWasted: totalDuplicateWasted,
131
+ totalWastedHuman: formatBytes(totalDuplicateWasted),
132
+ details: duplicates.sort((a, b) => b.wastedSpace - a.wastedSpace).slice(0, 10)
133
+ },
134
+ largestDirs,
135
+ junk: {
136
+ count: junkResult.junk.length,
137
+ totalSize: totalJunkSize,
138
+ totalSizeHuman: formatBytes(totalJunkSize),
139
+ categories: junkResult.stats.byCategory
140
+ }
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get hash of file content (partial or full)
146
+ */
147
+ function getFileHash(filePath, limit = null) {
148
+ const buffer = limit
149
+ ? Buffer.alloc(limit)
150
+ : fs.readFileSync(filePath);
151
+
152
+ if (limit) {
153
+ const fd = fs.openSync(filePath, 'r');
154
+ fs.readSync(fd, buffer, 0, limit, 0);
155
+ fs.closeSync(fd);
156
+ }
157
+
158
+ return crypto.createHash('md5').update(buffer).digest('hex');
159
+ }
160
+
161
+ module.exports = {
162
+ analyzeDirectory
163
+ };