mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix: write media buffers atomically
This commit is contained in:
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Control UI/chat: keep live replies visible when a raw session alias such as `main` sends the chat turn but Gateway emits events under the canonical session key for the same run. Fixes #73716. Thanks @teebes.
|
||||
- Media: write inbound media buffers through same-directory temp files before rename, so failed disk writes do not leave zero-byte artifacts for later voice transcription. Fixes #55966. Thanks @OpenCodeEngineer.
|
||||
- TTS/Telegram: keep trusted local audio generated by the TTS tool queued for voice-note delivery even when the run-level built-in tool list omits the raw `tts` name. Fixes #74752. Thanks @Loveworld3033 and @andyliu.
|
||||
- TTS: require explicit user or config audio intent for the agent speech tool so dashboard chats stay text unless audio is requested. Fixes #69777. Thanks @alexandre-leng.
|
||||
- Plugins/config: keep bundled source-checkout plugins from being runtime-gated by install-only `minHostVersion` metadata, accept prerelease host floors, trim plugin-service startup failures to one log line, and avoid broad channel-runtime loading during base config parsing. Thanks @vincentkoc.
|
||||
|
||||
@@ -307,6 +307,40 @@ describe("media store", () => {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not leave final media artifacts when buffer writes fail",
|
||||
run: async () => {
|
||||
await withTempStore(async (store) => {
|
||||
const mediaDir = await store.ensureMediaDir();
|
||||
const originalWriteFile = fs.writeFile.bind(fs);
|
||||
const attemptedPaths: string[] = [];
|
||||
vi.spyOn(fs, "writeFile").mockImplementation(async (...args) => {
|
||||
const [filePath] = args;
|
||||
if (
|
||||
typeof filePath === "string" &&
|
||||
filePath.includes(`${path.sep}failed-buffer${path.sep}`)
|
||||
) {
|
||||
attemptedPaths.push(filePath);
|
||||
await originalWriteFile(filePath, Buffer.alloc(0), args[2]);
|
||||
const err = new Error("no space left on device") as NodeJS.ErrnoException;
|
||||
err.code = "ENOSPC";
|
||||
throw err;
|
||||
}
|
||||
return await originalWriteFile(...args);
|
||||
});
|
||||
|
||||
await expect(
|
||||
store.saveMediaBuffer(Buffer.from("voice"), "audio/ogg", "failed-buffer"),
|
||||
).rejects.toMatchObject({ code: "ENOSPC" });
|
||||
|
||||
const failedDir = path.join(mediaDir, "failed-buffer");
|
||||
const entries = await fs.readdir(failedDir).catch(() => []);
|
||||
expect(attemptedPaths).toHaveLength(1);
|
||||
expect(path.basename(attemptedPaths[0] ?? "")).toMatch(/^\..+\.tmp$/);
|
||||
expect(entries).toEqual([]);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rejects traversal media subdirs before saving buffers",
|
||||
run: async () => {
|
||||
|
||||
@@ -345,9 +345,22 @@ async function writeSavedMediaBuffer(params: {
|
||||
buffer: Buffer;
|
||||
}): Promise<string> {
|
||||
const dest = path.join(params.dir, params.id);
|
||||
await retryAfterRecreatingDir(params.dir, () =>
|
||||
fs.writeFile(dest, params.buffer, { mode: MEDIA_FILE_MODE }),
|
||||
);
|
||||
await retryAfterRecreatingDir(params.dir, async () => {
|
||||
const tempDest = path.join(params.dir, `.${params.id}.${crypto.randomUUID()}.tmp`);
|
||||
try {
|
||||
await fs.writeFile(tempDest, params.buffer, { mode: MEDIA_FILE_MODE });
|
||||
const handle = await fs.open(tempDest, "r");
|
||||
try {
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
await fs.rename(tempDest, dest);
|
||||
} catch (err) {
|
||||
await fs.rm(tempDest, { force: true }).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
return dest;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user