goldsync 0.1.15 → 0.1.31
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 +60 -79
- package/assets/GoldSync.rbxmx +1130 -208
- package/bin/install-companion.mjs +29 -3
- package/media/configuration-assets.png +0 -0
- package/media/file-boundaries.png +0 -0
- package/package.json +2 -2
- package/src/companion.mjs +1 -1
- package/src/config.mjs +39 -6
- package/src/git-conflicts.mjs +1 -1
- package/src/server.mjs +108 -13
- package/src/store.mjs +231 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { access, copyFile, cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
|
+
import { access, copyFile, cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -38,6 +38,33 @@ const configPath = configIndex === -1
|
|
|
38
38
|
|
|
39
39
|
const dataDirectory = path.join(process.env.LOCALAPPDATA, "GoldSync");
|
|
40
40
|
const runtimeDirectory = path.join(dataDirectory, "runtime");
|
|
41
|
+
const installedAgent = path.join(runtimeDirectory, "bin", "goldsync-agent.mjs");
|
|
42
|
+
const pidFile = path.join(dataDirectory, "agent.pid");
|
|
43
|
+
let installedPid;
|
|
44
|
+
try {
|
|
45
|
+
installedPid = Number(await readFile(pidFile, "utf8"));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT") throw error;
|
|
48
|
+
}
|
|
49
|
+
if (Number.isInteger(installedPid) && installedPid > 0) {
|
|
50
|
+
const stopCommand = `
|
|
51
|
+
$ErrorActionPreference = 'Stop'
|
|
52
|
+
$agentProcess = Get-CimInstance Win32_Process -Filter "ProcessId = $env:GOLDSYNC_AGENT_PID"
|
|
53
|
+
if ($agentProcess) {
|
|
54
|
+
if ((Split-Path -Parent $agentProcess.ExecutablePath) -ine $env:GOLDSYNC_RUNTIME_DIR -or $agentProcess.CommandLine.IndexOf($env:GOLDSYNC_AGENT_SCRIPT, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
|
|
55
|
+
throw 'The recorded GoldSync process does not match the installed agent. No process was stopped.'
|
|
56
|
+
}
|
|
57
|
+
Stop-Process -Id $agentProcess.ProcessId
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", stopCommand], {
|
|
62
|
+
windowsHide: true,
|
|
63
|
+
env: { ...process.env, GOLDSYNC_AGENT_PID: String(installedPid), GOLDSYNC_AGENT_SCRIPT: installedAgent, GOLDSYNC_RUNTIME_DIR: runtimeDirectory },
|
|
64
|
+
}, (error) => error ? reject(error) : resolve());
|
|
65
|
+
});
|
|
66
|
+
await rm(pidFile, { force: true });
|
|
67
|
+
}
|
|
41
68
|
await mkdir(path.join(runtimeDirectory, "bin"), { recursive: true });
|
|
42
69
|
await cp(path.join(packageDirectory, "src"), path.join(runtimeDirectory, "src"), {
|
|
43
70
|
recursive: true,
|
|
@@ -69,7 +96,6 @@ try {
|
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
const config = await registerProject(path.join(dataDirectory, "projects.json"), configPath);
|
|
72
|
-
const installedAgent = path.join(runtimeDirectory, "bin", "goldsync-agent.mjs");
|
|
73
99
|
const startupCommand = `"${runtimeNode}" "${installedAgent}"`;
|
|
74
100
|
const vbsString = `"${startupCommand.replaceAll('"', '""')}"`;
|
|
75
101
|
const startupFile = path.join(
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goldsync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Automatic native Roblox asset synchronization for Rojo projects.",
|
|
5
5
|
"author": "tay",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"bin/goldsync-agent.mjs",
|
|
14
14
|
"bin/install-companion.mjs",
|
|
15
15
|
"LICENSE.txt",
|
|
16
|
-
"media
|
|
16
|
+
"media",
|
|
17
17
|
"src",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
package/src/companion.mjs
CHANGED
package/src/config.mjs
CHANGED
|
@@ -76,8 +76,8 @@ export async function loadConfig(configPath) {
|
|
|
76
76
|
const projectRoot = await realpath(path.dirname(absoluteConfigPath));
|
|
77
77
|
const raw = JSON.parse(await readFile(absoluteConfigPath, "utf8"));
|
|
78
78
|
|
|
79
|
-
if (raw.version !== 1) {
|
|
80
|
-
throw new Error("goldsync.project.json must use version 1");
|
|
79
|
+
if (raw.version !== 1 && raw.version !== 2) {
|
|
80
|
+
throw new Error("goldsync.project.json must use version 1 or 2");
|
|
81
81
|
}
|
|
82
82
|
const port = raw.port ?? 34873;
|
|
83
83
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
@@ -90,15 +90,16 @@ export async function loadConfig(configPath) {
|
|
|
90
90
|
|| raw.discoveryRoots.some((root) => !root || root.splitDepth === undefined))) {
|
|
91
91
|
throw new Error("discoveryRoots must contain directory roots with splitDepth");
|
|
92
92
|
}
|
|
93
|
-
const maxPathDepth = raw.maxPathDepth ?? 3;
|
|
94
|
-
if (!Number.isInteger(maxPathDepth) || maxPathDepth < 2 || maxPathDepth > 10) {
|
|
95
|
-
throw new Error(
|
|
93
|
+
const maxPathDepth = raw.maxPathDepth ?? (raw.version === 2 ? 32 : 3);
|
|
94
|
+
if (!Number.isInteger(maxPathDepth) || maxPathDepth < 2 || maxPathDepth > (raw.version === 2 ? 32 : 10)) {
|
|
95
|
+
throw new Error(`maxPathDepth must be an integer between 2 and ${raw.version === 2 ? 32 : 10}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const ids = new Set();
|
|
99
99
|
const studioPaths = new Set();
|
|
100
100
|
const files = new Set();
|
|
101
101
|
const splitRoots = [];
|
|
102
|
+
const treeRoots = [];
|
|
102
103
|
const roots = [...raw.roots, ...(raw.discoveryRoots ?? [])].flatMap((root, index) => {
|
|
103
104
|
const label = index < raw.roots.length ? `roots[${index}]` : `discoveryRoots[${index - raw.roots.length}]`;
|
|
104
105
|
const id = requireString(root.id, `${label}.id`);
|
|
@@ -121,6 +122,22 @@ export async function loadConfig(configPath) {
|
|
|
121
122
|
}
|
|
122
123
|
studioPaths.add(studioPathKey);
|
|
123
124
|
|
|
125
|
+
if (root.mode === "tree") {
|
|
126
|
+
if (raw.version !== 2 || root.file !== undefined || splitDepth !== undefined) {
|
|
127
|
+
throw new Error(`${label}: tree roots require version 2 and cannot specify file or splitDepth`);
|
|
128
|
+
}
|
|
129
|
+
const absoluteDirectory = resolveInsideProject(projectRoot, root.directory, `${label}.directory`);
|
|
130
|
+
if (root.containers !== undefined) throw new Error(`${label}: tree roots split Folders automatically; remove containers`);
|
|
131
|
+
treeRoots.push({
|
|
132
|
+
id, studioPath, absoluteDirectory,
|
|
133
|
+
directory: path.relative(projectRoot, absoluteDirectory).replaceAll(path.sep, "/"),
|
|
134
|
+
});
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
if (root.mode !== undefined) {
|
|
138
|
+
throw new Error(`${label}.mode must be tree when specified`);
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
if (splitDepth !== undefined) {
|
|
125
142
|
const directory = requireString(root.directory, `${label}.directory`);
|
|
126
143
|
const absoluteDirectory = resolveInsideProject(projectRoot, directory, `${label}.directory`);
|
|
@@ -150,6 +167,21 @@ export async function loadConfig(configPath) {
|
|
|
150
167
|
});
|
|
151
168
|
|
|
152
169
|
const configuredRoots = [...roots, ...splitRoots];
|
|
170
|
+
for (const tree of treeRoots) {
|
|
171
|
+
for (const other of [...configuredRoots, ...treeRoots]) {
|
|
172
|
+
if (other === tree) continue;
|
|
173
|
+
const common = Math.min(tree.studioPath.length, other.studioPath.length);
|
|
174
|
+
if (tree.studioPath.slice(0, common).every((segment, index) => segment === other.studioPath[index])) {
|
|
175
|
+
throw new Error(`tree root ${tree.id} overlaps ${other.id}`);
|
|
176
|
+
}
|
|
177
|
+
const otherDirectory = other.absoluteDirectory ?? path.dirname(other.absoluteFile);
|
|
178
|
+
const relative = path.relative(tree.absoluteDirectory, otherDirectory);
|
|
179
|
+
const reverse = path.relative(otherDirectory, tree.absoluteDirectory);
|
|
180
|
+
if ((!relative.startsWith("..") && !path.isAbsolute(relative)) || (!reverse.startsWith("..") && !path.isAbsolute(reverse))) {
|
|
181
|
+
throw new Error(`tree directory ${tree.id} overlaps ${other.id}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
153
185
|
for (const root of roots) {
|
|
154
186
|
if (configuredRoots.some((other) => other !== root
|
|
155
187
|
&& other.studioPath.length > root.studioPath.length
|
|
@@ -166,7 +198,7 @@ export async function loadConfig(configPath) {
|
|
|
166
198
|
}
|
|
167
199
|
|
|
168
200
|
return {
|
|
169
|
-
version:
|
|
201
|
+
version: raw.version,
|
|
170
202
|
name: requireString(raw.name, "name"),
|
|
171
203
|
projectId: requireString(raw.projectId, "projectId"),
|
|
172
204
|
workspaceId: createHash("sha256").update(absoluteConfigPath.toLowerCase()).digest("hex").slice(0, 12),
|
|
@@ -181,5 +213,6 @@ export async function loadConfig(configPath) {
|
|
|
181
213
|
sessionMarkerFile: path.join(projectRoot, ".goldsync", "rojo-session.model.json"),
|
|
182
214
|
roots,
|
|
183
215
|
splitRoots,
|
|
216
|
+
treeRoots,
|
|
184
217
|
};
|
|
185
218
|
}
|
package/src/git-conflicts.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export class GitConflictStore {
|
|
|
61
61
|
return new Map();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const output = await runGit(this.repositoryRoot, ["ls-files", "-u", "-z"
|
|
64
|
+
const output = await runGit(this.repositoryRoot, ["ls-files", "-u", "-z"]);
|
|
65
65
|
const byPath = new Map();
|
|
66
66
|
for (const entry of output.toString("utf8").split("\0")) {
|
|
67
67
|
if (!entry) {
|
package/src/server.mjs
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { ConflictError } from "./store.mjs";
|
|
5
6
|
|
|
6
7
|
const MAX_SNAPSHOT_BYTES = 128 * 1024 * 1024;
|
|
7
8
|
|
|
8
|
-
function sendJson(response, status, value) {
|
|
9
|
+
function sendJson(response, status, value, request) {
|
|
9
10
|
const body = Buffer.from(JSON.stringify(value));
|
|
11
|
+
const etag = request ? `"${createHash("sha256").update(body).digest("hex")}"` : null;
|
|
12
|
+
if (etag && request.headers["if-none-match"] === etag) {
|
|
13
|
+
response.writeHead(304, { ETag: etag, "Cache-Control": "no-store" });
|
|
14
|
+
response.end();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
10
17
|
response.writeHead(status, {
|
|
11
18
|
"Content-Type": "application/json",
|
|
12
19
|
"Content-Length": body.length,
|
|
13
20
|
"Cache-Control": "no-store",
|
|
21
|
+
...(etag ? { ETag: etag } : {}),
|
|
14
22
|
});
|
|
15
23
|
response.end(body);
|
|
16
24
|
}
|
|
@@ -79,20 +87,21 @@ export class GoldSyncServer {
|
|
|
79
87
|
for (const splitRoot of this.config.splitRoots ?? []) {
|
|
80
88
|
watchedDirectories.set(splitRoot.absoluteDirectory, true);
|
|
81
89
|
}
|
|
90
|
+
for (const treeRoot of this.config.treeRoots ?? []) {
|
|
91
|
+
watchedDirectories.set(treeRoot.absoluteDirectory, true);
|
|
92
|
+
}
|
|
82
93
|
for (const [directory, recursive] of watchedDirectories) {
|
|
83
|
-
const watcher = watch(directory, { recursive }, (
|
|
94
|
+
const watcher = watch(directory, { recursive }, (event, filename) => {
|
|
95
|
+
if (recursive && (event === "rename" || !filename)) this.store.treeScanNeeded = true;
|
|
84
96
|
if (!filename) {
|
|
85
97
|
return;
|
|
86
98
|
}
|
|
87
99
|
const changedFile = path.join(directory, filename.toString()).toLowerCase();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (!matched && recursive && path.extname(changedFile) === ".rbxm") {
|
|
100
|
+
const root = this.store.rootsByFile.get(changedFile);
|
|
101
|
+
if (root) this.scheduleRefresh(root.id);
|
|
102
|
+
if (!root && recursive && path.extname(changedFile) === ".rbxm") {
|
|
103
|
+
this.store.treeScanNeeded = true;
|
|
104
|
+
if ((this.config.treeRoots ?? []).length > 0) return;
|
|
96
105
|
this.store
|
|
97
106
|
.refreshSplitRoots()
|
|
98
107
|
.then(() => this.gitConflicts?.syncRoots?.(this.store.getRoots()))
|
|
@@ -143,13 +152,51 @@ export class GoldSyncServer {
|
|
|
143
152
|
sendJson(response, 409, { error: "this Studio place is not bound to the current Rojo session" });
|
|
144
153
|
return;
|
|
145
154
|
}
|
|
155
|
+
if (this.config.version === 2 && request.headers["x-goldsync-tree-roots"] !== "4") {
|
|
156
|
+
sendJson(response, 409, { error: "This project requires GoldSync 0.1.29 or newer. Update GoldSync and restart Studio." });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
146
159
|
const url = new URL(request.url, `http://${this.config.host}:${this.config.port}`);
|
|
160
|
+
if (this.store.boundaryChange) {
|
|
161
|
+
sendJson(response, 409, { error: "Boundary conversion in progress; retry after it finishes" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const boundaryMatch = /^\/v1\/roots\/([a-z0-9-]+)\/boundary$/.exec(url.pathname);
|
|
165
|
+
if (request.method === "PUT" && boundaryMatch) {
|
|
166
|
+
if (!this.syncEnabled) { sendJson(response, 409, { error: "Connect Rojo before converting a boundary" }); return; }
|
|
167
|
+
const bytes = await readBody(request);
|
|
168
|
+
if (bytes.length < 4) throw new Error("Invalid boundary packet");
|
|
169
|
+
const headerLength = bytes.readUInt32LE();
|
|
170
|
+
if (headerLength > bytes.length - 4) throw new Error("Invalid boundary header");
|
|
171
|
+
const payload = JSON.parse(bytes.subarray(4, 4 + headerLength));
|
|
172
|
+
if (!Array.isArray(payload.entries) || payload.entries.length > 10000) throw new Error("Invalid boundary entries");
|
|
173
|
+
const conflicts = this.gitConflicts ? await this.gitConflicts.list() : new Map();
|
|
174
|
+
if (Object.keys(payload.expected ?? {}).some((id) => conflicts.has(id))) {
|
|
175
|
+
sendJson(response, 409, { error: "Resolve Git conflicts before converting the boundary" }); return;
|
|
176
|
+
}
|
|
177
|
+
let offset = 4 + headerLength;
|
|
178
|
+
for (const entry of payload.entries) {
|
|
179
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || offset + entry.size > bytes.length) throw new Error("Invalid snapshot length");
|
|
180
|
+
entry.bytes = bytes.subarray(offset, offset + entry.size);
|
|
181
|
+
offset += entry.size;
|
|
182
|
+
}
|
|
183
|
+
if (offset !== bytes.length) throw new Error("Unexpected boundary packet data");
|
|
184
|
+
try {
|
|
185
|
+
const roots = await this.store.replaceBoundary(boundaryMatch[1], payload.entries, payload.expected);
|
|
186
|
+
this.gitConflicts?.syncRoots?.(this.store.getRoots());
|
|
187
|
+
sendJson(response, 200, { roots });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof ConflictError) { sendJson(response, 409, { error: "Files changed; refresh before converting" }); return; }
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
147
194
|
if (request.method === "GET" && url.pathname === "/v1/config") {
|
|
148
195
|
await this.store.refreshSplitRoots();
|
|
149
196
|
this.gitConflicts?.syncRoots?.(this.store.getRoots());
|
|
150
197
|
const conflicts = this.gitConflicts ? await this.gitConflicts.list() : new Map();
|
|
151
198
|
sendJson(response, 200, {
|
|
152
|
-
version: 1,
|
|
199
|
+
version: this.config.version ?? 1,
|
|
153
200
|
name: this.config.name,
|
|
154
201
|
projectId: this.config.projectId,
|
|
155
202
|
workspaceId: this.config.workspaceId,
|
|
@@ -157,6 +204,7 @@ export class GoldSyncServer {
|
|
|
157
204
|
pollIntervalMs: this.config.pollIntervalMs,
|
|
158
205
|
debounceMs: this.config.debounceMs,
|
|
159
206
|
rojoConnected: this.syncEnabled,
|
|
207
|
+
treeRoots: (this.config.treeRoots ?? []).map((root) => ({ id: root.id, studioPath: root.studioPath })),
|
|
160
208
|
splitRoots: (this.config.splitRoots ?? []).map((root) => ({
|
|
161
209
|
id: root.id,
|
|
162
210
|
studioPath: root.studioPath,
|
|
@@ -172,11 +220,58 @@ export class GoldSyncServer {
|
|
|
172
220
|
conflictStages: conflict?.stages ?? null,
|
|
173
221
|
};
|
|
174
222
|
}),
|
|
175
|
-
});
|
|
223
|
+
}, request);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (request.method === "POST" && url.pathname === "/v1/snapshots") {
|
|
228
|
+
if (!this.syncEnabled) {
|
|
229
|
+
sendJson(response, 409, { error: "Snapshot downloads are paused until Rojo is connected" });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const ids = JSON.parse((await readBody(request)).toString("utf8"));
|
|
233
|
+
if (!Array.isArray(ids) || ids.length > 50 || ids.some((id) => typeof id !== "string")) {
|
|
234
|
+
sendJson(response, 400, { error: "Expected at most 50 snapshot IDs" });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const conflicts = this.gitConflicts ? await this.gitConflicts.list() : new Map();
|
|
238
|
+
const chunks = [];
|
|
239
|
+
let size = 0;
|
|
240
|
+
for (const id of ids) {
|
|
241
|
+
const manifest = this.store.getManifest(id);
|
|
242
|
+
if (conflicts.has(id) || !manifest.exists || manifest.size + size > 8 * 1024 * 1024) continue;
|
|
243
|
+
const snapshot = await this.store.get(id);
|
|
244
|
+
if (!snapshot || snapshot.bytes.length + size > 8 * 1024 * 1024) continue;
|
|
245
|
+
const header = Buffer.from(JSON.stringify({ id, hash: snapshot.hash, size: snapshot.bytes.length }));
|
|
246
|
+
if (size + snapshot.bytes.length + header.length + 4 > 8 * 1024 * 1024) continue;
|
|
247
|
+
const length = Buffer.alloc(4);
|
|
248
|
+
length.writeUInt32LE(header.length);
|
|
249
|
+
chunks.push(length, header, snapshot.bytes);
|
|
250
|
+
size += snapshot.bytes.length + header.length + 4;
|
|
251
|
+
}
|
|
252
|
+
const body = Buffer.concat(chunks);
|
|
253
|
+
response.writeHead(200, { "Content-Type": "application/octet-stream", "Content-Length": body.length, "Cache-Control": "no-store" });
|
|
254
|
+
response.end(body);
|
|
176
255
|
return;
|
|
177
256
|
}
|
|
178
257
|
|
|
179
258
|
const splitMatch = /^\/v1\/splits\/([a-z0-9-]+)\/discover$/.exec(url.pathname);
|
|
259
|
+
const treeMatch = /^\/v1\/trees\/([a-z0-9-]+)\/discover$/.exec(url.pathname);
|
|
260
|
+
if (request.method === "POST" && treeMatch) {
|
|
261
|
+
if (!this.syncEnabled) {
|
|
262
|
+
sendJson(response, 409, { error: "Tree discovery is paused until Rojo is connected" });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const payload = JSON.parse((await readBody(request)).toString("utf8"));
|
|
266
|
+
if (!Array.isArray(payload.entries) || payload.entries.length > 10000 || payload.entries.some((entry) => !entry || typeof entry !== "object")) {
|
|
267
|
+
sendJson(response, 400, { error: "entries must be an array with at most 10000 tree entries" });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await this.store.discoverTreePaths(treeMatch[1], payload.entries);
|
|
271
|
+
this.gitConflicts?.syncRoots?.(this.store.getRoots());
|
|
272
|
+
sendJson(response, 200, { discovered: payload.entries.length });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
180
275
|
if (request.method === "POST" && splitMatch) {
|
|
181
276
|
const payload = JSON.parse((await readBody(request)).toString("utf8"));
|
|
182
277
|
if (!Array.isArray(payload.paths) || payload.paths.length > 10000) {
|
|
@@ -252,7 +347,7 @@ export class GoldSyncServer {
|
|
|
252
347
|
}
|
|
253
348
|
const expectedHash = expectedHashHeader.replace(/^"|"$/g, "");
|
|
254
349
|
try {
|
|
255
|
-
sendJson(response, 200, await this.store.delete(id, expectedHash));
|
|
350
|
+
sendJson(response, 200, await this.store.delete(id, expectedHash, request.headers["x-goldsync-plain-folder"] === "1"));
|
|
256
351
|
} catch (error) {
|
|
257
352
|
if (error instanceof ConflictError) {
|
|
258
353
|
sendJson(response, 409, { error: error.message, current: error.current });
|