@gmickel/gno 0.29.0 → 0.29.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/core/file-ops.ts +172 -8
- package/src/serve/routes/api.ts +10 -1
package/package.json
CHANGED
package/src/core/file-ops.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// node:fs/promises for rename/unlink (no Bun equivalent for structure ops)
|
|
8
|
-
import { rename, unlink } from "node:fs/promises";
|
|
9
|
-
// node:os platform: no Bun equivalent
|
|
10
|
-
import { platform } from "node:os";
|
|
11
|
-
// node:path dirname: no Bun equivalent
|
|
12
|
-
import { dirname } from "node:path";
|
|
8
|
+
import { mkdir, rename, unlink } from "node:fs/promises";
|
|
9
|
+
// node:os platform/homedir/tmpdir: no Bun equivalent
|
|
10
|
+
import { homedir, platform as getPlatform, tmpdir } from "node:os";
|
|
11
|
+
// node:path dirname/join/parse: no Bun equivalent
|
|
12
|
+
import { dirname, join, parse } from "node:path";
|
|
13
13
|
|
|
14
14
|
export async function atomicWrite(
|
|
15
15
|
path: string,
|
|
@@ -48,12 +48,176 @@ export async function renameFilePath(
|
|
|
48
48
|
await rename(currentPath, nextPath);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
type TrashFileDeps = {
|
|
52
|
+
homeDir?: string;
|
|
53
|
+
platform?: ReturnType<typeof getPlatform>;
|
|
54
|
+
runCommand?: typeof runCommand;
|
|
55
|
+
tempDir?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function isCrossDeviceRenameError(
|
|
59
|
+
error: unknown
|
|
60
|
+
): error is NodeJS.ErrnoException {
|
|
61
|
+
return (
|
|
62
|
+
error instanceof Error &&
|
|
63
|
+
"code" in error &&
|
|
64
|
+
typeof error.code === "string" &&
|
|
65
|
+
error.code === "EXDEV"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function nextAvailableTrashPath(
|
|
70
|
+
trashDir: string,
|
|
71
|
+
sourcePath: string
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
const { ext, name, base } = parse(sourcePath);
|
|
74
|
+
let candidate = join(trashDir, base);
|
|
75
|
+
let suffix = 2;
|
|
76
|
+
while (await Bun.file(candidate).exists()) {
|
|
77
|
+
candidate = join(trashDir, `${name} ${suffix}${ext}`);
|
|
78
|
+
suffix += 1;
|
|
79
|
+
}
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function moveFilePath(
|
|
84
|
+
sourcePath: string,
|
|
85
|
+
targetPath: string
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await rename(sourcePath, targetPath);
|
|
89
|
+
return;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!isCrossDeviceRenameError(error)) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await Bun.write(targetPath, Bun.file(sourcePath));
|
|
97
|
+
await unlink(sourcePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function trashFilePathOnDarwin(
|
|
101
|
+
path: string,
|
|
102
|
+
homeDir: string
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const trashDir = join(homeDir, ".Trash");
|
|
105
|
+
await mkdir(trashDir, { recursive: true });
|
|
106
|
+
const targetPath = await nextAvailableTrashPath(trashDir, path);
|
|
107
|
+
await moveFilePath(path, targetPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function encodeTrashInfoPath(path: string): string {
|
|
111
|
+
return path
|
|
112
|
+
.split("/")
|
|
113
|
+
.map((segment, index) =>
|
|
114
|
+
index === 0 && segment.length === 0 ? "" : encodeURIComponent(segment)
|
|
115
|
+
)
|
|
116
|
+
.join("/");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function trashFilePathOnLinux(
|
|
120
|
+
path: string,
|
|
121
|
+
homeDir: string
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const trashRoot = join(homeDir, ".local", "share", "Trash");
|
|
124
|
+
const filesDir = join(trashRoot, "files");
|
|
125
|
+
const infoDir = join(trashRoot, "info");
|
|
126
|
+
await mkdir(filesDir, { recursive: true });
|
|
127
|
+
await mkdir(infoDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
const targetPath = await nextAvailableTrashPath(filesDir, path);
|
|
130
|
+
const infoPath = join(infoDir, `${parse(targetPath).base}.trashinfo`);
|
|
131
|
+
const infoContent = [
|
|
132
|
+
"[Trash Info]",
|
|
133
|
+
`Path=${encodeTrashInfoPath(path)}`,
|
|
134
|
+
`DeletionDate=${new Date().toISOString().slice(0, 19)}`,
|
|
135
|
+
"",
|
|
136
|
+
].join("\n");
|
|
137
|
+
|
|
138
|
+
await moveFilePath(path, targetPath);
|
|
139
|
+
try {
|
|
140
|
+
await Bun.write(infoPath, infoContent);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await moveFilePath(targetPath, path).catch(() => {
|
|
143
|
+
/* ignore rollback errors */
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function trashFilePathOnWindows(
|
|
150
|
+
path: string,
|
|
151
|
+
deps: Required<Pick<TrashFileDeps, "runCommand" | "tempDir">>
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
const scriptPath = join(deps.tempDir, `gno-trash-${crypto.randomUUID()}.ps1`);
|
|
154
|
+
const script = `param([string]$LiteralPath)
|
|
155
|
+
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
156
|
+
if (Test-Path -LiteralPath $LiteralPath -PathType Container) {
|
|
157
|
+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(
|
|
158
|
+
$LiteralPath,
|
|
159
|
+
[Microsoft.VisualBasic.FileIO.UIOption]::OnlyErrorDialogs,
|
|
160
|
+
[Microsoft.VisualBasic.FileIO.RecycleOption]::SendToRecycleBin
|
|
161
|
+
)
|
|
162
|
+
} else {
|
|
163
|
+
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(
|
|
164
|
+
$LiteralPath,
|
|
165
|
+
[Microsoft.VisualBasic.FileIO.UIOption]::OnlyErrorDialogs,
|
|
166
|
+
[Microsoft.VisualBasic.FileIO.RecycleOption]::SendToRecycleBin
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
await Bun.write(scriptPath, script);
|
|
172
|
+
try {
|
|
173
|
+
await deps.runCommand([
|
|
174
|
+
"powershell",
|
|
175
|
+
"-NoProfile",
|
|
176
|
+
"-ExecutionPolicy",
|
|
177
|
+
"Bypass",
|
|
178
|
+
"-File",
|
|
179
|
+
scriptPath,
|
|
180
|
+
path,
|
|
181
|
+
]);
|
|
182
|
+
} finally {
|
|
183
|
+
await unlink(scriptPath).catch(() => {
|
|
184
|
+
/* ignore cleanup errors */
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function trashFilePath(
|
|
190
|
+
path: string,
|
|
191
|
+
deps: TrashFileDeps = {}
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const platform = deps.platform ?? getPlatform();
|
|
194
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
195
|
+
const runner = deps.runCommand ?? runCommand;
|
|
196
|
+
const tempDir = deps.tempDir ?? tmpdir();
|
|
197
|
+
|
|
198
|
+
if (platform === "darwin") {
|
|
199
|
+
await trashFilePathOnDarwin(path, homeDir);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (platform === "linux") {
|
|
204
|
+
await trashFilePathOnLinux(path, homeDir);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (platform === "win32") {
|
|
209
|
+
await trashFilePathOnWindows(path, {
|
|
210
|
+
runCommand: runner,
|
|
211
|
+
tempDir,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(`Trash is not supported on platform: ${platform}`);
|
|
53
217
|
}
|
|
54
218
|
|
|
55
219
|
export async function revealFilePath(path: string): Promise<void> {
|
|
56
|
-
if (
|
|
220
|
+
if (getPlatform() === "darwin") {
|
|
57
221
|
await runCommand(["open", "-R", path]);
|
|
58
222
|
return;
|
|
59
223
|
}
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -1258,7 +1258,16 @@ export async function handleTrashDoc(
|
|
|
1258
1258
|
defaultSyncService.syncCollection(collectionArg, storeArg, optionsArg));
|
|
1259
1259
|
ctxHolder.watchService?.suppress(fullPath);
|
|
1260
1260
|
await (deps?.trashFilePath ?? trashFilePath)(fullPath);
|
|
1261
|
-
await store.markInactive(doc.collection, [
|
|
1261
|
+
const markInactiveResult = await store.markInactive(doc.collection, [
|
|
1262
|
+
doc.relPath,
|
|
1263
|
+
]);
|
|
1264
|
+
if (!markInactiveResult.ok) {
|
|
1265
|
+
return errorResponse(
|
|
1266
|
+
"RUNTIME",
|
|
1267
|
+
`File moved to Trash, but removing it from the current index failed: ${markInactiveResult.error.message}`,
|
|
1268
|
+
500
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1262
1271
|
let warning: string | undefined;
|
|
1263
1272
|
try {
|
|
1264
1273
|
await syncCollection(collection, store, {
|