endef 1.0.9 → 2.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/README.md +312 -0
- package/bin/main.js +5 -4
- package/package.json +12 -5
- package/src/config.js +107 -0
- package/src/de.js +53 -9
- package/src/en.js +64 -49
- package/src/pack.js +158 -0
- package/src/run.js +197 -7
- package/src/utils.js +68 -0
- package/src/de.sh +0 -36
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# endef
|
|
2
|
+
|
|
3
|
+
`endef` 是一个纯 Node.js 的文件旁路转换工具。
|
|
4
|
+
|
|
5
|
+
它适合这样的场景:某些常见源码或文本后缀的文件在系统读取、复制、粘贴、编辑器打开时出现乱码或异常,但通过 Node.js 脚本读取仍然正常。`endef` 会先把目标文件内容写到同目录的 `.endef` 副本里,再把 `.endef` 副本内容刷回原文件,并删除临时副本。
|
|
6
|
+
|
|
7
|
+
示例:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
src/App.vue -> src/App.vue.endef -> src/App.vue
|
|
11
|
+
src/main.ts -> src/main.ts.endef -> src/main.ts
|
|
12
|
+
README.md -> README.md.endef -> README.md
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> `endef` 不是杀毒工具,也不能保证绕过所有拦截方式。建议在干净系统、离线磁盘、PE 或 Linux Live 环境里使用,恢复后仍然需要做安全扫描和人工抽查。
|
|
16
|
+
|
|
17
|
+
## 特性
|
|
18
|
+
|
|
19
|
+
- 纯 Node.js 实现,不依赖 Bash、find、xargs、grep 等 shell 工具
|
|
20
|
+
- 默认旁路后缀为 `.endef`
|
|
21
|
+
- `en` 默认转换所有匹配后缀的文件
|
|
22
|
+
- `en --marker` 可以只转换命中 marker 的文件,marker 支持数组配置,也支持 CLI 手动指定多个
|
|
23
|
+
- `de` 会把 `.endef` 内容写回原文件,并删除 `.endef` 临时副本
|
|
24
|
+
- `pack` 会把恢复后的目录打包成 `.tar.gz`
|
|
25
|
+
- 不引入生产依赖
|
|
26
|
+
|
|
27
|
+
## 环境要求
|
|
28
|
+
|
|
29
|
+
推荐 Node.js 22 或更高版本。
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g endef
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
生成 `.endef` 副本:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
endef en /path/to/project
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
只转换命中默认 marker 的文件:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
endef en /path/to/project --marker
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
手动指定 marker:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
endef en /path/to/project --marker E-SafeNet
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
指定多个 marker:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
endef en /path/to/project --marker E-SafeNet --marker BadText
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
也可以用英文逗号分隔:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
endef en /path/to/project --marker E-SafeNet,BadText
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
把 `.endef` 副本刷回原文件,并删除 `.endef`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
endef de /path/to/project
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
打包当前目录:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
endef pack
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
默认会使用当前目录名生成压缩包。例如当前目录是 `foo`,会生成:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
foo.tar.gz
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
指定输出文件名:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
endef pack recovered
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
会生成:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
recovered.tar.gz
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 命令
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
endef en [directory] [options]
|
|
103
|
+
endef de [directory] [options]
|
|
104
|
+
endef pack [name] [options]
|
|
105
|
+
endef all [directory] [options]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`en` 会扫描目标目录,把匹配后缀的文件读取出来,并写成同目录的 `.endef` 副本。
|
|
109
|
+
|
|
110
|
+
`de` 会扫描目标目录中的 `.endef` 文件,把副本内容写回原文件,然后删除 `.endef` 副本。这个命令会直接修改文件,请在执行前确认目录和备份策略。
|
|
111
|
+
|
|
112
|
+
`pack` 会按当前配置扫描当前目录,把没有被 `excludeDirs` 排除的文件打包成 `.tar.gz`。如果输出文件已存在,会直接覆盖。
|
|
113
|
+
|
|
114
|
+
`all` 会按顺序执行 `en`、`de`、`pack`,适合确认配置后一次性完成旁路转换、写回和打包。
|
|
115
|
+
|
|
116
|
+
## 参数
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
--config <file> 使用指定配置文件
|
|
120
|
+
--suffix <suffix> 旁路文件后缀,默认:.endef
|
|
121
|
+
--ext <list> 目标文件后缀,多个值用英文逗号分隔
|
|
122
|
+
--exclude <list> 排除目录名,多个值用英文逗号分隔
|
|
123
|
+
--concurrency <n> 并发文件操作数量
|
|
124
|
+
--marker [text] 只为命中任意 marker 的文件生成副本
|
|
125
|
+
--out <file> 压缩包输出名称
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
示例:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
endef en . --ext .js,.ts,.vue,.md --exclude node_modules,.git,dist
|
|
132
|
+
endef en . --marker E-SafeNet --marker BadText
|
|
133
|
+
endef de .
|
|
134
|
+
endef pack recovered
|
|
135
|
+
endef all . --marker --out recovered
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 配置文件
|
|
139
|
+
|
|
140
|
+
`endef` 会按下面的顺序读取配置,后面的配置覆盖前面的配置:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
用户目录:~/.endef/config.json
|
|
144
|
+
当前目录:.endef.json
|
|
145
|
+
命令参数:优先级最高
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
配置示例:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"suffix": ".endef",
|
|
153
|
+
"markers": ["E-SafeNet"],
|
|
154
|
+
"extensions": [
|
|
155
|
+
".js",
|
|
156
|
+
".jsx",
|
|
157
|
+
".cjs",
|
|
158
|
+
".mjs",
|
|
159
|
+
".mts",
|
|
160
|
+
".ts",
|
|
161
|
+
".tsx",
|
|
162
|
+
".vue",
|
|
163
|
+
".md",
|
|
164
|
+
".yaml",
|
|
165
|
+
".yml",
|
|
166
|
+
".json",
|
|
167
|
+
".type.ts",
|
|
168
|
+
".scss",
|
|
169
|
+
".css",
|
|
170
|
+
".html",
|
|
171
|
+
".txt",
|
|
172
|
+
".env",
|
|
173
|
+
".c",
|
|
174
|
+
".h",
|
|
175
|
+
".hpp",
|
|
176
|
+
".java",
|
|
177
|
+
".svn-base",
|
|
178
|
+
".lock",
|
|
179
|
+
".git",
|
|
180
|
+
".gitconfig",
|
|
181
|
+
".less"
|
|
182
|
+
],
|
|
183
|
+
"excludeDirs": ["node_modules", ".git", "dist", ".next", ".nuxt", ".cache"],
|
|
184
|
+
"excludeFiles": [".endef.json"],
|
|
185
|
+
"concurrency": 32
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## marker 模式
|
|
190
|
+
|
|
191
|
+
默认情况下:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
endef en .
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
会为所有匹配后缀的文件生成 `.endef` 副本。
|
|
198
|
+
|
|
199
|
+
启用 marker 模式后:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
endef en . --marker
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
只会为命中任意 marker 的文件生成 `.endef` 副本。`--marker` 不带值时使用配置中的 `markers`,默认是:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
["E-SafeNet"]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
如果 CLI 指定了 marker,则使用 CLI 的值:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
endef en . --marker E-SafeNet --marker BadText
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 恢复策略
|
|
218
|
+
|
|
219
|
+
恢复时,`endef` 会把:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
file.ext.endef
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
写回:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
file.ext
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
然后删除:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
file.ext.endef
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
如果 `file.ext` 已经存在,会被 `.endef` 副本内容覆盖。这个行为符合工具的核心假设:原文件可能已经被异常内容污染,而 `.endef` 是通过旁路读取保存出来的可恢复内容。
|
|
238
|
+
|
|
239
|
+
## 打包策略
|
|
240
|
+
|
|
241
|
+
打包命令:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
endef pack
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
默认输出会使用当前目录名。例如当前目录是 `foo`,默认输出:
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
foo.tar.gz
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
如果指定:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
endef pack baz
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
则输出:
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
baz.tar.gz
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`pack` 会沿用配置里的 `excludeDirs`,也支持命令行临时指定:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
endef pack --exclude node_modules,.git,dist
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
打包时会自动跳过:
|
|
272
|
+
|
|
273
|
+
- 被 `excludeDirs` 排除的目录
|
|
274
|
+
- 输出压缩包自身
|
|
275
|
+
- 残留的 `.endef` 临时副本
|
|
276
|
+
|
|
277
|
+
压缩包格式是 `.tar.gz`,不需要额外生产依赖。
|
|
278
|
+
|
|
279
|
+
## 一步执行
|
|
280
|
+
|
|
281
|
+
如果已经确认配置,可以使用:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
endef all .
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
它等价于依次执行:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
endef en .
|
|
291
|
+
endef de .
|
|
292
|
+
endef pack
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
同样支持 marker、排除目录和输出名称:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
endef all . --marker E-SafeNet,BadText --exclude node_modules,.git,dist --out recovered
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## 安全建议
|
|
302
|
+
|
|
303
|
+
- 操作前尽量手动备份目录或磁盘
|
|
304
|
+
- 优先在干净系统、离线环境或挂载出来的磁盘上运行
|
|
305
|
+
- 先在小目录中测试 `endef en` 和 `endef de`
|
|
306
|
+
- 不确定 marker 是否完整时,优先使用默认的全量 `endef en`
|
|
307
|
+
- 不要直接运行恢复出来的未知脚本或可执行文件
|
|
308
|
+
- 恢复后建议做杀毒、哈希校验和人工抽查
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|
package/bin/main.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// ECMAScript module loader
|
|
3
|
-
_import = require('esm')(module /*, options*/)
|
|
4
2
|
|
|
5
|
-
const { run
|
|
3
|
+
const { run } = require('../src/run')
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
run(process.argv).catch(error => {
|
|
6
|
+
console.error(error && error.message ? error.message : error)
|
|
7
|
+
process.exitCode = 1
|
|
8
|
+
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "endef",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Recover text and source files through safe .endef sidecar copies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"endef": "bin/main.js"
|
|
7
7
|
},
|
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
"bin",
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
15
|
+
"keywords": [
|
|
16
|
+
"file-recovery",
|
|
17
|
+
"sidecar",
|
|
18
|
+
"restore",
|
|
19
|
+
"cli",
|
|
20
|
+
"nodejs"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
18
24
|
},
|
|
25
|
+
"license": "MIT",
|
|
19
26
|
"devDependencies": {
|
|
20
27
|
"prettier": "^2.8.8"
|
|
21
28
|
},
|
package/src/config.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
const defaultConfig = {
|
|
6
|
+
suffix: '.endef',
|
|
7
|
+
markers: ['E-SafeNet'],
|
|
8
|
+
extensions: [
|
|
9
|
+
'.js',
|
|
10
|
+
'.jsx',
|
|
11
|
+
'.cjs',
|
|
12
|
+
'.mjs',
|
|
13
|
+
'.mts',
|
|
14
|
+
'.ts',
|
|
15
|
+
'.tsx',
|
|
16
|
+
'.vue',
|
|
17
|
+
'.md',
|
|
18
|
+
'.yaml',
|
|
19
|
+
'.yml',
|
|
20
|
+
'.json',
|
|
21
|
+
'.type.ts',
|
|
22
|
+
'.scss',
|
|
23
|
+
'.css',
|
|
24
|
+
'.html',
|
|
25
|
+
'.txt',
|
|
26
|
+
'.env',
|
|
27
|
+
'.c',
|
|
28
|
+
'.h',
|
|
29
|
+
'.hpp',
|
|
30
|
+
'.java',
|
|
31
|
+
'.svn-base',
|
|
32
|
+
'.lock',
|
|
33
|
+
'.git',
|
|
34
|
+
'.gitconfig',
|
|
35
|
+
'.less'
|
|
36
|
+
],
|
|
37
|
+
excludeDirs: ['node_modules', '.git', 'dist', '.next', '.nuxt', '.cache'],
|
|
38
|
+
excludeFiles: ['.endef.json'],
|
|
39
|
+
concurrency: 32
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadConfig(cwd, explicitConfigFile, targetDirectory) {
|
|
43
|
+
const configFiles = [path.join(os.homedir(), '.endef', 'config.json'), path.join(cwd, '.endef.json')]
|
|
44
|
+
const targetConfigFile = targetDirectory ? path.join(path.resolve(cwd, targetDirectory), '.endef.json') : ''
|
|
45
|
+
|
|
46
|
+
if (targetConfigFile && targetConfigFile !== configFiles[1]) {
|
|
47
|
+
configFiles.push(targetConfigFile)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (explicitConfigFile) {
|
|
51
|
+
configFiles.push(path.resolve(cwd, explicitConfigFile))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return configFiles.reduce(
|
|
55
|
+
(config, file) => {
|
|
56
|
+
if (!fs.existsSync(file)) {
|
|
57
|
+
return config
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const nextConfig = JSON.parse(stripBom(fs.readFileSync(file, 'utf8')))
|
|
62
|
+
return mergeConfig(config, nextConfig)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Cannot read config ${file}: ${error.message}`)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{ ...defaultConfig }
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stripBom(value) {
|
|
72
|
+
return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function mergeConfig(base, next) {
|
|
76
|
+
const markers = normalizeMarkers(next.markers)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
...next,
|
|
81
|
+
extensions: Array.isArray(next.extensions) ? next.extensions : base.extensions,
|
|
82
|
+
excludeDirs: Array.isArray(next.excludeDirs) ? next.excludeDirs : base.excludeDirs,
|
|
83
|
+
excludeFiles: Array.isArray(next.excludeFiles) ? next.excludeFiles : base.excludeFiles,
|
|
84
|
+
markers: markers.length > 0 ? markers : base.markers
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeMarkers(value) {
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
return value.map(item => String(item).trim()).filter(Boolean)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function splitMarkers(value) {
|
|
97
|
+
if (!value) {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return String(value)
|
|
102
|
+
.split(',')
|
|
103
|
+
.map(item => item.trim())
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { defaultConfig, loadConfig, normalizeMarkers, splitMarkers }
|
package/src/de.js
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const fs = require('fs/promises')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { defaultConfig } = require('./config')
|
|
4
|
+
const { createLimiter, walk } = require('./utils')
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
async function restoreFiles(directory, options = {}) {
|
|
7
|
+
const config = {
|
|
8
|
+
...defaultConfig,
|
|
9
|
+
...options,
|
|
10
|
+
directory: path.resolve(directory || process.cwd())
|
|
11
|
+
}
|
|
12
|
+
const stats = createStats()
|
|
13
|
+
const limit = createLimiter(config.concurrency)
|
|
14
|
+
const tasks = []
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
await walk(config.directory, config, async filePath => {
|
|
17
|
+
if (!filePath.endsWith(config.suffix)) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
tasks.push(
|
|
22
|
+
limit(async () => {
|
|
23
|
+
await restoreOne(filePath, config, stats)
|
|
24
|
+
}).catch(error => {
|
|
25
|
+
stats.failed += 1
|
|
26
|
+
console.error(`Failed: ${filePath}`)
|
|
27
|
+
console.error(error && error.message ? error.message : error)
|
|
28
|
+
})
|
|
29
|
+
)
|
|
12
30
|
})
|
|
31
|
+
|
|
32
|
+
await Promise.all(tasks)
|
|
33
|
+
console.log(`Done. restored=${stats.restored}, deleted=${stats.deleted}, failed=${stats.failed}`)
|
|
34
|
+
return stats
|
|
13
35
|
}
|
|
36
|
+
|
|
37
|
+
async function restoreOne(filePath, config, stats) {
|
|
38
|
+
const targetPath = filePath.slice(0, -config.suffix.length)
|
|
39
|
+
const data = await fs.readFile(filePath, 'utf8')
|
|
40
|
+
|
|
41
|
+
await fs.writeFile(targetPath, data, 'utf8')
|
|
42
|
+
await fs.rm(filePath, { force: true })
|
|
43
|
+
|
|
44
|
+
stats.restored += 1
|
|
45
|
+
stats.deleted += 1
|
|
46
|
+
console.log(`Restored: ${filePath} -> ${targetPath}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createStats() {
|
|
50
|
+
return {
|
|
51
|
+
restored: 0,
|
|
52
|
+
deleted: 0,
|
|
53
|
+
failed: 0
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = restoreFiles
|
package/src/en.js
CHANGED
|
@@ -1,54 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
const fs = require('fs/promises')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { defaultConfig } = require('./config')
|
|
4
|
+
const { createLimiter, matchesExtension, walk } = require('./utils')
|
|
5
|
+
|
|
6
|
+
async function processFiles(directory, options = {}) {
|
|
7
|
+
const config = {
|
|
8
|
+
...defaultConfig,
|
|
9
|
+
...options,
|
|
10
|
+
directory: path.resolve(directory || process.cwd())
|
|
11
|
+
}
|
|
12
|
+
const stats = createStats()
|
|
13
|
+
const limit = createLimiter(config.concurrency)
|
|
14
|
+
const tasks = []
|
|
15
|
+
|
|
16
|
+
await walk(config.directory, config, async filePath => {
|
|
17
|
+
if (config.excludeFiles.includes(path.basename(filePath))) {
|
|
18
|
+
stats.skipped += 1
|
|
19
|
+
return
|
|
9
20
|
}
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
if (!matchesExtension(filePath, config.extensions) || filePath.endsWith(config.suffix)) {
|
|
23
|
+
stats.skipped += 1
|
|
24
|
+
return
|
|
25
|
+
}
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
27
|
+
tasks.push(
|
|
28
|
+
limit(async () => {
|
|
29
|
+
const outputPath = `${filePath}${config.suffix}`
|
|
30
|
+
const data = await fs.readFile(filePath, 'utf8')
|
|
19
31
|
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
processFiles(filePath, fileExtensions, excludedDirectories); // 递归处理子目录
|
|
26
|
-
} else {
|
|
27
|
-
// 处理文件
|
|
28
|
-
const fileExtension = path.extname(filePath);
|
|
29
|
-
// 检查文件后缀名是否符合要求
|
|
30
|
-
if (fileExtensions.includes(fileExtension)) {
|
|
31
|
-
fs.readFile(filePath, 'utf8', (err, data) => {
|
|
32
|
-
if (err) {
|
|
33
|
-
console.error('Error reading file:', err);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const tempFilePath = filePath + '._$_foobar';
|
|
38
|
-
|
|
39
|
-
// 写入临时文件
|
|
40
|
-
fs.writeFile(tempFilePath, data, 'utf8', err => {
|
|
41
|
-
if (err) {
|
|
42
|
-
console.error('Error writing temp file:', err);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log('Temp file written:', tempFilePath);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
}
|
|
32
|
+
if (config.markerFilter && !matchesAnyMarker(data, config.markers)) {
|
|
33
|
+
stats.skipped += 1
|
|
34
|
+
return
|
|
50
35
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
|
|
37
|
+
await fs.writeFile(outputPath, data, 'utf8')
|
|
38
|
+
console.log(`Written: ${outputPath}`)
|
|
39
|
+
stats.written += 1
|
|
40
|
+
}).catch(error => {
|
|
41
|
+
stats.failed += 1
|
|
42
|
+
console.error(`Failed: ${filePath}`)
|
|
43
|
+
console.error(error && error.message ? error.message : error)
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await Promise.all(tasks)
|
|
49
|
+
console.log(`Done. written=${stats.written}, skipped=${stats.skipped}, failed=${stats.failed}`)
|
|
50
|
+
return stats
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createStats() {
|
|
54
|
+
return {
|
|
55
|
+
written: 0,
|
|
56
|
+
skipped: 0,
|
|
57
|
+
failed: 0
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function matchesAnyMarker(data, markers) {
|
|
62
|
+
if (!Array.isArray(markers) || markers.length === 0) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return markers.some(marker => data.includes(marker))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = processFiles
|
package/src/pack.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const fsp = require('fs/promises')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const zlib = require('zlib')
|
|
5
|
+
const { once } = require('events')
|
|
6
|
+
const { pipeline } = require('stream/promises')
|
|
7
|
+
const { defaultConfig } = require('./config')
|
|
8
|
+
const { walk } = require('./utils')
|
|
9
|
+
|
|
10
|
+
async function packFiles(directory, options = {}) {
|
|
11
|
+
const config = {
|
|
12
|
+
...defaultConfig,
|
|
13
|
+
...options,
|
|
14
|
+
directory: path.resolve(directory || process.cwd())
|
|
15
|
+
}
|
|
16
|
+
const outputPath = resolveOutputPath(config.directory, config.out)
|
|
17
|
+
const files = []
|
|
18
|
+
|
|
19
|
+
await walk(config.directory, config, async filePath => {
|
|
20
|
+
if (path.resolve(filePath) === outputPath || filePath.endsWith(config.suffix)) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
files.push(filePath)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await fsp.rm(outputPath, { force: true })
|
|
28
|
+
await writeTarGz(config.directory, outputPath, files)
|
|
29
|
+
console.log(`Packed: ${outputPath}`)
|
|
30
|
+
console.log(`Done. files=${files.length}`)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
outputPath,
|
|
34
|
+
files: files.length
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveOutputPath(directory, out) {
|
|
39
|
+
const outputName = out ? normalizeOutputName(out) : defaultArchiveName(directory)
|
|
40
|
+
const outputPath = path.isAbsolute(outputName) ? outputName : path.join(directory, outputName)
|
|
41
|
+
|
|
42
|
+
return path.resolve(outputPath)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultArchiveName(directory) {
|
|
46
|
+
const name = path.basename(path.resolve(directory))
|
|
47
|
+
return `${name || 'endef'}.tar.gz`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeOutputName(out) {
|
|
51
|
+
return `${out}.tar.gz`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function writeTarGz(rootDirectory, outputPath, files) {
|
|
55
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true })
|
|
56
|
+
|
|
57
|
+
const gzip = zlib.createGzip()
|
|
58
|
+
const output = fs.createWriteStream(outputPath)
|
|
59
|
+
const writer = writeTarEntries(rootDirectory, files, gzip)
|
|
60
|
+
|
|
61
|
+
await Promise.all([writer, pipeline(gzip, output)])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function writeTarEntries(rootDirectory, files, stream) {
|
|
65
|
+
for (const filePath of files) {
|
|
66
|
+
const stat = await fsp.stat(filePath)
|
|
67
|
+
const archivePath = toArchivePath(path.relative(rootDirectory, filePath))
|
|
68
|
+
|
|
69
|
+
await writeToStream(stream, createHeader(archivePath, stat))
|
|
70
|
+
|
|
71
|
+
await writeFileToStream(filePath, stream)
|
|
72
|
+
await writeToStream(stream, Buffer.alloc(paddingSize(stat.size)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stream.end(Buffer.alloc(1024))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function writeFileToStream(filePath, stream) {
|
|
79
|
+
for await (const chunk of fs.createReadStream(filePath)) {
|
|
80
|
+
await writeToStream(stream, chunk)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function writeToStream(stream, chunk) {
|
|
85
|
+
if (!stream.write(chunk)) {
|
|
86
|
+
await once(stream, 'drain')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toArchivePath(filePath) {
|
|
91
|
+
return filePath.split(path.sep).join('/')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createHeader(filePath, stat) {
|
|
95
|
+
const header = Buffer.alloc(512, 0)
|
|
96
|
+
const tarPath = splitTarPath(filePath)
|
|
97
|
+
|
|
98
|
+
writeString(header, tarPath.name, 0, 100)
|
|
99
|
+
writeOctal(header, 0o644, 100, 8)
|
|
100
|
+
writeOctal(header, 0, 108, 8)
|
|
101
|
+
writeOctal(header, 0, 116, 8)
|
|
102
|
+
writeOctal(header, stat.size, 124, 12)
|
|
103
|
+
writeOctal(header, Math.floor(stat.mtimeMs / 1000), 136, 12)
|
|
104
|
+
header.fill(0x20, 148, 156)
|
|
105
|
+
header[156] = '0'.charCodeAt(0)
|
|
106
|
+
writeString(header, 'ustar', 257, 6)
|
|
107
|
+
writeString(header, '00', 263, 2)
|
|
108
|
+
writeString(header, tarPath.prefix, 345, 155)
|
|
109
|
+
|
|
110
|
+
const checksum = header.reduce((sum, value) => sum + value, 0)
|
|
111
|
+
writeOctal(header, checksum, 148, 8)
|
|
112
|
+
|
|
113
|
+
return header
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function splitTarPath(filePath) {
|
|
117
|
+
if (Buffer.byteLength(filePath) <= 100) {
|
|
118
|
+
return {
|
|
119
|
+
name: filePath,
|
|
120
|
+
prefix: ''
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const parts = filePath.split('/')
|
|
125
|
+
|
|
126
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
127
|
+
const prefix = parts.slice(0, index).join('/')
|
|
128
|
+
const name = parts.slice(index).join('/')
|
|
129
|
+
|
|
130
|
+
if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) {
|
|
131
|
+
return { name, prefix }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new Error(`Tar path is too long: ${filePath}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function writeString(buffer, value, offset, length) {
|
|
139
|
+
const data = Buffer.from(value)
|
|
140
|
+
|
|
141
|
+
if (data.length > length) {
|
|
142
|
+
throw new Error(`Tar header field is too long: ${value}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
data.copy(buffer, offset)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function writeOctal(buffer, value, offset, length) {
|
|
149
|
+
const data = value.toString(8).padStart(length - 1, '0') + '\0'
|
|
150
|
+
buffer.write(data, offset, length, 'ascii')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function paddingSize(size) {
|
|
154
|
+
const remainder = size % 512
|
|
155
|
+
return remainder === 0 ? 0 : 512 - remainder
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = packFiles
|
package/src/run.js
CHANGED
|
@@ -1,12 +1,202 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const enF = require('./en')
|
|
2
|
+
const deF = require('./de')
|
|
3
|
+
const packF = require('./pack')
|
|
4
|
+
const { loadConfig, splitMarkers } = require('./config')
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(`Usage:
|
|
8
|
+
endef en [directory] [options]
|
|
9
|
+
endef de [directory] [options]
|
|
10
|
+
endef pack [name] [options]
|
|
11
|
+
endef all [directory] [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--config <file> Use a specific config file
|
|
15
|
+
--suffix <suffix> Sidecar suffix, default: .endef
|
|
16
|
+
--ext <list> Comma separated target extensions
|
|
17
|
+
--exclude <list> Comma separated excluded directory names
|
|
18
|
+
--concurrency <n> Concurrent file operation limit
|
|
19
|
+
--marker [text] Only create sidecar files when any marker is found
|
|
20
|
+
--out <file> Archive output name
|
|
21
|
+
`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseArgs(command, args) {
|
|
25
|
+
const result = {
|
|
26
|
+
directory: process.cwd(),
|
|
27
|
+
markerFilter: undefined,
|
|
28
|
+
markers: [],
|
|
29
|
+
positionals: []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
33
|
+
const arg = args[index]
|
|
34
|
+
|
|
35
|
+
if (arg.startsWith('--suffix=')) {
|
|
36
|
+
result.suffix = arg.slice('--suffix='.length)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (arg === '--suffix') {
|
|
41
|
+
result.suffix = args[index + 1]
|
|
42
|
+
index += 1
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (arg.startsWith('--config=')) {
|
|
47
|
+
result.configFile = arg.slice('--config='.length)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (arg === '--config') {
|
|
52
|
+
result.configFile = args[index + 1]
|
|
53
|
+
index += 1
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (arg.startsWith('--ext=')) {
|
|
58
|
+
result.extensions = splitList(arg.slice('--ext='.length))
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (arg === '--ext') {
|
|
63
|
+
result.extensions = splitList(args[index + 1])
|
|
64
|
+
index += 1
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (arg.startsWith('--exclude=')) {
|
|
69
|
+
result.excludeDirs = splitList(arg.slice('--exclude='.length))
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (arg === '--exclude') {
|
|
74
|
+
result.excludeDirs = splitList(args[index + 1])
|
|
75
|
+
index += 1
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (arg.startsWith('--concurrency=')) {
|
|
80
|
+
result.concurrency = Number(arg.slice('--concurrency='.length))
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (arg.startsWith('--out=')) {
|
|
85
|
+
result.out = arg.slice('--out='.length)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (arg === '--out') {
|
|
90
|
+
result.out = args[index + 1]
|
|
91
|
+
index += 1
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (arg === '--concurrency') {
|
|
96
|
+
result.concurrency = Number(args[index + 1])
|
|
97
|
+
index += 1
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (arg.startsWith('--marker=')) {
|
|
102
|
+
result.markerFilter = true
|
|
103
|
+
result.markers.push(...splitMarkers(arg.slice('--marker='.length)))
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg === '--marker') {
|
|
108
|
+
result.markerFilter = true
|
|
109
|
+
|
|
110
|
+
if (args[index + 1] && !args[index + 1].startsWith('-')) {
|
|
111
|
+
result.markers.push(...splitMarkers(args[index + 1]))
|
|
112
|
+
index += 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!arg.startsWith('-')) {
|
|
119
|
+
result.positionals.push(arg)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
applyPositionals(command, result)
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function applyPositionals(command, result) {
|
|
128
|
+
if (command === 'pack') {
|
|
129
|
+
result.out = result.out || result.positionals[0]
|
|
130
|
+
delete result.positionals
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
result.directory = result.positionals[0] || result.directory
|
|
135
|
+
delete result.positionals
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function splitList(value) {
|
|
139
|
+
if (!value) {
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return value
|
|
144
|
+
.split(',')
|
|
145
|
+
.map(item => item.trim())
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function run(args) {
|
|
150
|
+
const command = args[2]
|
|
151
|
+
|
|
152
|
+
if (!command || command === '-h' || command === '--help') {
|
|
153
|
+
printHelp()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parsed = parseArgs(command, args.slice(3))
|
|
158
|
+
const config = loadConfig(process.cwd(), parsed.configFile, parsed.directory)
|
|
159
|
+
const cliOptions = omitUndefined(parsed)
|
|
160
|
+
const options = {
|
|
161
|
+
...config,
|
|
162
|
+
...cliOptions,
|
|
163
|
+
directory: cliOptions.directory || config.directory || process.cwd(),
|
|
164
|
+
markers: cliOptions.markers && cliOptions.markers.length > 0 ? cliOptions.markers : config.markers
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (command === 'en') {
|
|
168
|
+
await enF(options.directory, options)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (command === 'de') {
|
|
173
|
+
await deF(options.directory, options)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (command === 'pack') {
|
|
178
|
+
await packF(options.directory, options)
|
|
179
|
+
return
|
|
7
180
|
}
|
|
8
181
|
|
|
9
|
-
if (
|
|
10
|
-
|
|
182
|
+
if (command === 'all') {
|
|
183
|
+
await enF(options.directory, options)
|
|
184
|
+
await deF(options.directory, options)
|
|
185
|
+
await packF(options.directory, options)
|
|
186
|
+
return
|
|
11
187
|
}
|
|
188
|
+
|
|
189
|
+
printHelp()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function omitUndefined(value) {
|
|
193
|
+
return Object.keys(value).reduce((result, key) => {
|
|
194
|
+
if (value[key] !== undefined) {
|
|
195
|
+
result[key] = value[key]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
}, {})
|
|
12
200
|
}
|
|
201
|
+
|
|
202
|
+
module.exports = { run }
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const fs = require('fs/promises')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
async function walk(directory, config, onFile) {
|
|
5
|
+
let entries
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
entries = await fs.opendir(directory)
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(`Cannot read directory: ${directory}`)
|
|
11
|
+
console.error(error && error.message ? error.message : error)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for await (const entry of entries) {
|
|
16
|
+
const filePath = path.join(directory, entry.name)
|
|
17
|
+
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (config.excludeDirs.includes(entry.name)) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await walk(filePath, config, onFile)
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (entry.isFile()) {
|
|
28
|
+
await onFile(filePath)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesExtension(filePath, extensions) {
|
|
34
|
+
const normalizedPath = filePath.toLowerCase()
|
|
35
|
+
return extensions.some(extension => normalizedPath.endsWith(extension.toLowerCase()))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createLimiter(concurrency) {
|
|
39
|
+
const max = Number.isFinite(concurrency) && concurrency > 0 ? concurrency : 32
|
|
40
|
+
const queue = []
|
|
41
|
+
let active = 0
|
|
42
|
+
|
|
43
|
+
return function limit(task) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
queue.push({ task, resolve, reject })
|
|
46
|
+
runNext()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runNext() {
|
|
51
|
+
if (active >= max || queue.length === 0) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const item = queue.shift()
|
|
56
|
+
active += 1
|
|
57
|
+
|
|
58
|
+
Promise.resolve()
|
|
59
|
+
.then(item.task)
|
|
60
|
+
.then(item.resolve, item.reject)
|
|
61
|
+
.finally(() => {
|
|
62
|
+
active -= 1
|
|
63
|
+
runNext()
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { walk, matchesExtension, createLimiter }
|
package/src/de.sh
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# 处理文件名并重命名
|
|
4
|
-
process_filename() {
|
|
5
|
-
local file=$1
|
|
6
|
-
local filename=$(basename "$file")
|
|
7
|
-
local dir=$(dirname "$file")
|
|
8
|
-
|
|
9
|
-
# 判断文件名是否以指定字符串结尾
|
|
10
|
-
if [[ $filename =~ \._\$_foobar$ ]]; then
|
|
11
|
-
local new_filename=${filename%%._$_foobar*} # 移除指定字符串
|
|
12
|
-
local new_path="$dir/$new_filename" # 构建新的文件路径
|
|
13
|
-
|
|
14
|
-
# 判断新文件名对应的文件是否存在
|
|
15
|
-
if [[ -f "$new_path" ]]; then
|
|
16
|
-
if grep -q "E-SafeNet" "$new_path" || [[ $2 == "over" ]]; then
|
|
17
|
-
rm "$new_path" # 删除包含特定内容的文件
|
|
18
|
-
mv "$file" "$new_path" # 重命名文件
|
|
19
|
-
echo "Renamed: $filename -> $new_filename"
|
|
20
|
-
else
|
|
21
|
-
rm "$file" # 删除不包含特定内容的文件
|
|
22
|
-
echo "Deleted: $filename"
|
|
23
|
-
fi
|
|
24
|
-
fi
|
|
25
|
-
fi
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
# 指定目标目录
|
|
29
|
-
directory="$1" # 替换为您的目录路径
|
|
30
|
-
|
|
31
|
-
# 导出函数以便在并行处理中使用
|
|
32
|
-
export -f process_filename
|
|
33
|
-
|
|
34
|
-
# 处理文件名并重命名(并行处理)
|
|
35
|
-
find "$directory" -type f -print0 | \
|
|
36
|
-
xargs -0 -P "$(nproc)" -I{} bash -c 'process_filename "$@"' _ {} "$2"
|