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 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: start } = _import('../src/run')
3
+ const { run } = require('../src/run')
6
4
 
7
- start(process.argv)
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": "1.0.9",
4
- "description": "Encrypt or Decrypt Files Test",
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
- "license": "MIT",
16
- "dependencies": {
17
- "esm": "^3.2.25"
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
- import { spawn } from 'child_process'
2
- import { resolve } from 'path'
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
- export const deSrc = resolve(__dirname)
5
- export const curDir = process.cwd()
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
- export default function (args3) {
8
- spawn(`bash`, [`${deSrc}\/de.sh ${curDir} ${args3}`], {
9
- cwd: curDir,
10
- stdio: 'inherit',
11
- shell: process.platform === 'win32'
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
- import fs from 'fs';
2
- import path from 'path';
3
-
4
- export default function processFiles(directory, fileExtensions, excludedDirectories = []) {
5
- fs.readdir(directory, (err, files) => {
6
- if (err) {
7
- console.error('Error reading directory:', err);
8
- return;
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
- files.forEach(file => {
12
- const filePath = path.join(directory, file);
22
+ if (!matchesExtension(filePath, config.extensions) || filePath.endsWith(config.suffix)) {
23
+ stats.skipped += 1
24
+ return
25
+ }
13
26
 
14
- fs.stat(filePath, (err, stats) => {
15
- if (err) {
16
- console.error('Error stating file:', err);
17
- return;
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 (stats.isDirectory()) {
21
- // 排除指定的文件夹
22
- if (excludedDirectories.includes(file)) {
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
- import enF from './en'
2
- import deF from './de'
1
+ const enF = require('./en')
2
+ const deF = require('./de')
3
+ const packF = require('./pack')
4
+ const { loadConfig, splitMarkers } = require('./config')
3
5
 
4
- export async function run(args) {
5
- if (args[2] === 'en') {
6
- enF(process.cwd(), ['.js', '.jsx', '.cjs', '.mjs', '.mts', '.ts', '.tsx', '.vue', '.md', '.yaml', '.type.ts', '.scss'], ['node_modules', '.git'])
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 (args[2] === 'de') {
10
- deF(args[3] || 'none')
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"