endef 2.0.1 → 2.0.3
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 +10 -67
- package/package.json +1 -1
- package/src/config.js +2 -25
- package/src/de.js +33 -55
- package/src/en.js +2 -15
- package/src/pack.js +144 -20
- package/src/run.js +26 -25
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`endef` 是一个纯 Node.js 的文件旁路转换工具。
|
|
4
4
|
|
|
5
|
-
它适合这样的场景:某些常见源码或文本后缀的文件在系统读取、复制、粘贴、编辑器打开时出现乱码或异常,但通过 Node.js 脚本读取仍然正常。`endef` 会先把目标文件内容写到同目录的 `.endef`
|
|
5
|
+
它适合这样的场景:某些常见源码或文本后缀的文件在系统读取、复制、粘贴、编辑器打开时出现乱码或异常,但通过 Node.js 脚本读取仍然正常。`endef` 会先把目标文件内容写到同目录的 `.endef` 副本里,再通过临时 `.tar.gz` 恢复包解压回原文件名。
|
|
6
6
|
|
|
7
7
|
示例:
|
|
8
8
|
|
|
@@ -19,8 +19,7 @@ README.md -> README.md.endef -> README.md
|
|
|
19
19
|
- 纯 Node.js 实现,不依赖 Bash、find、xargs、grep 等 shell 工具
|
|
20
20
|
- 默认旁路后缀为 `.endef`
|
|
21
21
|
- `en` 默认转换所有匹配后缀的文件
|
|
22
|
-
- `
|
|
23
|
-
- `de` 会删除原文件,并把 `.endef` 临时副本重命名回原文件名
|
|
22
|
+
- `de` 会把 `.endef` 临时副本做成恢复包,解压回原文件名,再删除 `.endef`
|
|
24
23
|
- `pack` 会把恢复后的目录打包成 `.tar.gz`
|
|
25
24
|
- 不引入生产依赖
|
|
26
25
|
|
|
@@ -42,31 +41,7 @@ npm install -g endef
|
|
|
42
41
|
endef en /path/to/project
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
|
|
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` 副本重命名回原文件名:
|
|
44
|
+
通过临时恢复包解压回原文件名,并删除 `.endef`:
|
|
70
45
|
|
|
71
46
|
```bash
|
|
72
47
|
endef de /path/to/project
|
|
@@ -107,11 +82,11 @@ endef all [directory] [options]
|
|
|
107
82
|
|
|
108
83
|
`en` 会扫描目标目录,把匹配后缀的文件读取出来,并写成同目录的 `.endef` 副本。
|
|
109
84
|
|
|
110
|
-
`de` 会扫描目标目录中的 `.endef`
|
|
85
|
+
`de` 会扫描目标目录中的 `.endef` 文件,生成一个临时恢复包,把 `.endef` 内容映射回原文件名并解压,然后删除 `.endef` 副本。这个命令会直接修改文件,请在执行前确认目录和备份策略。
|
|
111
86
|
|
|
112
87
|
`pack` 会按当前配置扫描当前目录,把没有被 `excludeDirs` 排除的文件打包成 `.tar.gz`。如果输出文件已存在,会直接覆盖。
|
|
113
88
|
|
|
114
|
-
`all`
|
|
89
|
+
`all` 会先执行 `en`,然后生成一次最终恢复包;这个包既会被解压回目录,也会作为最终 `.tar.gz` 保留下来,避免 `de` 和 `pack` 重复打包。
|
|
115
90
|
|
|
116
91
|
## 参数
|
|
117
92
|
|
|
@@ -121,7 +96,6 @@ endef all [directory] [options]
|
|
|
121
96
|
--ext <list> 目标文件后缀,多个值用英文逗号分隔
|
|
122
97
|
--exclude <list> 排除目录名,多个值用英文逗号分隔
|
|
123
98
|
--concurrency <n> 并发文件操作数量
|
|
124
|
-
--marker [text] 只为命中任意 marker 的文件生成副本
|
|
125
99
|
--out <file> 压缩包输出名称
|
|
126
100
|
```
|
|
127
101
|
|
|
@@ -129,10 +103,9 @@ endef all [directory] [options]
|
|
|
129
103
|
|
|
130
104
|
```bash
|
|
131
105
|
endef en . --ext .js,.ts,.vue,.md --exclude node_modules,.git,dist
|
|
132
|
-
endef en . --marker E-SafeNet --marker BadText
|
|
133
106
|
endef de .
|
|
134
107
|
endef pack recovered
|
|
135
|
-
endef all . --
|
|
108
|
+
endef all . --out recovered
|
|
136
109
|
```
|
|
137
110
|
|
|
138
111
|
## 配置文件
|
|
@@ -150,7 +123,6 @@ endef all . --marker --out recovered
|
|
|
150
123
|
```json
|
|
151
124
|
{
|
|
152
125
|
"suffix": ".endef",
|
|
153
|
-
"markers": ["E-SafeNet"],
|
|
154
126
|
"extensions": [
|
|
155
127
|
".js",
|
|
156
128
|
".jsx",
|
|
@@ -186,34 +158,6 @@ endef all . --marker --out recovered
|
|
|
186
158
|
}
|
|
187
159
|
```
|
|
188
160
|
|
|
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
161
|
## 恢复策略
|
|
218
162
|
|
|
219
163
|
恢复时,`endef` 会把:
|
|
@@ -222,13 +166,13 @@ endef en . --marker E-SafeNet --marker BadText
|
|
|
222
166
|
file.ext.endef
|
|
223
167
|
```
|
|
224
168
|
|
|
225
|
-
|
|
169
|
+
通过临时恢复包解压为:
|
|
226
170
|
|
|
227
171
|
```text
|
|
228
172
|
file.ext
|
|
229
173
|
```
|
|
230
174
|
|
|
231
|
-
|
|
175
|
+
`de` 会先生成临时 `.tar.gz` 恢复包,包内路径使用原文件名;随后解压这个恢复包覆盖原文件,解压成功后删除 `file.ext.endef` 和临时恢复包。这个行为符合工具的核心假设:原文件可能已经被异常内容污染,而 `.endef` 是通过旁路读取保存出来的可恢复内容。
|
|
232
176
|
|
|
233
177
|
## 打包策略
|
|
234
178
|
|
|
@@ -286,10 +230,10 @@ endef de .
|
|
|
286
230
|
endef pack
|
|
287
231
|
```
|
|
288
232
|
|
|
289
|
-
|
|
233
|
+
同样支持排除目录和输出名称:
|
|
290
234
|
|
|
291
235
|
```bash
|
|
292
|
-
endef all . --
|
|
236
|
+
endef all . --exclude node_modules,.git,dist --out recovered
|
|
293
237
|
```
|
|
294
238
|
|
|
295
239
|
## 安全建议
|
|
@@ -297,7 +241,6 @@ endef all . --marker E-SafeNet,BadText --exclude node_modules,.git,dist --out re
|
|
|
297
241
|
- 操作前尽量手动备份目录或磁盘
|
|
298
242
|
- 优先在干净系统、离线环境或挂载出来的磁盘上运行
|
|
299
243
|
- 先在小目录中测试 `endef en` 和 `endef de`
|
|
300
|
-
- 不确定 marker 是否完整时,优先使用默认的全量 `endef en`
|
|
301
244
|
- 不要直接运行恢复出来的未知脚本或可执行文件
|
|
302
245
|
- 恢复后建议做杀毒、哈希校验和人工抽查
|
|
303
246
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -4,7 +4,6 @@ const path = require('path')
|
|
|
4
4
|
|
|
5
5
|
const defaultConfig = {
|
|
6
6
|
suffix: '.endef',
|
|
7
|
-
markers: ['E-SafeNet'],
|
|
8
7
|
extensions: [
|
|
9
8
|
'.js',
|
|
10
9
|
'.jsx',
|
|
@@ -73,35 +72,13 @@ function stripBom(value) {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
function mergeConfig(base, next) {
|
|
76
|
-
const markers = normalizeMarkers(next.markers)
|
|
77
|
-
|
|
78
75
|
return {
|
|
79
76
|
...base,
|
|
80
77
|
...next,
|
|
81
78
|
extensions: Array.isArray(next.extensions) ? next.extensions : base.extensions,
|
|
82
79
|
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
|
|
80
|
+
excludeFiles: Array.isArray(next.excludeFiles) ? next.excludeFiles : base.excludeFiles
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
|
|
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 }
|
|
84
|
+
module.exports = { defaultConfig, loadConfig }
|
package/src/de.js
CHANGED
|
@@ -1,69 +1,47 @@
|
|
|
1
1
|
const fs = require('fs/promises')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { defaultConfig } = require('./config')
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
collectRecoveryEntries,
|
|
6
|
+
createPackConfig,
|
|
7
|
+
extractTarGz,
|
|
8
|
+
removeSidecars,
|
|
9
|
+
writeTarGz
|
|
10
|
+
} = require('./pack')
|
|
5
11
|
|
|
6
12
|
async function restoreFiles(directory, options = {}) {
|
|
7
|
-
const config = {
|
|
13
|
+
const config = createPackConfig(directory, {
|
|
8
14
|
...defaultConfig,
|
|
9
|
-
...options
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
)
|
|
15
|
+
...options
|
|
16
|
+
})
|
|
17
|
+
const archivePath = path.join(config.directory, `.endef-restore-${Date.now()}.tar.gz`)
|
|
18
|
+
const entries = await collectRecoveryEntries(config, {
|
|
19
|
+
onlySidecars: true,
|
|
20
|
+
outputPath: archivePath
|
|
30
21
|
})
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const targetExists = await exists(targetPath)
|
|
40
|
-
|
|
41
|
-
await fs.rm(targetPath, { force: true })
|
|
42
|
-
await fs.rename(filePath, targetPath)
|
|
43
|
-
|
|
44
|
-
stats.restored += 1
|
|
45
|
-
if (targetExists) {
|
|
46
|
-
stats.removed += 1
|
|
23
|
+
if (entries.length === 0) {
|
|
24
|
+
console.log('Done. restored=0, deleted=0, failed=0')
|
|
25
|
+
return {
|
|
26
|
+
restored: 0,
|
|
27
|
+
deleted: 0,
|
|
28
|
+
failed: 0
|
|
29
|
+
}
|
|
47
30
|
}
|
|
48
|
-
stats.renamed += 1
|
|
49
|
-
console.log(`Restored: ${filePath} -> ${targetPath}`)
|
|
50
|
-
}
|
|
51
31
|
|
|
52
|
-
async function exists(filePath) {
|
|
53
32
|
try {
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
failed: 0
|
|
33
|
+
await writeTarGz(config.directory, archivePath, entries)
|
|
34
|
+
const restored = await extractTarGz(archivePath, config.directory)
|
|
35
|
+
const deleted = await removeSidecars(config.directory, config)
|
|
36
|
+
|
|
37
|
+
console.log(`Done. restored=${restored}, deleted=${deleted}, failed=0`)
|
|
38
|
+
return {
|
|
39
|
+
restored,
|
|
40
|
+
deleted,
|
|
41
|
+
failed: 0
|
|
42
|
+
}
|
|
43
|
+
} finally {
|
|
44
|
+
await fs.rm(archivePath, { force: true })
|
|
67
45
|
}
|
|
68
46
|
}
|
|
69
47
|
|
package/src/en.js
CHANGED
|
@@ -27,14 +27,9 @@ async function processFiles(directory, options = {}) {
|
|
|
27
27
|
tasks.push(
|
|
28
28
|
limit(async () => {
|
|
29
29
|
const outputPath = `${filePath}${config.suffix}`
|
|
30
|
-
const data = await fs.readFile(filePath
|
|
30
|
+
const data = await fs.readFile(filePath)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
stats.skipped += 1
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await fs.writeFile(outputPath, data, 'utf8')
|
|
32
|
+
await fs.writeFile(outputPath, data)
|
|
38
33
|
console.log(`Written: ${outputPath}`)
|
|
39
34
|
stats.written += 1
|
|
40
35
|
}).catch(error => {
|
|
@@ -58,12 +53,4 @@ function createStats() {
|
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
|
|
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
56
|
module.exports = processFiles
|
package/src/pack.js
CHANGED
|
@@ -2,36 +2,93 @@ const fs = require('fs')
|
|
|
2
2
|
const fsp = require('fs/promises')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const zlib = require('zlib')
|
|
5
|
+
const { promisify } = require('util')
|
|
5
6
|
const { once } = require('events')
|
|
6
7
|
const { pipeline } = require('stream/promises')
|
|
7
8
|
const { defaultConfig } = require('./config')
|
|
8
9
|
const { walk } = require('./utils')
|
|
9
10
|
|
|
11
|
+
const gunzip = promisify(zlib.gunzip)
|
|
12
|
+
|
|
10
13
|
async function packFiles(directory, options = {}) {
|
|
11
|
-
const config =
|
|
14
|
+
const config = createPackConfig(directory, options)
|
|
15
|
+
const outputPath = resolveOutputPath(config.directory, config.out)
|
|
16
|
+
const entries = await collectPackEntries(config, outputPath)
|
|
17
|
+
|
|
18
|
+
await fsp.rm(outputPath, { force: true })
|
|
19
|
+
await writeTarGz(config.directory, outputPath, entries)
|
|
20
|
+
console.log(`Packed: ${outputPath}`)
|
|
21
|
+
console.log(`Done. files=${entries.length}`)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
outputPath,
|
|
25
|
+
files: entries.length
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createPackConfig(directory, options) {
|
|
30
|
+
return {
|
|
12
31
|
...defaultConfig,
|
|
13
32
|
...options,
|
|
14
33
|
directory: path.resolve(directory || process.cwd())
|
|
15
34
|
}
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function collectPackEntries(config, outputPath) {
|
|
38
|
+
const entries = []
|
|
18
39
|
|
|
19
40
|
await walk(config.directory, config, async filePath => {
|
|
20
|
-
if (
|
|
41
|
+
if (shouldSkipPackFile(filePath, config, outputPath)) {
|
|
21
42
|
return
|
|
22
43
|
}
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
entries.push(createEntry(config.directory, filePath, filePath))
|
|
25
46
|
})
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
return entries
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function collectRecoveryEntries(config, options = {}) {
|
|
52
|
+
const entries = []
|
|
53
|
+
const sidecarTargets = new Set()
|
|
54
|
+
|
|
55
|
+
await walk(config.directory, config, async filePath => {
|
|
56
|
+
if (filePath.endsWith(config.suffix)) {
|
|
57
|
+
sidecarTargets.add(path.resolve(filePath.slice(0, -config.suffix.length)))
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await walk(config.directory, config, async filePath => {
|
|
62
|
+
const resolvedPath = path.resolve(filePath)
|
|
63
|
+
|
|
64
|
+
if (options.outputPath && resolvedPath === path.resolve(options.outputPath)) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (filePath.endsWith(config.suffix)) {
|
|
69
|
+
const targetPath = filePath.slice(0, -config.suffix.length)
|
|
70
|
+
entries.push(createEntry(config.directory, filePath, targetPath))
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.onlySidecars || sidecarTargets.has(resolvedPath)) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
entries.push(createEntry(config.directory, filePath, filePath))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return entries
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function shouldSkipPackFile(filePath, config, outputPath) {
|
|
85
|
+
return path.resolve(filePath) === outputPath || filePath.endsWith(config.suffix)
|
|
86
|
+
}
|
|
31
87
|
|
|
88
|
+
function createEntry(rootDirectory, sourcePath, archiveTargetPath) {
|
|
32
89
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
90
|
+
sourcePath,
|
|
91
|
+
archivePath: toArchivePath(path.relative(rootDirectory, archiveTargetPath))
|
|
35
92
|
}
|
|
36
93
|
}
|
|
37
94
|
|
|
@@ -51,30 +108,77 @@ function normalizeOutputName(out) {
|
|
|
51
108
|
return `${out}.tar.gz`
|
|
52
109
|
}
|
|
53
110
|
|
|
54
|
-
async function writeTarGz(rootDirectory, outputPath,
|
|
111
|
+
async function writeTarGz(rootDirectory, outputPath, entries) {
|
|
55
112
|
await fsp.mkdir(path.dirname(outputPath), { recursive: true })
|
|
56
113
|
|
|
57
114
|
const gzip = zlib.createGzip()
|
|
58
115
|
const output = fs.createWriteStream(outputPath)
|
|
59
|
-
const writer = writeTarEntries(
|
|
116
|
+
const writer = writeTarEntries(entries, gzip)
|
|
60
117
|
|
|
61
118
|
await Promise.all([writer, pipeline(gzip, output)])
|
|
62
119
|
}
|
|
63
120
|
|
|
64
|
-
async function writeTarEntries(
|
|
65
|
-
for (const
|
|
66
|
-
const stat = await fsp.stat(
|
|
67
|
-
const archivePath = toArchivePath(path.relative(rootDirectory, filePath))
|
|
68
|
-
|
|
69
|
-
await writeToStream(stream, createHeader(archivePath, stat))
|
|
121
|
+
async function writeTarEntries(entries, stream) {
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const stat = await fsp.stat(entry.sourcePath)
|
|
70
124
|
|
|
71
|
-
await
|
|
125
|
+
await writeToStream(stream, createHeader(entry.archivePath, stat))
|
|
126
|
+
await writeFileToStream(entry.sourcePath, stream)
|
|
72
127
|
await writeToStream(stream, Buffer.alloc(paddingSize(stat.size)))
|
|
73
128
|
}
|
|
74
129
|
|
|
75
130
|
stream.end(Buffer.alloc(1024))
|
|
76
131
|
}
|
|
77
132
|
|
|
133
|
+
async function extractTarGz(archivePath, directory) {
|
|
134
|
+
const archive = await fsp.readFile(archivePath)
|
|
135
|
+
const data = await gunzip(archive)
|
|
136
|
+
let offset = 0
|
|
137
|
+
let extracted = 0
|
|
138
|
+
|
|
139
|
+
while (offset + 512 <= data.length) {
|
|
140
|
+
const header = data.subarray(offset, offset + 512)
|
|
141
|
+
|
|
142
|
+
if (header.every(value => value === 0)) {
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const entry = readHeader(header)
|
|
147
|
+
offset += 512
|
|
148
|
+
|
|
149
|
+
const content = data.subarray(offset, offset + entry.size)
|
|
150
|
+
const outputPath = path.resolve(directory, entry.name)
|
|
151
|
+
|
|
152
|
+
if (!outputPath.startsWith(path.resolve(directory) + path.sep)) {
|
|
153
|
+
throw new Error(`Unsafe archive path: ${entry.name}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await fsp.mkdir(path.dirname(outputPath), { recursive: true })
|
|
157
|
+
await fsp.rm(outputPath, { force: true })
|
|
158
|
+
await fsp.writeFile(outputPath, content, { flag: 'wx' })
|
|
159
|
+
|
|
160
|
+
offset += entry.size + paddingSize(entry.size)
|
|
161
|
+
extracted += 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return extracted
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function removeSidecars(directory, config) {
|
|
168
|
+
let deleted = 0
|
|
169
|
+
|
|
170
|
+
await walk(directory, config, async filePath => {
|
|
171
|
+
if (!filePath.endsWith(config.suffix)) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await fsp.rm(filePath, { force: true })
|
|
176
|
+
deleted += 1
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
return deleted
|
|
180
|
+
}
|
|
181
|
+
|
|
78
182
|
async function writeFileToStream(filePath, stream) {
|
|
79
183
|
for await (const chunk of fs.createReadStream(filePath)) {
|
|
80
184
|
await writeToStream(stream, chunk)
|
|
@@ -113,6 +217,20 @@ function createHeader(filePath, stat) {
|
|
|
113
217
|
return header
|
|
114
218
|
}
|
|
115
219
|
|
|
220
|
+
function readHeader(header) {
|
|
221
|
+
const name = readString(header, 0, 100)
|
|
222
|
+
const prefix = readString(header, 345, 155)
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
name: prefix ? `${prefix}/${name}` : name,
|
|
226
|
+
size: parseInt(readString(header, 124, 12).trim() || '0', 8)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readString(buffer, offset, length) {
|
|
231
|
+
return buffer.subarray(offset, offset + length).toString('utf8').replace(/\0.*$/, '')
|
|
232
|
+
}
|
|
233
|
+
|
|
116
234
|
function splitTarPath(filePath) {
|
|
117
235
|
if (Buffer.byteLength(filePath) <= 100) {
|
|
118
236
|
return {
|
|
@@ -156,3 +274,9 @@ function paddingSize(size) {
|
|
|
156
274
|
}
|
|
157
275
|
|
|
158
276
|
module.exports = packFiles
|
|
277
|
+
module.exports.createPackConfig = createPackConfig
|
|
278
|
+
module.exports.collectRecoveryEntries = collectRecoveryEntries
|
|
279
|
+
module.exports.extractTarGz = extractTarGz
|
|
280
|
+
module.exports.removeSidecars = removeSidecars
|
|
281
|
+
module.exports.resolveOutputPath = resolveOutputPath
|
|
282
|
+
module.exports.writeTarGz = writeTarGz
|
package/src/run.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
const enF = require('./en')
|
|
2
2
|
const deF = require('./de')
|
|
3
3
|
const packF = require('./pack')
|
|
4
|
-
const { loadConfig
|
|
4
|
+
const { loadConfig } = require('./config')
|
|
5
|
+
const {
|
|
6
|
+
collectRecoveryEntries,
|
|
7
|
+
createPackConfig,
|
|
8
|
+
extractTarGz,
|
|
9
|
+
removeSidecars,
|
|
10
|
+
resolveOutputPath,
|
|
11
|
+
writeTarGz
|
|
12
|
+
} = require('./pack')
|
|
5
13
|
|
|
6
14
|
function printHelp() {
|
|
7
15
|
console.log(`Usage:
|
|
@@ -16,7 +24,6 @@ Options:
|
|
|
16
24
|
--ext <list> Comma separated target extensions
|
|
17
25
|
--exclude <list> Comma separated excluded directory names
|
|
18
26
|
--concurrency <n> Concurrent file operation limit
|
|
19
|
-
--marker [text] Only create sidecar files when any marker is found
|
|
20
27
|
--out <file> Archive output name
|
|
21
28
|
`)
|
|
22
29
|
}
|
|
@@ -24,8 +31,6 @@ Options:
|
|
|
24
31
|
function parseArgs(command, args) {
|
|
25
32
|
const result = {
|
|
26
33
|
directory: process.cwd(),
|
|
27
|
-
markerFilter: undefined,
|
|
28
|
-
markers: [],
|
|
29
34
|
positionals: []
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -98,23 +103,6 @@ function parseArgs(command, args) {
|
|
|
98
103
|
continue
|
|
99
104
|
}
|
|
100
105
|
|
|
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
106
|
if (!arg.startsWith('-')) {
|
|
119
107
|
result.positionals.push(arg)
|
|
120
108
|
}
|
|
@@ -160,8 +148,7 @@ async function run(args) {
|
|
|
160
148
|
const options = {
|
|
161
149
|
...config,
|
|
162
150
|
...cliOptions,
|
|
163
|
-
directory: cliOptions.directory || config.directory || process.cwd()
|
|
164
|
-
markers: cliOptions.markers && cliOptions.markers.length > 0 ? cliOptions.markers : config.markers
|
|
151
|
+
directory: cliOptions.directory || config.directory || process.cwd()
|
|
165
152
|
}
|
|
166
153
|
|
|
167
154
|
if (command === 'en') {
|
|
@@ -181,14 +168,28 @@ async function run(args) {
|
|
|
181
168
|
|
|
182
169
|
if (command === 'all') {
|
|
183
170
|
await enF(options.directory, options)
|
|
184
|
-
await
|
|
185
|
-
await packF(options.directory, options)
|
|
171
|
+
await runAll(options.directory, options)
|
|
186
172
|
return
|
|
187
173
|
}
|
|
188
174
|
|
|
189
175
|
printHelp()
|
|
190
176
|
}
|
|
191
177
|
|
|
178
|
+
async function runAll(directory, options) {
|
|
179
|
+
const config = createPackConfig(directory, options)
|
|
180
|
+
const outputPath = resolveOutputPath(config.directory, config.out)
|
|
181
|
+
const entries = await collectRecoveryEntries(config, {
|
|
182
|
+
outputPath
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await writeTarGz(config.directory, outputPath, entries)
|
|
186
|
+
const restored = await extractTarGz(outputPath, config.directory)
|
|
187
|
+
const deleted = await removeSidecars(config.directory, config)
|
|
188
|
+
|
|
189
|
+
console.log(`Packed: ${outputPath}`)
|
|
190
|
+
console.log(`Done. files=${entries.length}, restored=${restored}, deleted=${deleted}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
192
193
|
function omitUndefined(value) {
|
|
193
194
|
return Object.keys(value).reduce((result, key) => {
|
|
194
195
|
if (value[key] !== undefined) {
|