@fanboynz/network-scanner 1.0.35
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/.github/workflows/npm-publish.yml +33 -0
- package/JSONMANUAL.md +121 -0
- package/LICENSE +674 -0
- package/README.md +357 -0
- package/config.json +74 -0
- package/lib/browserexit.js +522 -0
- package/lib/browserhealth.js +308 -0
- package/lib/cloudflare.js +660 -0
- package/lib/colorize.js +168 -0
- package/lib/compare.js +159 -0
- package/lib/compress.js +129 -0
- package/lib/fingerprint.js +613 -0
- package/lib/flowproxy.js +274 -0
- package/lib/grep.js +348 -0
- package/lib/ignore_similar.js +237 -0
- package/lib/nettools.js +1200 -0
- package/lib/output.js +633 -0
- package/lib/redirect.js +384 -0
- package/lib/searchstring.js +561 -0
- package/lib/validate_rules.js +1107 -0
- package/nwss.1 +824 -0
- package/nwss.js +2488 -0
- package/package.json +45 -0
- package/regex-samples.md +27 -0
- package/scanner-script-org.js +588 -0
package/lib/colorize.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// === Color Utility Module (colorize.js) ===
|
|
2
|
+
// Centralized color management for nwss.js console output
|
|
3
|
+
// Provides consistent theming across all modules
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects if color output should be enabled based on command line arguments
|
|
7
|
+
* @returns {boolean} True if --color or --colour flag is present
|
|
8
|
+
*/
|
|
9
|
+
function shouldEnableColors() {
|
|
10
|
+
return process.argv.includes('--color') || process.argv.includes('--colour');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Initialize color support based on command line flags
|
|
14
|
+
const enableColors = shouldEnableColors();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ANSI color codes object
|
|
18
|
+
* Only contains actual escape sequences if colors are enabled
|
|
19
|
+
*/
|
|
20
|
+
const colors = {
|
|
21
|
+
// Reset and formatting
|
|
22
|
+
reset: enableColors ? '\x1b[0m' : '',
|
|
23
|
+
bright: enableColors ? '\x1b[1m' : '',
|
|
24
|
+
dim: enableColors ? '\x1b[2m' : '',
|
|
25
|
+
|
|
26
|
+
// Standard colors
|
|
27
|
+
red: enableColors ? '\x1b[31m' : '',
|
|
28
|
+
green: enableColors ? '\x1b[32m' : '',
|
|
29
|
+
yellow: enableColors ? '\x1b[33m' : '',
|
|
30
|
+
blue: enableColors ? '\x1b[34m' : '',
|
|
31
|
+
magenta: enableColors ? '\x1b[35m' : '',
|
|
32
|
+
cyan: enableColors ? '\x1b[36m' : '',
|
|
33
|
+
white: enableColors ? '\x1b[37m' : '',
|
|
34
|
+
|
|
35
|
+
// Extended colors
|
|
36
|
+
gray: enableColors ? '\x1b[90m' : '',
|
|
37
|
+
brightRed: enableColors ? '\x1b[91m' : '',
|
|
38
|
+
brightGreen: enableColors ? '\x1b[92m' : '',
|
|
39
|
+
brightYellow: enableColors ? '\x1b[93m' : '',
|
|
40
|
+
brightBlue: enableColors ? '\x1b[94m' : '',
|
|
41
|
+
brightMagenta: enableColors ? '\x1b[95m' : '',
|
|
42
|
+
brightCyan: enableColors ? '\x1b[96m' : '',
|
|
43
|
+
brightWhite: enableColors ? '\x1b[97m' : ''
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Applies color formatting to text if colors are enabled
|
|
48
|
+
* @param {string} text - The text to colorize
|
|
49
|
+
* @param {string} color - The ANSI color code to apply
|
|
50
|
+
* @returns {string} Colored text (or plain text if colors disabled)
|
|
51
|
+
*/
|
|
52
|
+
function colorize(text, color) {
|
|
53
|
+
return enableColors ? `${color}${text}${colors.reset}` : text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pre-built color functions for common message types
|
|
58
|
+
* These provide semantic coloring for different types of console output
|
|
59
|
+
*/
|
|
60
|
+
const messageColors = {
|
|
61
|
+
// Status and logging
|
|
62
|
+
debug: (text) => colorize(text, colors.gray),
|
|
63
|
+
info: (text) => colorize(text, colors.blue),
|
|
64
|
+
warn: (text) => colorize(text, colors.yellow),
|
|
65
|
+
error: (text) => colorize(text, colors.red),
|
|
66
|
+
success: (text) => colorize(text, colors.green),
|
|
67
|
+
|
|
68
|
+
// Process states
|
|
69
|
+
scanning: (text) => colorize(text, colors.yellow),
|
|
70
|
+
loaded: (text) => colorize(text, colors.green),
|
|
71
|
+
processing: (text) => colorize(text, colors.cyan),
|
|
72
|
+
match: (text) => colorize(text, colors.green),
|
|
73
|
+
blocked: (text) => colorize(text, colors.red),
|
|
74
|
+
|
|
75
|
+
// Special emphasis
|
|
76
|
+
highlight: (text) => colorize(text, colors.brightCyan),
|
|
77
|
+
emphasis: (text) => colorize(text, colors.bright),
|
|
78
|
+
timing: (text) => colorize(text, colors.cyan),
|
|
79
|
+
|
|
80
|
+
// File operations
|
|
81
|
+
fileOp: (text) => colorize(text, colors.magenta),
|
|
82
|
+
compression: (text) => colorize(text, colors.cyan)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a colored tag with consistent formatting
|
|
87
|
+
* Used for status tags like [debug], [info], [warn], etc.
|
|
88
|
+
* @param {string} tag - The tag text (without brackets)
|
|
89
|
+
* @param {string} color - The color to apply
|
|
90
|
+
* @returns {string} Formatted colored tag
|
|
91
|
+
*/
|
|
92
|
+
function createTag(tag, color) {
|
|
93
|
+
return colorize(`[${tag}]`, color);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pre-built tags for common log levels
|
|
98
|
+
*/
|
|
99
|
+
const tags = {
|
|
100
|
+
debug: createTag('debug', colors.gray),
|
|
101
|
+
info: createTag('info', colors.blue),
|
|
102
|
+
warn: createTag('warn', colors.yellow),
|
|
103
|
+
error: createTag('error', colors.red),
|
|
104
|
+
match: createTag('match', colors.green),
|
|
105
|
+
compare: createTag('compare', colors.blue)
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Formats a complete log message with colored tag and message
|
|
110
|
+
* @param {string} tag - The tag name (debug, info, warn, error, etc.)
|
|
111
|
+
* @param {string} message - The message content
|
|
112
|
+
* @returns {string} Formatted log message
|
|
113
|
+
*/
|
|
114
|
+
function formatLogMessage(tag, message) {
|
|
115
|
+
const coloredTag = tags[tag] || createTag(tag, colors.white);
|
|
116
|
+
return `${coloredTag} ${message}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Utility function to check if colors are currently enabled
|
|
121
|
+
* @returns {boolean} Current color enable status
|
|
122
|
+
*/
|
|
123
|
+
function isColorEnabled() {
|
|
124
|
+
return enableColors;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates a rainbow effect for special messages (like completion)
|
|
129
|
+
* @param {string} text - Text to apply rainbow effect to
|
|
130
|
+
* @returns {string} Text with rainbow coloring (if colors enabled)
|
|
131
|
+
*/
|
|
132
|
+
function rainbow(text) {
|
|
133
|
+
if (!enableColors || text.length === 0) return text;
|
|
134
|
+
|
|
135
|
+
const rainbowColors = [
|
|
136
|
+
colors.red, colors.yellow, colors.green,
|
|
137
|
+
colors.cyan, colors.blue, colors.magenta
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
return text
|
|
141
|
+
.split('')
|
|
142
|
+
.map((char, index) => {
|
|
143
|
+
const colorIndex = index % rainbowColors.length;
|
|
144
|
+
return `${rainbowColors[colorIndex]}${char}`;
|
|
145
|
+
})
|
|
146
|
+
.join('') + colors.reset;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
// Core functions
|
|
151
|
+
colorize,
|
|
152
|
+
colors,
|
|
153
|
+
|
|
154
|
+
// Semantic coloring
|
|
155
|
+
messageColors,
|
|
156
|
+
tags,
|
|
157
|
+
createTag,
|
|
158
|
+
formatLogMessage,
|
|
159
|
+
|
|
160
|
+
// Utility functions
|
|
161
|
+
isColorEnabled,
|
|
162
|
+
shouldEnableColors,
|
|
163
|
+
rainbow,
|
|
164
|
+
|
|
165
|
+
// Legacy compatibility - keep original function names
|
|
166
|
+
colorize: colorize, // Explicit export for backward compatibility
|
|
167
|
+
colors: colors // Explicit export for backward compatibility
|
|
168
|
+
};
|
package/lib/compare.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loads rules from a comparison file and returns them as a Set for fast lookup
|
|
6
|
+
* @param {string} compareFilePath - Path to the file containing existing rules
|
|
7
|
+
* @param {boolean} forceDebug - Whether to show debug output
|
|
8
|
+
* @returns {Set<string>} Set of existing rules (normalized)
|
|
9
|
+
*/
|
|
10
|
+
function loadComparisonRules(compareFilePath, forceDebug = false) {
|
|
11
|
+
try {
|
|
12
|
+
if (!fs.existsSync(compareFilePath)) {
|
|
13
|
+
throw new Error(`Comparison file not found: ${compareFilePath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const content = fs.readFileSync(compareFilePath, 'utf8');
|
|
17
|
+
const lines = content.split('\n')
|
|
18
|
+
.map(line => line.trim())
|
|
19
|
+
.filter(line => line && !line.startsWith('!') && !line.startsWith('#')); // Skip comments and empty lines
|
|
20
|
+
|
|
21
|
+
const rules = new Set();
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
// Normalize the rule by removing different prefixes/formats
|
|
25
|
+
let normalizedRule = line;
|
|
26
|
+
|
|
27
|
+
// Remove adblock prefixes (||, |, etc.)
|
|
28
|
+
normalizedRule = normalizedRule.replace(/^\|\|/, '');
|
|
29
|
+
normalizedRule = normalizedRule.replace(/^\|/, '');
|
|
30
|
+
|
|
31
|
+
// Remove localhost prefixes
|
|
32
|
+
normalizedRule = normalizedRule.replace(/^127\.0\.0\.1\s+/, '');
|
|
33
|
+
normalizedRule = normalizedRule.replace(/^0\.0\.0\.0\s+/, '');
|
|
34
|
+
|
|
35
|
+
// Remove adblock suffixes and modifiers
|
|
36
|
+
normalizedRule = normalizedRule.replace(/\^.*$/, ''); // Remove ^ and everything after
|
|
37
|
+
normalizedRule = normalizedRule.replace(/\$.*$/, ''); // Remove $ and everything after
|
|
38
|
+
|
|
39
|
+
// Clean up and add to set
|
|
40
|
+
normalizedRule = normalizedRule.trim();
|
|
41
|
+
if (normalizedRule) {
|
|
42
|
+
rules.add(normalizedRule);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (forceDebug) {
|
|
47
|
+
console.log(`[debug] Loaded ${rules.size} comparison rules from ${compareFilePath}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return rules;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new Error(`Failed to load comparison file: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes a rule to match the format used in comparison
|
|
58
|
+
* @param {string} rule - The rule to normalize
|
|
59
|
+
* @returns {string} Normalized rule
|
|
60
|
+
*/
|
|
61
|
+
function normalizeRule(rule) {
|
|
62
|
+
let normalized = rule;
|
|
63
|
+
|
|
64
|
+
// Remove adblock prefixes
|
|
65
|
+
normalized = normalized.replace(/^\|\|/, '');
|
|
66
|
+
normalized = normalized.replace(/^\|/, '');
|
|
67
|
+
|
|
68
|
+
// Remove localhost prefixes
|
|
69
|
+
normalized = normalized.replace(/^127\.0\.0\.1\s+/, '');
|
|
70
|
+
normalized = normalized.replace(/^0\.0\.0\.0\s+/, '');
|
|
71
|
+
|
|
72
|
+
// Remove adblock suffixes and modifiers
|
|
73
|
+
normalized = normalized.replace(/\^.*$/, '');
|
|
74
|
+
normalized = normalized.replace(/\$.*$/, '');
|
|
75
|
+
|
|
76
|
+
return normalized.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Filters out rules that exist in the comparison set, with smart title handling
|
|
81
|
+
* @param {Array<string>} rules - Array of rules to filter
|
|
82
|
+
* @param {Set<string>} comparisonRules - Set of existing rules
|
|
83
|
+
* @param {boolean} forceDebug - Whether to show debug output
|
|
84
|
+
* @returns {Array<string>} Filtered rules array
|
|
85
|
+
*/
|
|
86
|
+
function filterUniqueRules(rules, comparisonRules, forceDebug = false) {
|
|
87
|
+
const result = [];
|
|
88
|
+
let duplicateCount = 0;
|
|
89
|
+
let orphanedTitles = 0;
|
|
90
|
+
|
|
91
|
+
// Group rules by titles for smart filtering
|
|
92
|
+
const groups = [];
|
|
93
|
+
let currentGroup = { title: null, rules: [] };
|
|
94
|
+
|
|
95
|
+
for (const rule of rules) {
|
|
96
|
+
if (rule.startsWith('!')) {
|
|
97
|
+
// Start a new group when we encounter a title
|
|
98
|
+
if (currentGroup.title !== null || currentGroup.rules.length > 0) {
|
|
99
|
+
groups.push(currentGroup);
|
|
100
|
+
}
|
|
101
|
+
currentGroup = { title: rule, rules: [] };
|
|
102
|
+
} else {
|
|
103
|
+
// Add rule to current group
|
|
104
|
+
currentGroup.rules.push(rule);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Don't forget the last group
|
|
109
|
+
if (currentGroup.title !== null || currentGroup.rules.length > 0) {
|
|
110
|
+
groups.push(currentGroup);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Process each group
|
|
114
|
+
for (const group of groups) {
|
|
115
|
+
const filteredRules = [];
|
|
116
|
+
|
|
117
|
+
// Filter rules in this group
|
|
118
|
+
for (const rule of group.rules) {
|
|
119
|
+
const normalized = normalizeRule(rule);
|
|
120
|
+
|
|
121
|
+
if (!comparisonRules.has(normalized)) {
|
|
122
|
+
filteredRules.push(rule);
|
|
123
|
+
} else {
|
|
124
|
+
duplicateCount++;
|
|
125
|
+
if (forceDebug) {
|
|
126
|
+
console.log(`[debug] Filtered duplicate rule: ${rule} (normalized: ${normalized})`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Only include title if there are remaining rules, or if there's no title (rules without titles)
|
|
132
|
+
if (group.title && filteredRules.length > 0) {
|
|
133
|
+
result.push(group.title);
|
|
134
|
+
result.push(...filteredRules);
|
|
135
|
+
} else if (!group.title && filteredRules.length > 0) {
|
|
136
|
+
// Rules without a title - just add them
|
|
137
|
+
result.push(...filteredRules);
|
|
138
|
+
} else if (group.title && filteredRules.length === 0) {
|
|
139
|
+
// Title with no remaining rules - this is an orphaned title
|
|
140
|
+
orphanedTitles++;
|
|
141
|
+
if (forceDebug) {
|
|
142
|
+
console.log(`[debug] Filtered orphaned title: ${group.title} (no unique rules remaining)`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (forceDebug) {
|
|
148
|
+
console.log(`[debug] Filtered ${duplicateCount} duplicate rules and ${orphanedTitles} orphaned titles`);
|
|
149
|
+
console.log(`[debug] ${result.filter(r => !r.startsWith('!')).length} unique rules remaining`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
loadComparisonRules,
|
|
157
|
+
normalizeRule,
|
|
158
|
+
filterUniqueRules
|
|
159
|
+
};
|
package/lib/compress.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// === Log Compression Utility ===
|
|
2
|
+
// This module provides gzip compression functionality for log files
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compresses a file using gzip and optionally removes the original
|
|
10
|
+
* @param {string} filePath - Path to the file to compress
|
|
11
|
+
* @param {boolean} removeOriginal - Whether to remove the original file after compression
|
|
12
|
+
* @returns {Promise<string>} - Path to the compressed file
|
|
13
|
+
*/
|
|
14
|
+
async function compressFile(filePath, removeOriginal = true) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const compressedPath = `${filePath}.gz`;
|
|
17
|
+
|
|
18
|
+
// Create read and write streams
|
|
19
|
+
const readStream = fs.createReadStream(filePath);
|
|
20
|
+
const writeStream = fs.createWriteStream(compressedPath);
|
|
21
|
+
const gzipStream = zlib.createGzip();
|
|
22
|
+
|
|
23
|
+
// Handle errors
|
|
24
|
+
const handleError = (error) => {
|
|
25
|
+
// Clean up partial compressed file on error
|
|
26
|
+
try {
|
|
27
|
+
if (fs.existsSync(compressedPath)) {
|
|
28
|
+
fs.unlinkSync(compressedPath);
|
|
29
|
+
}
|
|
30
|
+
} catch (cleanupErr) {
|
|
31
|
+
// Ignore cleanup errors
|
|
32
|
+
}
|
|
33
|
+
reject(error);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
readStream.on('error', handleError);
|
|
37
|
+
writeStream.on('error', handleError);
|
|
38
|
+
gzipStream.on('error', handleError);
|
|
39
|
+
|
|
40
|
+
// Handle successful completion
|
|
41
|
+
writeStream.on('finish', () => {
|
|
42
|
+
if (removeOriginal) {
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(filePath);
|
|
45
|
+
} catch (removeErr) {
|
|
46
|
+
// If we can't remove original, still consider compression successful
|
|
47
|
+
console.warn(`[warn] Failed to remove original file ${filePath}: ${removeErr.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
resolve(compressedPath);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Pipe the streams
|
|
54
|
+
readStream.pipe(gzipStream).pipe(writeStream);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compresses multiple files and returns results
|
|
60
|
+
* @param {string[]} filePaths - Array of file paths to compress
|
|
61
|
+
* @param {boolean} removeOriginals - Whether to remove original files
|
|
62
|
+
* @returns {Promise<Object>} - Object with successful and failed compressions
|
|
63
|
+
*/
|
|
64
|
+
async function compressMultipleFiles(filePaths, removeOriginals = true) {
|
|
65
|
+
const results = {
|
|
66
|
+
successful: [],
|
|
67
|
+
failed: []
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const filePath of filePaths) {
|
|
71
|
+
try {
|
|
72
|
+
if (fs.existsSync(filePath)) {
|
|
73
|
+
const compressedPath = await compressFile(filePath, removeOriginals);
|
|
74
|
+
results.successful.push({
|
|
75
|
+
original: filePath,
|
|
76
|
+
compressed: compressedPath
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
results.failed.push({
|
|
80
|
+
path: filePath,
|
|
81
|
+
error: 'File does not exist'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
results.failed.push({
|
|
86
|
+
path: filePath,
|
|
87
|
+
error: error.message
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gets the compression ratio of a file
|
|
97
|
+
* @param {string} originalPath - Path to original file
|
|
98
|
+
* @param {string} compressedPath - Path to compressed file
|
|
99
|
+
* @returns {number} - Compression ratio (0-1, where 0.5 means 50% of original size)
|
|
100
|
+
*/
|
|
101
|
+
function getCompressionRatio(originalPath, compressedPath) {
|
|
102
|
+
try {
|
|
103
|
+
const originalSize = fs.statSync(originalPath).size;
|
|
104
|
+
const compressedSize = fs.statSync(compressedPath).size;
|
|
105
|
+
return compressedSize / originalSize;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Formats file size in human readable format
|
|
113
|
+
* @param {number} bytes - Size in bytes
|
|
114
|
+
* @returns {string} - Formatted size string
|
|
115
|
+
*/
|
|
116
|
+
function formatFileSize(bytes) {
|
|
117
|
+
if (bytes === 0) return '0 B';
|
|
118
|
+
const k = 1024;
|
|
119
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
120
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
121
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
compressFile,
|
|
126
|
+
compressMultipleFiles,
|
|
127
|
+
getCompressionRatio,
|
|
128
|
+
formatFileSize
|
|
129
|
+
};
|