pi-hashline-edit-pro 0.15.0 → 0.15.1
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/package.json +1 -1
- package/src/fs-write.ts +93 -95
package/package.json
CHANGED
package/src/fs-write.ts
CHANGED
|
@@ -1,116 +1,114 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
open,
|
|
6
|
+
readlink,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
stat,
|
|
10
|
+
writeFile,
|
|
11
11
|
} from "fs/promises";
|
|
12
12
|
import { dirname, join, parse, resolve, sep } from "path";
|
|
13
13
|
import { errCode } from "./validation";
|
|
14
14
|
|
|
15
15
|
export async function resolveTarget(path: string): Promise<string> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const absolutePath = resolve(path);
|
|
17
|
+
const { root } = parse(absolutePath);
|
|
18
|
+
const parts = absolutePath
|
|
19
|
+
.slice(root.length)
|
|
20
|
+
.split(sep)
|
|
21
|
+
.filter((part) => part.length > 0);
|
|
22
|
+
const visitedSymlinks = new Set<string>();
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
async function resParts(
|
|
25
|
+
currentPath: string,
|
|
26
|
+
remainingParts: string[],
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
if (remainingParts.length === 0) {
|
|
29
|
+
return currentPath;
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const [nextPart, ...tail] = remainingParts;
|
|
33
|
+
const candidatePath = join(currentPath, nextPart);
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
try {
|
|
36
|
+
const candidateStats = await lstat(candidatePath);
|
|
37
|
+
if (!candidateStats.isSymbolicLink()) {
|
|
38
|
+
return resParts(candidatePath, tail);
|
|
39
|
+
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
if (visitedSymlinks.has(candidatePath)) {
|
|
42
|
+
const error = new Error(
|
|
43
|
+
`Too many symbolic links while resolving ${path}`,
|
|
44
|
+
) as NodeJS.ErrnoException;
|
|
45
|
+
error.code = "ELOOP";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
visitedSymlinks.add(candidatePath);
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
const linkTargetPath = resolve(
|
|
51
|
+
dirname(candidatePath),
|
|
52
|
+
await readlink(candidatePath),
|
|
53
|
+
);
|
|
54
|
+
const targetParts = linkTargetPath
|
|
55
|
+
.slice(parse(linkTargetPath).root.length)
|
|
56
|
+
.split(sep)
|
|
57
|
+
.filter((part) => part.length > 0);
|
|
58
|
+
return resParts(parse(linkTargetPath).root, [
|
|
59
|
+
...targetParts,
|
|
60
|
+
...tail,
|
|
61
|
+
]);
|
|
62
|
+
} catch (error: unknown) {
|
|
63
|
+
if (errCode(error) === "ENOENT") {
|
|
64
|
+
return join(candidatePath, ...tail);
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
return resParts(root, parts);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export async function writeAtomic(
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
path: string,
|
|
75
|
+
content: string,
|
|
76
76
|
): Promise<void> {
|
|
77
|
-
|
|
77
|
+
const targetPath = await resolveTarget(path);
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
let existingStats: Awaited<ReturnType<typeof stat>> | null = null;
|
|
80
|
+
try {
|
|
81
|
+
existingStats = await stat(targetPath);
|
|
82
|
+
} catch (error: unknown) {
|
|
83
|
+
if (errCode(error) !== "ENOENT") {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
if (existingStats && existingStats.nlink > 1) {
|
|
89
|
+
await writeFile(targetPath, content, "utf-8");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
93
|
+
const dir = dirname(targetPath);
|
|
94
|
+
const tempPath = join(dir, `.tmp-${randomUUID()}`);
|
|
95
|
+
await mkdir(dir, { recursive: true });
|
|
96
|
+
const tempHandle = await open(tempPath, "wx", 0o600);
|
|
97
|
+
try {
|
|
98
|
+
await tempHandle.writeFile(content, "utf-8");
|
|
99
|
+
if (existingStats) {
|
|
100
|
+
await tempHandle.chmod(existingStats.mode & 0o7777);
|
|
101
|
+
}
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
await tempHandle.close();
|
|
104
|
+
try { await rm(tempPath, { force: true }); } catch { /* best-effort */ }
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await tempHandle.close();
|
|
109
|
+
await rename(tempPath, targetPath);
|
|
110
|
+
} catch (error: unknown) {
|
|
111
|
+
try { await rm(tempPath, { force: true }); } catch { /* best-effort */ }
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
116
114
|
}
|