fsd-fs 0.14.0 → 0.15.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 郑州渺漠信息科技有限公司
3
+ Copyright (c) 2026 Liang Xingchen https://github.com/liangxingchen
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,29 +1,79 @@
1
1
  # fsd-fs
2
- FSD 本地磁盘文件读写适配器。
3
2
 
4
- ```js
5
- const FSD = require('fsd');
6
- const FSAdapter = require('fsd-fs');
3
+ FSD 本地文件系统适配器 - 提供对服务器磁盘文件的读写访问。
7
4
 
8
- const adapter = new FSAdapter(config);
5
+ [![npm version](https://badge.fury.io/js/fsd-fs.svg)](https://www.npmjs.com/package/fsd-fs)
9
6
 
10
- const fsd = FSD({ adapter: adapter });
7
+ ## 概述
11
8
 
12
- let file = fsd('/test.txt');
9
+ `fsd-fs` `fsd` 核心库的本地文件系统适配器,提供完整的文件和目录操作能力。
13
10
 
14
- let content = await fsd.read();
11
+ ### 核心特性
15
12
 
13
+ - ✅ 完整的文件读写操作
14
+ - ✅ 目录管理(创建、删除、遍历)
15
+ - ✅ 流式读写(大文件处理)
16
+ - ✅ 分段上传(大文件优化)
17
+ - ✅ 递归目录列表
18
+ - ✅ Glob 模式匹配
19
+ - ✅ 文件权限控制
20
+ - ✅ 并发控制优化
21
+
22
+ ## 安装
23
+
24
+ ```bash
25
+ npm install fsd-fs
16
26
  ```
17
27
 
18
- FSD 文档: https://github.com/liangxingchen/fsd
28
+ ## 配置
29
+
30
+ ```typescript
31
+ import FSD from 'fsd';
32
+ import FSAdapter from 'fsd-fs';
33
+
34
+ // 创建适配器
35
+ const adapter = new FSAdapter({
36
+ root: '/app/uploads', // 必需:本地存储根路径
37
+ mode: 0o644, // 可选:创建文件权限(默认 0o644)
38
+ tmpdir: '/tmp/fsd-multipart', // 可选:临时目录(分段上传时使用,默认系统临时目录)
39
+ urlPrefix: 'https://cdn.example.com' // 可选:URL 前缀(生成访问链接时使用)
40
+ });
41
+
42
+ // 创建 FSD 实例
43
+ const fsd = FSD({ adapter });
44
+ ```
45
+
46
+ ### 配置选项说明
47
+
48
+ | 选项 | 类型 | 必需 | 默认值 | 说明 |
49
+ |------|------|------|--------|------|
50
+ | `root` | string | 是 | - | 本地存储根路径,所有文件操作限制在此目录下 |
51
+ | `mode` | number | 否 | 0o644 | 创建文件时的权限模式(八进制) |
52
+ | `tmpdir` | string | 否 | os.tmpdir() | 分段上传时的临时文件存储目录 |
53
+ | `urlPrefix` | string | 否 | - | URL 前缀,用于生成访问链接,通常配合 CDN 或反向代理使用 |
54
+
55
+ ### 文件权限模式
56
+
57
+ 文件权限使用八进制表示法:
58
+
59
+ | 模式 | 说明 |
60
+ |------|------|
61
+ | 0o644 | rw-r--r-- (用户读写,组和其他只读) - 最常用 |
62
+ | 0o755 | rwxr-xr-x (用户读写执行,组和其他读执行) - 目录常用 |
63
+ | 0o600 | rw------- (仅用户可读写) - 敏感文件 |
64
+ | 0o666 | rw-rw-rw- (用户、组、其他都可读写) - 默认值 |
19
65
 
20
- 适配器初始化选项:
66
+ 权限位说明:
67
+ - `4` = read (读)
68
+ - `2` = write (写)
69
+ - `1` = execute (执行)
70
+ - `0` = 无权限
21
71
 
22
- | 选项 | 类型 | 必须 | 说明 |
23
- | --------- | ------ | ---- | ----------------------------- |
24
- | root | string | Yes | 本地存储根路径,例如 '/app/uploads' |
25
- | mode | number | | 创建的文件权限,默认 `0o666` |
26
- | tmpdir | string | | 临时目录,用于分段上传时暂存文件,如不使用分段上传,可省略 |
27
- | urlPrefix | string | | URL前缀,用于生成下载链接 |
72
+ 示例 `0o644`:
73
+ - 用户: 4+2+0 = 6 (rw)
74
+ - 组: 4+0+0 = 4 (r--)
75
+ - 其他: 4+0+0 = 4 (r--)
28
76
 
77
+ ## License
29
78
 
79
+ MIT
package/index.d.ts CHANGED
@@ -1,10 +1,148 @@
1
1
  import { Adapter } from 'fsd';
2
2
 
3
+ /**
4
+ * FSAdapter 配置选项
5
+ *
6
+ * 本地文件系统适配器的初始化配置。
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const adapter = new FSAdapter({
11
+ * root: '/app/uploads',
12
+ * mode: 0o644,
13
+ * tmpdir: '/tmp/fsd-tmp',
14
+ * urlPrefix: 'https://cdn.example.com'
15
+ * });
16
+ * ```
17
+ */
3
18
  export interface FSAdapterOptions {
19
+ /**
20
+ * 本地存储根路径(必需)
21
+ *
22
+ * 所有文件操作都会在此根路径下进行。
23
+ * 此路径必须是绝对路径,并且适配器有读取/写入权限。
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * // Linux/Mac
28
+ * root: '/app/uploads'
29
+ *
30
+ * // Windows
31
+ * root: 'C:\\app\\uploads'
32
+ * ```
33
+ *
34
+ * @remarks
35
+ * 确保此目录存在且有适当的读写权限。
36
+ */
4
37
  root: string;
38
+
39
+ /**
40
+ * 创建文件的权限模式(可选)
41
+ *
42
+ * 默认值:`0o644` (rw-rw-rw-)
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * mode: 0o644, // rw-r--r-- (用户读写,组和其他只读)
47
+ * mode: 0o755, // rwxr-xr-x (用户读写执行,组和其他读执行)
48
+ * mode: 0o600, // rw------- (仅用户可读写)
49
+ * ```
50
+ *
51
+ * @remarks
52
+ * 使用八进制表示法(0o 前缀)或十进制都可以。
53
+ */
5
54
  mode?: number;
55
+
56
+ /**
57
+ * URL 前缀(可选)
58
+ *
59
+ * 用于生成访问链接时添加前缀。
60
+ * 通常配合 CDN 或反向代理使用。
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * urlPrefix: 'https://cdn.example.com',
65
+ * // file.createUrl() 返回: https://cdn.example.com/uploads/file.txt
66
+ * ```
67
+ *
68
+ * @remarks
69
+ * 尾部会自动去除 `/`,例如 `'https://cdn.com/'` 会被标准化为 `'https://cdn.com'`。
70
+ */
6
71
  urlPrefix?: string;
72
+
73
+ /**
74
+ * 临时目录(可选)
75
+ *
76
+ * 用于分段上传时暂存分片文件。
77
+ * 如果不使用分段上传功能,可以省略此选项。
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * tmpdir: '/tmp/fsd-multipart',
82
+ * // 或使用系统默认临时目录
83
+ * tmpdir: os.tmpdir()
84
+ * ```
85
+ *
86
+ * @remarks
87
+ * 默认值:系统临时目录(通过 `os.tmpdir()` 获取)。
88
+ * 确保临时目录有足够的磁盘空间用于存储分片文件。
89
+ */
7
90
  tmpdir?: string;
8
91
  }
9
92
 
93
+ /**
94
+ * 本地文件系统适配器
95
+ *
96
+ * 提供对服务器本地磁盘文件的读写访问。
97
+ *
98
+ * @remarks
99
+ * ### 特性
100
+ * - 支持完整的文件和目录操作
101
+ * - 支持流式读写
102
+ * - 支持分段上传(大文件)
103
+ * - 支持递归目录列表
104
+ * - 使用 glob 模式匹配
105
+ *
106
+ * ### 权限要求
107
+ * - `root` 目录必须有读写权限
108
+ * - `tmpdir` 目录(用于分段上传)必须有读写权限
109
+ *
110
+ * ### 性能优化
111
+ * - 使用 `mapLimit` 限制并发操作,避免文件系统过载
112
+ * - 使用 `glob` 进行高效的目录遍历
113
+ *
114
+ * ### 分段上传机制
115
+ * 1. 调用 `initMultipartUpload()` 初始化任务
116
+ * 2. 每个分片写入到 `tmpdir` 下的临时文件
117
+ * 3. 调用 `completeMultipartUpload()` 时,所有临时文件会被顺序追加到目标文件
118
+ * 4. 完成后临时文件会被自动删除
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import FSAdapter from 'fsd-fs';
123
+ * import FSD from 'fsd';
124
+ *
125
+ * // 创建适配器
126
+ * const adapter = new FSAdapter({
127
+ * root: '/app/uploads',
128
+ * mode: 0o644,
129
+ * tmpdir: '/tmp/fsd-tmp'
130
+ * });
131
+ *
132
+ * // 创建 FSD 实例
133
+ * const fsd = FSD({ adapter });
134
+ *
135
+ * // 基本操作
136
+ * await fsd('/test.txt').write('Hello, FS!');
137
+ * const content = await fsd('/test.txt').read('utf8');
138
+ *
139
+ * // 分段上传大文件
140
+ * const file = fsd('/large-file.dat');
141
+ * const tasks = await file.initMultipartUpload(3);
142
+ * for (let i = 0; i < tasks.length; i++) {
143
+ * await file.writePart(tasks[i], getDataForPart(i));
144
+ * }
145
+ * await file.completeMultipartUpload(['part://...', 'part://...', 'part://...']);
146
+ * ```
147
+ */
10
148
  export default class FSAdapter extends Adapter<FSAdapterOptions> {}
package/lib/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const os = require("os");
4
- const Path = require("path");
5
- const fs = require("fs");
6
- const isStream = require("is-stream");
3
+ const tslib_1 = require("tslib");
4
+ const os_1 = tslib_1.__importDefault(require("os"));
5
+ const path_1 = tslib_1.__importDefault(require("path"));
6
+ const fs_1 = tslib_1.__importDefault(require("fs"));
7
+ const is_stream_1 = tslib_1.__importDefault(require("is-stream"));
7
8
  const glob_1 = require("glob");
8
- const mapLimit = require("async/mapLimit");
9
- const Debugger = require("debug");
10
- const debug = Debugger('fsd-fs');
9
+ const mapLimit_1 = tslib_1.__importDefault(require("async/mapLimit"));
10
+ const debug_1 = tslib_1.__importDefault(require("debug"));
11
+ const debug = (0, debug_1.default)('fsd-fs');
11
12
  async function getStat(path) {
12
13
  try {
13
- return await fs.promises.stat(path);
14
+ return await fs_1.default.promises.stat(path);
14
15
  }
15
16
  catch (_e) {
16
17
  return null;
@@ -24,8 +25,8 @@ class FSAdapter {
24
25
  this._options = Object.assign({
25
26
  urlPrefix: '',
26
27
  root: '/',
27
- mode: 0o666,
28
- tmpdir: os.tmpdir()
28
+ mode: 0o644,
29
+ tmpdir: os_1.default.tmpdir()
29
30
  }, options);
30
31
  let { urlPrefix } = this._options;
31
32
  if (urlPrefix.endsWith('/')) {
@@ -36,13 +37,13 @@ class FSAdapter {
36
37
  async append(path, data) {
37
38
  debug('append %s', path);
38
39
  let { root, mode } = this._options;
39
- let p = Path.join(root, path);
40
- if (isStream.readable(data)) {
40
+ let p = path_1.default.join(root, path);
41
+ if (is_stream_1.default.readable(data)) {
41
42
  let stream = data;
42
43
  await new Promise((resolve, reject) => {
43
- fs.stat(p, (error, stat) => {
44
+ fs_1.default.stat(p, (error, stat) => {
44
45
  let start = error ? 0 : stat.size;
45
- let writeStream = fs.createWriteStream(p, {
46
+ let writeStream = fs_1.default.createWriteStream(p, {
46
47
  flags: 'a',
47
48
  mode,
48
49
  start
@@ -52,33 +53,33 @@ class FSAdapter {
52
53
  });
53
54
  return;
54
55
  }
55
- await fs.promises.appendFile(p, data, { mode });
56
+ await fs_1.default.promises.appendFile(p, data, { mode });
56
57
  }
57
58
  async createReadStream(path, options) {
58
59
  debug('createReadStream %s options: %o', path, options);
59
- let p = Path.join(this._options.root, path);
60
- return fs.createReadStream(p, options);
60
+ let p = path_1.default.join(this._options.root, path);
61
+ return fs_1.default.createReadStream(p, options);
61
62
  }
62
63
  async createWriteStream(path, options) {
63
64
  debug('createWriteStream %s', path);
64
65
  let p;
65
66
  if (path.startsWith('task://')) {
66
- p = Path.join(this._options.tmpdir, path.replace('task://', ''));
67
+ p = path_1.default.join(this._options.tmpdir, path.replace('task://', ''));
67
68
  }
68
69
  else {
69
- p = Path.join(this._options.root, path);
70
+ p = path_1.default.join(this._options.root, path);
70
71
  }
71
- return fs.createWriteStream(p, options);
72
+ return fs_1.default.createWriteStream(p, options);
72
73
  }
73
74
  async unlink(path) {
74
75
  debug('unlink %s', path);
75
- let p = Path.join(this._options.root, path);
76
- await fs.promises.rm(p, { recursive: true, force: true });
76
+ let p = path_1.default.join(this._options.root, path);
77
+ await fs_1.default.promises.rm(p, { recursive: true, force: true });
77
78
  }
78
79
  async mkdir(path, recursive) {
79
80
  debug('mkdir %s', path);
80
- let fsPath = Path.join(this._options.root, path);
81
- await fs.promises.mkdir(fsPath, { recursive });
81
+ let fsPath = path_1.default.join(this._options.root, path);
82
+ await fs_1.default.promises.mkdir(fsPath, { recursive });
82
83
  }
83
84
  async readdir(path, recursion) {
84
85
  debug('readdir %s', path);
@@ -86,13 +87,13 @@ class FSAdapter {
86
87
  recursion = '**/*';
87
88
  }
88
89
  let pattern = recursion || '*';
89
- let p = Path.join(this._options.root, path);
90
+ let p = path_1.default.join(this._options.root, path);
90
91
  let files = await (0, glob_1.glob)(pattern, {
91
92
  cwd: p
92
93
  });
93
94
  files.reverse();
94
- return await mapLimit(files, 20, async (name) => {
95
- let filePath = Path.join(p, name);
95
+ return await (0, mapLimit_1.default)(files, 20, async (name) => {
96
+ let filePath = path_1.default.join(p, name);
96
97
  let stat = await getStat(filePath);
97
98
  let isDir = stat.isDirectory();
98
99
  return {
@@ -112,44 +113,44 @@ class FSAdapter {
112
113
  async copy(path, dest) {
113
114
  debug('copy %s to %s', path, dest);
114
115
  const { root } = this._options;
115
- let from = Path.join(root, path);
116
- let to = Path.join(root, dest);
116
+ let from = path_1.default.join(root, path);
117
+ let to = path_1.default.join(root, dest);
117
118
  if (!(await getStat(from)))
118
119
  throw new Error(`source file '${path}' is not exists!`);
119
- await fs.promises.cp(from, to, { recursive: true, force: true });
120
+ await fs_1.default.promises.cp(from, to, { recursive: true, force: true });
120
121
  }
121
122
  async rename(path, dest) {
122
123
  debug('rename %s to %s', path, dest);
123
- let from = Path.join(this._options.root, path);
124
- let to = Path.join(this._options.root, dest);
125
- await fs.promises.rename(from, to);
124
+ let from = path_1.default.join(this._options.root, path);
125
+ let to = path_1.default.join(this._options.root, dest);
126
+ await fs_1.default.promises.rename(from, to);
126
127
  }
127
128
  async exists(path) {
128
129
  debug('check exists %s', path);
129
- let p = Path.join(this._options.root, path);
130
+ let p = path_1.default.join(this._options.root, path);
130
131
  return !!(await getStat(p));
131
132
  }
132
133
  async isFile(path) {
133
134
  debug('check is file %s', path);
134
- let p = Path.join(this._options.root, path);
135
+ let p = path_1.default.join(this._options.root, path);
135
136
  let stat = await getStat(p);
136
137
  return stat?.isFile();
137
138
  }
138
139
  async isDirectory(path) {
139
140
  debug('check is directory %s', path);
140
- let p = Path.join(this._options.root, path);
141
+ let p = path_1.default.join(this._options.root, path);
141
142
  let stat = await getStat(p);
142
143
  return stat?.isDirectory();
143
144
  }
144
145
  async size(path) {
145
146
  debug('get file size %s', path);
146
- let p = Path.join(this._options.root, path);
147
+ let p = path_1.default.join(this._options.root, path);
147
148
  let stat = await getStat(p);
148
149
  return stat.size;
149
150
  }
150
151
  async lastModified(path) {
151
152
  debug('get file lastModified %s', path);
152
- let p = Path.join(this._options.root, path);
153
+ let p = path_1.default.join(this._options.root, path);
153
154
  let stat = await getStat(p);
154
155
  return stat.mtime;
155
156
  }
@@ -178,27 +179,27 @@ class FSAdapter {
178
179
  for (let part of parts) {
179
180
  if (!part.startsWith('part://'))
180
181
  throw new Error(`${part} is not a part file`);
181
- let partPath = Path.join(this._options.tmpdir, part.replace('part://', ''));
182
+ let partPath = path_1.default.join(this._options.tmpdir, part.replace('part://', ''));
182
183
  let stat = await getStat(partPath);
183
184
  if (!stat)
184
185
  throw new Error(`part file ${part} is not exists`);
185
186
  partPaths.push({ file: partPath, size: stat.size });
186
187
  }
187
- let p = Path.join(this._options.root, path);
188
+ let p = path_1.default.join(this._options.root, path);
188
189
  let start = 0;
189
190
  for (let info of partPaths) {
190
- let writeStream = fs.createWriteStream(p, {
191
+ let writeStream = fs_1.default.createWriteStream(p, {
191
192
  flags: 'a',
192
193
  start
193
194
  });
194
- let stream = fs.createReadStream(info.file);
195
+ let stream = fs_1.default.createReadStream(info.file);
195
196
  await new Promise((resolve, reject) => {
196
197
  stream.pipe(writeStream).on('close', resolve).on('error', reject);
197
198
  });
198
199
  start += info.size;
199
200
  writeStream.close();
200
201
  }
201
- partPaths.forEach((info) => fs.promises.rm(info.file, { force: true }));
202
+ partPaths.forEach((info) => fs_1.default.promises.rm(info.file, { force: true }));
202
203
  }
203
204
  }
204
205
  exports.default = FSAdapter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fsd-fs",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "File system adapter for fsd",
5
5
  "main": "lib/index.js",
6
6
  "types": "index.d.ts",
@@ -9,13 +9,17 @@
9
9
  "prepublish": "npm run build"
10
10
  },
11
11
  "repository": "https://github.com/liangxingchen/fsd/tree/master/packages/fsd-fs",
12
- "author": "Liang <liang@miaomo.cc> (https://github.com/liangxingchen)",
12
+ "author": {
13
+ "name": "Liang",
14
+ "email": "liang@miaomo.cn",
15
+ "url": "https://github.com/liangxingchen"
16
+ },
13
17
  "license": "MIT",
14
18
  "dependencies": {
15
19
  "async": "*",
16
- "debug": "^4.4.0",
17
- "glob": "^10.4.5",
20
+ "debug": "^4.4.3",
21
+ "glob": "^10.5.0",
18
22
  "is-stream": "^2.0.1"
19
23
  },
20
- "gitHead": "74e32bf47242909f040eb6012dda56e5c5a668a0"
24
+ "gitHead": "ea754919fb95b49deffd529b5c01c66da0dc08f9"
21
25
  }