@ainc/fs 0.1.20 → 0.1.21

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.
@@ -12,6 +12,7 @@ exports.resolveFile = resolveFile;
12
12
  * 加载依赖
13
13
  *****************************************
14
14
  */
15
+ const node_path_1 = require("node:path");
15
16
  const stat_1 = require("./stat");
16
17
  /**
17
18
  *****************************************
@@ -19,10 +20,9 @@ const stat_1 = require("./stat");
19
20
  *****************************************
20
21
  */
21
22
  function resolveFile(path, extensions) {
22
- const stats = (0, stat_1.stat)(path);
23
- // 找到文件
24
- if (stats && stats.isFile()) {
25
- return path;
23
+ const dest = (0, node_path_1.resolve)(path);
24
+ if ((0, stat_1.isFile)(dest)) {
25
+ return dest;
26
26
  }
27
27
  // 不再查找
28
28
  if (!extensions || !extensions.length) {
@@ -30,10 +30,8 @@ function resolveFile(path, extensions) {
30
30
  }
31
31
  // 遍历扩展名
32
32
  for (let i = 0; i < extensions.length; ++i) {
33
- const file = path + extensions[i];
34
- const stats = (0, stat_1.stat)(file);
35
- // 查找到文件
36
- if (stats === null || stats === void 0 ? void 0 : stats.isFile()) {
33
+ const file = dest + extensions[i];
34
+ if ((0, stat_1.isFile)(file)) {
37
35
  return file;
38
36
  }
39
37
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ *****************************************
3
+ * 重试
4
+ *****************************************
5
+ */
6
+ export declare function retry<T>(times: number, handler: () => T): T;
package/dist/retry.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2026-03-01 20:06:58
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.retry = retry;
10
+ /**
11
+ *****************************************
12
+ * 重试
13
+ *****************************************
14
+ */
15
+ function retry(times, handler) {
16
+ let count = 1;
17
+ // 重试处理
18
+ for (;;) {
19
+ try {
20
+ const res = handler();
21
+ if (res instanceof Promise) {
22
+ return res.catch(tryPromise);
23
+ }
24
+ else {
25
+ return res;
26
+ }
27
+ }
28
+ catch (err) {
29
+ if (count++ >= times) {
30
+ throw err;
31
+ }
32
+ }
33
+ }
34
+ // 异步重试
35
+ async function tryPromise(err) {
36
+ if (count++ >= times) {
37
+ throw err;
38
+ }
39
+ for (;;) {
40
+ try {
41
+ return await handler();
42
+ }
43
+ catch (err) {
44
+ if (count++ >= times) {
45
+ throw err;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
package/dist/stat.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * 获取文件状态
4
4
  *****************************************
5
5
  */
6
- export declare function stat(path: string): import("fs").Stats | undefined;
6
+ export declare function stat(path: string): import("node:fs").Stats | undefined;
7
7
  /**
8
8
  *****************************************
9
9
  * 判断为目录
@@ -0,0 +1,96 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2026-03-14 13:42:34
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 并发控制
11
+ *****************************************
12
+ */
13
+ export class Concurrency {
14
+ /** 初始化 */
15
+ constructor(capacity, handler) {
16
+ /** 待处理任务 */
17
+ this._pending = [];
18
+ /** 正在执行的任务数 */
19
+ this._running = 0;
20
+ /** 结果缓存队列 */
21
+ this._results = [];
22
+ this._capacity = Math.max(1, capacity);
23
+ this._handler = handler;
24
+ }
25
+ /** 添加任务 */
26
+ add(data) {
27
+ this._pending.push(data);
28
+ return this;
29
+ }
30
+ /** 执行处理 */
31
+ async execute(handler) {
32
+ const results = [];
33
+ const fn = handler || (v => v);
34
+ // 遍历结果
35
+ while (await this.next()) {
36
+ results.push(await fn(this._value));
37
+ }
38
+ // 返回结果
39
+ return results;
40
+ }
41
+ /** 读取值 */
42
+ value() {
43
+ return this._value;
44
+ }
45
+ /** 执行处理 */
46
+ async next() {
47
+ if (this._pending.length) {
48
+ this._execute();
49
+ }
50
+ // 不存在结果,已经处理完
51
+ if (!this._results.length) {
52
+ return false;
53
+ }
54
+ // 等待结果
55
+ const res = this._results.shift();
56
+ await res.value;
57
+ if (res.err) {
58
+ throw res.err;
59
+ }
60
+ else {
61
+ this._value = res.value;
62
+ }
63
+ // 返回结果
64
+ return true;
65
+ }
66
+ /** 执行任务 */
67
+ async _execute() {
68
+ while (this._running < this._capacity) {
69
+ if (this._pending.length) {
70
+ this._process(this._pending.shift());
71
+ }
72
+ else {
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ /** 处理任务 */
78
+ _process(data) {
79
+ const res = {};
80
+ // 添加到结果集
81
+ this._results.push(res);
82
+ // 执行任务
83
+ try {
84
+ const value = res.value = this._handler(data);
85
+ if (value instanceof Promise) {
86
+ this._running++;
87
+ value
88
+ .then(val => res.value = val, err => res.err = err)
89
+ .finally(() => (this._running--, this._execute()));
90
+ }
91
+ }
92
+ catch (err) {
93
+ res.err = err;
94
+ }
95
+ }
96
+ }
package/esm/copy.mjs ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2025-05-25 11:02:21
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { cpSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { dirname } from 'node:path';
15
+ import { stat } from './stat.mjs';
16
+ import { mkdir } from './mkdir.mjs';
17
+ import { rm } from './rm.mjs';
18
+ /**
19
+ *****************************************
20
+ * 复制文件
21
+ *****************************************
22
+ */
23
+ export function copy(src, dest, options) {
24
+ const srcStats = stat(src);
25
+ if (!srcStats) {
26
+ return false;
27
+ }
28
+ // 处理目标文件
29
+ const destStats = stat(dest);
30
+ if (destStats) {
31
+ if (!options || !options.force) {
32
+ return false;
33
+ }
34
+ else {
35
+ rm(dest);
36
+ }
37
+ }
38
+ // 创建目录
39
+ if (!mkdir(dirname(dest), options)) {
40
+ return false;
41
+ }
42
+ // 执行拷贝
43
+ if (srcStats.isDirectory()) {
44
+ return (cpSync(src, dest, { ...options, recursive: true }), true);
45
+ }
46
+ else {
47
+ return (writeFileSync(dest, readFileSync(src)), true);
48
+ }
49
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2025-05-25 09:35:01
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { dirname as dir, resolve, isAbsolute } from 'node:path';
14
+ /**
15
+ *****************************************
16
+ * 获取父级目录
17
+ *****************************************
18
+ */
19
+ export function dirname(path) {
20
+ const dest = isAbsolute(path) ? path : resolve(path);
21
+ const parent = dir(dest);
22
+ // 校验目录
23
+ if (parent && parent !== dest) {
24
+ return parent;
25
+ }
26
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2026-02-25 16:26:38
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { tmpdir } from 'node:os';
14
+ import { resolve, join, dirname } from 'node:path';
15
+ import { stat, mkdir, rm, rename } from 'node:fs/promises';
16
+ import { createReadStream, createWriteStream } from 'node:fs';
17
+ import { Readable } from 'node:stream';
18
+ import { pipeline } from 'node:stream/promises';
19
+ import { retry } from './retry.mjs';
20
+ import { md5 } from './md5.mjs';
21
+ import { Concurrency } from './concurrency.mjs';
22
+ /**
23
+ *****************************************
24
+ * 下载文件
25
+ *****************************************
26
+ */
27
+ export async function download(dest, url, options = {}) {
28
+ const { concurrency, chunkSize, retryTimes, force, onProgress, ...init } = options;
29
+ const path = resolve(dest);
30
+ const opts = {
31
+ concurrency: concurrency && concurrency >= 1 ? concurrency : 8,
32
+ chunkSize: chunkSize && chunkSize >= 1 ? chunkSize : 1 * 1024 * 1024,
33
+ retryTimes: retryTimes && retryTimes >= 1 ? retryTimes : 3,
34
+ force,
35
+ onProgress,
36
+ };
37
+ // 创建下载目录
38
+ await mkdir(dirname(path), { recursive: true });
39
+ // 尝试分片下载
40
+ if (await downloadFileByChunk(path, url, init, opts)) {
41
+ return path;
42
+ }
43
+ else {
44
+ return retry(opts.retryTimes, () => downloadFile(path, url, init, opts));
45
+ }
46
+ }
47
+ /**
48
+ *****************************************
49
+ * 获取文件大小
50
+ *****************************************
51
+ */
52
+ async function downloadFileByChunk(dest, url, init, options) {
53
+ const stats = await stat(dest).catch(err => err.code === 'ENOENT' ? null : Promise.reject(err));
54
+ // 已经存在文件,不再下载
55
+ if (stats && stats.isFile() && !options.force) {
56
+ options.onProgress && options.onProgress(stats.size, stats.size);
57
+ return dest;
58
+ }
59
+ // 发起获取文件信息请求
60
+ const resp = await fetch(url, { method: 'HEAD' });
61
+ if (!resp.ok || resp.headers.get('accept-ranges') !== 'bytes') {
62
+ return;
63
+ }
64
+ // 获取文件总大小
65
+ const totalSize = parseInt(resp.headers.get('content-length') || '0', 10);
66
+ if (!totalSize) {
67
+ return;
68
+ }
69
+ // 获取分片配置
70
+ const chunkConcurrency = new Concurrency(options.concurrency, downloadChunk);
71
+ const chunkSize = options.chunkSize;
72
+ const chunkCount = Math.ceil(totalSize / chunkSize);
73
+ const downloadedSize = [0];
74
+ const downloadedTempFile = dest + '.download';
75
+ const dir = getDownloadDir(url, options);
76
+ const stream = createWriteStream(downloadedTempFile);
77
+ // 生成任务
78
+ for (let idx = 0; idx < chunkCount; idx++) {
79
+ chunkConcurrency.add({
80
+ start: idx * chunkSize,
81
+ end: Math.min((idx + 1) * chunkSize - 1, totalSize - 1),
82
+ idx,
83
+ url,
84
+ dir,
85
+ init,
86
+ options,
87
+ totalSize,
88
+ downloadedSize,
89
+ });
90
+ }
91
+ // 创建下载目录
92
+ await mkdir(dir, { recursive: true });
93
+ // 合并文件
94
+ await chunkConcurrency.execute(path => pipe(path, stream));
95
+ // 结束传输
96
+ stream.end();
97
+ // 重命名临时文件
98
+ await rename(downloadedTempFile, dest);
99
+ await rm(dir, { recursive: true });
100
+ // 返回文件路径
101
+ return dest;
102
+ }
103
+ /**
104
+ *****************************************
105
+ * 下载分片
106
+ *****************************************
107
+ */
108
+ async function downloadChunk(downloadChunkOptions) {
109
+ const { idx, start, end, url, dir, init, options, downloadedSize, totalSize } = downloadChunkOptions;
110
+ const path = join(dir, idx + '');
111
+ const opts = { ...options };
112
+ const requestInit = {
113
+ ...init,
114
+ headers: { ...init.headers, Range: `bytes=${start}-${end}` },
115
+ };
116
+ // 添加进度监听
117
+ if (options.onProgress) {
118
+ opts.onProgress = size => {
119
+ downloadedSize[idx] = size;
120
+ options.onProgress(downloadedSize.reduce((a, b) => a + b, 0), totalSize);
121
+ };
122
+ }
123
+ // 执行下载
124
+ return retry(options.retryTimes, () => downloadFile(path, url, requestInit, opts));
125
+ }
126
+ /**
127
+ *****************************************
128
+ * 生成下载 ID
129
+ *****************************************
130
+ */
131
+ function generateDownloadID(url, options) {
132
+ const now = new Date();
133
+ now.setHours(0, 0, 0, 0);
134
+ return md5(`${url}?chunkSize=${options.chunkSize || 0}&t=${now.getTime()}`);
135
+ }
136
+ /**
137
+ *****************************************
138
+ * 获取下载 URL
139
+ *****************************************
140
+ */
141
+ function getDownloadURL(url) {
142
+ if (typeof url === 'string') {
143
+ return url;
144
+ }
145
+ else {
146
+ return url instanceof URL ? url.href : url.url;
147
+ }
148
+ }
149
+ /**
150
+ *****************************************
151
+ * 获取下载目录
152
+ *****************************************
153
+ */
154
+ function getDownloadDir(url, options) {
155
+ return join(tmpdir(), 'downloads', generateDownloadID(getDownloadURL(url), options));
156
+ }
157
+ /**
158
+ *****************************************
159
+ * 写入文件流
160
+ *****************************************
161
+ */
162
+ function pipe(src, dest) {
163
+ return new Promise((resolve, reject) => {
164
+ const stream = createReadStream(src);
165
+ stream.once('close', resolve);
166
+ stream.once('error', reject);
167
+ stream.pipe(dest, { end: false });
168
+ });
169
+ }
170
+ /**
171
+ *****************************************
172
+ * 下载文件
173
+ *****************************************
174
+ */
175
+ async function downloadFile(dest, url, init, options) {
176
+ const stats = await stat(dest).catch(err => err.code === 'ENOENT' ? null : Promise.reject(err));
177
+ // 已经存在文件,不再下载
178
+ if (stats && stats.isFile() && !options.force) {
179
+ options.onProgress && options.onProgress(stats.size, stats.size);
180
+ return dest;
181
+ }
182
+ // 发送请求
183
+ const resp = await fetch(url, init);
184
+ if (!resp.ok) {
185
+ throw new Error(`${resp.status} ${resp.statusText}`);
186
+ }
187
+ // 设置流的大小(如果 Content-Length 头存在)
188
+ const totalSize = parseInt(resp.headers.get('Content-Length') || '0', 10);
189
+ const stream = Readable.from(resp.body || '');
190
+ const downloadedTempFile = dest + '.download';
191
+ const progress = options.onProgress;
192
+ // 添加进度监听
193
+ if (progress) {
194
+ let size = 0;
195
+ stream.on('data', chunk => progress(size += chunk.length, totalSize));
196
+ stream.on('end', () => size <= totalSize && progress(totalSize, totalSize));
197
+ }
198
+ // 下载文件
199
+ await pipeline(stream, createWriteStream(downloadedTempFile));
200
+ await rename(downloadedTempFile, dest);
201
+ // 返回文件路径
202
+ return dest;
203
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2026-03-14 10:51:52
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { download } from './download.mjs';
14
+ /**
15
+ *****************************************
16
+ * 下载文件
17
+ *****************************************
18
+ */
19
+ export async function downloadFile(dest, url) {
20
+ const print = stdout();
21
+ // 打印日志
22
+ console.log('[download]:', url);
23
+ // 下载文件
24
+ await download(dest, url, {
25
+ onProgress(size, total) {
26
+ if (size < total) {
27
+ print(` -> ${((size / total) * 100).toFixed(2)}% (${format(size)}/${format(total)})`);
28
+ }
29
+ else {
30
+ print('[download]: done');
31
+ }
32
+ },
33
+ });
34
+ }
35
+ /**
36
+ *****************************************
37
+ * 格式化字节大小为易读格式(B/KB/MB/GB)
38
+ *****************************************
39
+ */
40
+ function format(bytes) {
41
+ let limit = 1;
42
+ // 遍历单位
43
+ for (let unit of ['B', 'KB', 'MB', 'GB']) {
44
+ const next = 1024 * limit;
45
+ if (bytes < next) {
46
+ return `${(bytes / limit).toFixed(2)} ${unit}`;
47
+ }
48
+ else {
49
+ limit = next;
50
+ }
51
+ }
52
+ // 返回默认
53
+ return `${(bytes / limit).toFixed(2)} TB`;
54
+ }
55
+ /**
56
+ *****************************************
57
+ * 进度条
58
+ *****************************************
59
+ */
60
+ function stdout() {
61
+ let progressContent = '';
62
+ let progressSize = 0;
63
+ let progressTime = 0;
64
+ return function print(content) {
65
+ const now = Date.now();
66
+ if (content === progressContent || now - progressTime < 1000) {
67
+ return;
68
+ }
69
+ const size = content.length;
70
+ const diff = progressSize - size;
71
+ progressContent = content;
72
+ progressTime = now;
73
+ progressSize = size;
74
+ if (diff > 0) {
75
+ process.stdout.write(`\r${content}${' '.repeat(diff)}`);
76
+ }
77
+ else {
78
+ process.stdout.write(`\r${content}`);
79
+ }
80
+ };
81
+ }
package/esm/findUp.mjs ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2023-12-31 09:32:31
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { dirname } from 'node:path';
14
+ /**
15
+ *****************************************
16
+ * 向上查找
17
+ *****************************************
18
+ */
19
+ export function findUp(from, handler) {
20
+ let latest = '';
21
+ let current = from;
22
+ // 遍历目录
23
+ while (current !== latest) {
24
+ const result = handler(current);
25
+ // 存在结果
26
+ if (result !== undefined) {
27
+ return result;
28
+ }
29
+ // 获取上级目录
30
+ latest = current;
31
+ current = dirname(current);
32
+ }
33
+ }
package/esm/index.mjs ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2023-12-31 09:11:10
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 导出接口
11
+ *****************************************
12
+ */
13
+ export { resolve, join, isAbsolute } from 'node:path';
14
+ export { relative, isRelative } from './relative.mjs';
15
+ export { dirname } from './dirname.mjs';
16
+ export { stat, isDir, isFile } from './stat.mjs';
17
+ export { resolveFile } from './resolveFile.mjs';
18
+ export { readFile } from './readFile.mjs';
19
+ export { writeFile } from './writeFile.mjs';
20
+ export { copy } from './copy.mjs';
21
+ export { rm } from './rm.mjs';
22
+ export { lookup } from './lookup.mjs';
23
+ export { md5 } from './md5.mjs';
24
+ export { json, jsonc } from './jsonc.mjs';
25
+ export { findUp } from './findUp.mjs';
26
+ export { download } from './download.mjs';
package/esm/jsonc.mjs ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2023-12-31 10:16:56
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { stripComments } from './stripComments.mjs';
14
+ /**
15
+ *****************************************
16
+ * 解析 JSON
17
+ *****************************************
18
+ */
19
+ export function json(content) {
20
+ return JSON.parse(content);
21
+ }
22
+ /**
23
+ *****************************************
24
+ * 解析 JSONC
25
+ *****************************************
26
+ */
27
+ export function jsonc(content) {
28
+ return JSON.parse(stripComments(content));
29
+ }
package/esm/lookup.mjs ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ *****************************************
3
+ * Created by edonet@163.com
4
+ * Created on 2025-05-29 20:56:24
5
+ *****************************************
6
+ */
7
+ 'use strict';
8
+ /**
9
+ *****************************************
10
+ * 加载依赖
11
+ *****************************************
12
+ */
13
+ import { resolve as resolvePath } from 'node:path';
14
+ import { isFile } from './stat.mjs';
15
+ import { dirname } from './dirname.mjs';
16
+ /**
17
+ *****************************************
18
+ * 查找最近的文件
19
+ *****************************************
20
+ */
21
+ export function lookup(name, options) {
22
+ const opts = { cache: new Map(), resolve: isFile, ...options };
23
+ const dir = opts.from ? resolvePath(opts.from) : process.cwd();
24
+ // 查找文件
25
+ return find(name, dir, opts);
26
+ }
27
+ /**
28
+ *****************************************
29
+ * 查找最近的文件
30
+ *****************************************
31
+ */
32
+ function find(name, dir, options) {
33
+ const { cache, resolve } = options;
34
+ if (!dir) {
35
+ return;
36
+ }
37
+ // 解析路径
38
+ const path = resolvePath(dir, name);
39
+ if (cache.has(path)) {
40
+ return cache.get(path);
41
+ }
42
+ // 查找文件
43
+ const result = resolve(path, dir) || find(name, dirname(dir), options);
44
+ // 缓存结果
45
+ cache.set(path, result);
46
+ // 返回结果
47
+ return result;
48
+ }