@mison/wecom-cleaner 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/src/utils.js ADDED
@@ -0,0 +1,365 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { MONTH_RE } from './constants.js';
5
+
6
+ export function expandHome(rawPath) {
7
+ if (!rawPath) {
8
+ return rawPath;
9
+ }
10
+ if (rawPath === '~') {
11
+ return os.homedir();
12
+ }
13
+ if (rawPath.startsWith('~/')) {
14
+ return path.join(os.homedir(), rawPath.slice(2));
15
+ }
16
+ return rawPath;
17
+ }
18
+
19
+ export function inferDataRootFromProfilesRoot(rootDir) {
20
+ const expanded = expandHome(rootDir);
21
+ if (!expanded) {
22
+ return null;
23
+ }
24
+ const normalized = path.resolve(expanded);
25
+ const normalizedLower = normalized.toLowerCase();
26
+ const marker = `${path.sep}documents${path.sep}profiles`;
27
+ const idx = normalizedLower.lastIndexOf(marker);
28
+ if (idx > 0) {
29
+ return normalized.slice(0, idx);
30
+ }
31
+ const baseName = path.basename(normalized).toLowerCase();
32
+ const parentName = path.basename(path.dirname(normalized)).toLowerCase();
33
+ if (baseName === 'profiles' && parentName === 'documents') {
34
+ return path.resolve(normalized, '..', '..');
35
+ }
36
+ return null;
37
+ }
38
+
39
+ export async function ensureDir(dirPath) {
40
+ await fs.mkdir(dirPath, { recursive: true });
41
+ }
42
+
43
+ export async function pathExists(targetPath) {
44
+ try {
45
+ await fs.access(targetPath);
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ export async function readJson(filePath, fallbackValue) {
53
+ try {
54
+ const raw = await fs.readFile(filePath, 'utf-8');
55
+ return JSON.parse(raw);
56
+ } catch {
57
+ return fallbackValue;
58
+ }
59
+ }
60
+
61
+ export async function writeJson(filePath, data) {
62
+ await ensureDir(path.dirname(filePath));
63
+ await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
64
+ }
65
+
66
+ export function decodeBase64Utf8(raw) {
67
+ if (!raw || typeof raw !== 'string') {
68
+ return '';
69
+ }
70
+ try {
71
+ return Buffer.from(raw, 'base64').toString('utf-8');
72
+ } catch {
73
+ return '';
74
+ }
75
+ }
76
+
77
+ export function formatBytes(bytes) {
78
+ const num = Number(bytes || 0);
79
+ if (num < 1024) {
80
+ return `${num}B`;
81
+ }
82
+ const units = ['KB', 'MB', 'GB', 'TB'];
83
+ let value = num / 1024;
84
+ let idx = 0;
85
+ while (value >= 1024 && idx < units.length - 1) {
86
+ value /= 1024;
87
+ idx += 1;
88
+ }
89
+ return `${value.toFixed(1)}${units[idx]}`;
90
+ }
91
+
92
+ export function normalizeMonthKey(input) {
93
+ if (!input || typeof input !== 'string') {
94
+ return null;
95
+ }
96
+ const match = input.trim().match(MONTH_RE);
97
+ if (!match?.groups) {
98
+ return null;
99
+ }
100
+ const year = Number(match.groups.y);
101
+ const month = Number(match.groups.m);
102
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
103
+ return null;
104
+ }
105
+ return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}`;
106
+ }
107
+
108
+ export function monthToSortableNumber(monthKey) {
109
+ const normalized = normalizeMonthKey(monthKey);
110
+ if (!normalized) {
111
+ return Number.NaN;
112
+ }
113
+ const [y, m] = normalized.split('-').map(Number);
114
+ return y * 100 + m;
115
+ }
116
+
117
+ export function sortMonthKeys(values, order = 'asc') {
118
+ const list = [...new Set(values.map((x) => normalizeMonthKey(x)).filter(Boolean))];
119
+ list.sort((a, b) => monthToSortableNumber(a) - monthToSortableNumber(b));
120
+ return order === 'desc' ? list.reverse() : list;
121
+ }
122
+
123
+ export function monthByDaysBefore(days) {
124
+ const now = new Date();
125
+ const past = new Date(now.getTime() - Number(days || 0) * 24 * 3600 * 1000);
126
+ return `${past.getUTCFullYear()}-${String(past.getUTCMonth() + 1).padStart(2, '0')}`;
127
+ }
128
+
129
+ export function compareMonthKey(a, b) {
130
+ return monthToSortableNumber(a) - monthToSortableNumber(b);
131
+ }
132
+
133
+ export function shortId(fullId) {
134
+ if (!fullId) {
135
+ return '-';
136
+ }
137
+ return String(fullId).slice(0, 8);
138
+ }
139
+
140
+ function charWidth(ch) {
141
+ if (!ch) {
142
+ return 0;
143
+ }
144
+ const code = ch.codePointAt(0);
145
+ if (!code) {
146
+ return 0;
147
+ }
148
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
149
+ return 0;
150
+ }
151
+ if (
152
+ (code >= 0x1100 && code <= 0x115f) ||
153
+ (code >= 0x2e80 && code <= 0xa4cf) ||
154
+ (code >= 0xac00 && code <= 0xd7a3) ||
155
+ (code >= 0xf900 && code <= 0xfaff) ||
156
+ (code >= 0xfe10 && code <= 0xfe19) ||
157
+ (code >= 0xfe30 && code <= 0xfe6f) ||
158
+ (code >= 0xff00 && code <= 0xff60) ||
159
+ (code >= 0xffe0 && code <= 0xffe6)
160
+ ) {
161
+ return 2;
162
+ }
163
+ return 1;
164
+ }
165
+
166
+ export function stringWidth(text) {
167
+ return Array.from(String(text ?? '')).reduce((acc, ch) => acc + charWidth(ch), 0);
168
+ }
169
+
170
+ export function trimToWidth(text, width) {
171
+ const input = String(text ?? '');
172
+ if (width <= 0) {
173
+ return '';
174
+ }
175
+ if (stringWidth(input) <= width) {
176
+ return input;
177
+ }
178
+ const suffix = width >= 3 ? '...' : '';
179
+ const target = width - stringWidth(suffix);
180
+ if (target <= 0) {
181
+ return suffix.slice(0, width);
182
+ }
183
+ let used = 0;
184
+ let out = '';
185
+ for (const ch of Array.from(input)) {
186
+ const w = charWidth(ch);
187
+ if (used + w > target) {
188
+ break;
189
+ }
190
+ out += ch;
191
+ used += w;
192
+ }
193
+ return `${out}${suffix}`;
194
+ }
195
+
196
+ export function padToWidth(text, width) {
197
+ const clipped = trimToWidth(text, width);
198
+ const padSize = Math.max(0, width - stringWidth(clipped));
199
+ return `${clipped}${' '.repeat(padSize)}`;
200
+ }
201
+
202
+ export function renderTable(headers, rows, options = {}) {
203
+ const terminalWidth = Number(options.terminalWidth || process.stdout.columns || 120);
204
+ const separator = ' | ';
205
+ const minColWidth = Number(options.minColWidth || 6);
206
+
207
+ const colCount = headers.length;
208
+ const widths = headers.map((header, col) => {
209
+ let max = stringWidth(header);
210
+ for (const row of rows) {
211
+ max = Math.max(max, stringWidth(row[col] ?? ''));
212
+ }
213
+ return Math.min(Math.max(max, minColWidth), 46);
214
+ });
215
+
216
+ const separatorWidth = separator.length * (colCount - 1);
217
+ const sum = () => widths.reduce((acc, w) => acc + w, 0) + separatorWidth;
218
+
219
+ while (sum() > terminalWidth && widths.some((w) => w > minColWidth)) {
220
+ let maxIdx = 0;
221
+ for (let i = 1; i < widths.length; i += 1) {
222
+ if (widths[i] > widths[maxIdx]) {
223
+ maxIdx = i;
224
+ }
225
+ }
226
+ widths[maxIdx] = Math.max(minColWidth, widths[maxIdx] - 1);
227
+ }
228
+
229
+ const drawRow = (row) => row.map((cell, idx) => padToWidth(cell, widths[idx])).join(separator);
230
+
231
+ const top = drawRow(headers);
232
+ const divider = widths.map((w) => '-'.repeat(w)).join(separator.replace(/./g, '-'));
233
+ const body = rows.map(drawRow);
234
+
235
+ return [top, divider, ...body].join('\n');
236
+ }
237
+
238
+ export function printSection(title) {
239
+ console.log(`\n=== ${title} ===`);
240
+ }
241
+
242
+ export async function mapLimit(items, limit, worker) {
243
+ const actualLimit = Math.max(1, Number(limit || 1));
244
+ const results = new Array(items.length);
245
+ let index = 0;
246
+
247
+ async function runWorker() {
248
+ while (true) {
249
+ const current = index;
250
+ if (current >= items.length) {
251
+ return;
252
+ }
253
+ index += 1;
254
+ results[current] = await worker(items[current], current);
255
+ }
256
+ }
257
+
258
+ const runners = [];
259
+ for (let i = 0; i < Math.min(actualLimit, items.length); i += 1) {
260
+ runners.push(runWorker());
261
+ }
262
+ await Promise.all(runners);
263
+ return results;
264
+ }
265
+
266
+ export async function calculateDirectorySize(targetPath) {
267
+ try {
268
+ const stat = await fs.stat(targetPath);
269
+ if (stat.isFile()) {
270
+ return stat.size;
271
+ }
272
+ if (!stat.isDirectory()) {
273
+ return 0;
274
+ }
275
+ } catch {
276
+ return 0;
277
+ }
278
+
279
+ let total = 0;
280
+ async function walk(dirPath) {
281
+ let entries;
282
+ try {
283
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
284
+ } catch {
285
+ return;
286
+ }
287
+
288
+ for (const entry of entries) {
289
+ const fullPath = path.join(dirPath, entry.name);
290
+ try {
291
+ if (entry.isSymbolicLink()) {
292
+ continue;
293
+ }
294
+ if (entry.isDirectory()) {
295
+ await walk(fullPath);
296
+ } else {
297
+ const stat = await fs.stat(fullPath);
298
+ total += stat.size;
299
+ }
300
+ } catch {
301
+ // 忽略单个文件的读取异常,继续统计其他条目
302
+ }
303
+ }
304
+ }
305
+ await walk(targetPath);
306
+ return total;
307
+ }
308
+
309
+ export function formatLocalDate(tsMillis) {
310
+ const d = new Date(tsMillis);
311
+ const y = d.getFullYear();
312
+ const m = String(d.getMonth() + 1).padStart(2, '0');
313
+ const day = String(d.getDate()).padStart(2, '0');
314
+ const hh = String(d.getHours()).padStart(2, '0');
315
+ const mm = String(d.getMinutes()).padStart(2, '0');
316
+ return `${y}-${m}-${day} ${hh}:${mm}`;
317
+ }
318
+
319
+ export async function appendJsonLine(filePath, payload) {
320
+ await ensureDir(path.dirname(filePath));
321
+ await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, 'utf-8');
322
+ }
323
+
324
+ export async function readJsonLines(filePath) {
325
+ const rows = [];
326
+ try {
327
+ const raw = await fs.readFile(filePath, 'utf-8');
328
+ for (const line of raw.split(/\r?\n/)) {
329
+ const text = line.trim();
330
+ if (!text) {
331
+ continue;
332
+ }
333
+ try {
334
+ rows.push(JSON.parse(text));
335
+ } catch {
336
+ // 忽略损坏的 JSONL 行,保持读取过程不中断
337
+ }
338
+ }
339
+ } catch {
340
+ return rows;
341
+ }
342
+ return rows;
343
+ }
344
+
345
+ export function printProgress(prefix, current, total) {
346
+ const safeTotal = Math.max(1, total);
347
+ const percent = Math.min(100, Math.floor((current / safeTotal) * 100));
348
+ const line = `${prefix} ${current}/${total} (${percent}%)`;
349
+ process.stdout.write(`\r${line.padEnd(70, ' ')}`);
350
+ if (current >= total) {
351
+ process.stdout.write('\n');
352
+ }
353
+ }
354
+
355
+ export function uniqueBy(items, keyFn) {
356
+ const map = new Map();
357
+ for (const item of items) {
358
+ map.set(keyFn(item), item);
359
+ }
360
+ return [...map.values()];
361
+ }
362
+
363
+ export function sleep(ms) {
364
+ return new Promise((resolve) => setTimeout(resolve, ms));
365
+ }