fix: write media buffers atomically · openclaw/openclaw@689a1cd
steipete
·
2026-05-02
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
|
20 | 20 | ### Fixes |
21 | 21 | |
22 | 22 | - 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. |
| 23 | +- 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. |
23 | 24 | - 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. |
24 | 25 | - 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. |
25 | 26 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -307,6 +307,40 @@ describe("media store", () => {
|
307 | 307 | }); |
308 | 308 | }, |
309 | 309 | }, |
| 310 | +{ |
| 311 | +name: "does not leave final media artifacts when buffer writes fail", |
| 312 | +run: async () => { |
| 313 | +await withTempStore(async (store) => { |
| 314 | +const mediaDir = await store.ensureMediaDir(); |
| 315 | +const originalWriteFile = fs.writeFile.bind(fs); |
| 316 | +const attemptedPaths: string[] = []; |
| 317 | +vi.spyOn(fs, "writeFile").mockImplementation(async (...args) => { |
| 318 | +const [filePath] = args; |
| 319 | +if ( |
| 320 | +typeof filePath === "string" && |
| 321 | +filePath.includes(`${path.sep}failed-buffer${path.sep}`) |
| 322 | +) { |
| 323 | +attemptedPaths.push(filePath); |
| 324 | +await originalWriteFile(filePath, Buffer.alloc(0), args[2]); |
| 325 | +const err = new Error("no space left on device") as NodeJS.ErrnoException; |
| 326 | +err.code = "ENOSPC"; |
| 327 | +throw err; |
| 328 | +} |
| 329 | +return await originalWriteFile(...args); |
| 330 | +}); |
| 331 | + |
| 332 | +await expect( |
| 333 | +store.saveMediaBuffer(Buffer.from("voice"), "audio/ogg", "failed-buffer"), |
| 334 | +).rejects.toMatchObject({ code: "ENOSPC" }); |
| 335 | + |
| 336 | +const failedDir = path.join(mediaDir, "failed-buffer"); |
| 337 | +const entries = await fs.readdir(failedDir).catch(() => []); |
| 338 | +expect(attemptedPaths).toHaveLength(1); |
| 339 | +expect(path.basename(attemptedPaths[0] ?? "")).toMatch(/^\..+\.tmp$/); |
| 340 | +expect(entries).toEqual([]); |
| 341 | +}); |
| 342 | +}, |
| 343 | +}, |
310 | 344 | { |
311 | 345 | name: "rejects traversal media subdirs before saving buffers", |
312 | 346 | run: async () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -345,9 +345,22 @@ async function writeSavedMediaBuffer(params: {
|
345 | 345 | buffer: Buffer; |
346 | 346 | }): Promise<string> { |
347 | 347 | const dest = path.join(params.dir, params.id); |
348 | | -await retryAfterRecreatingDir(params.dir, () => |
349 | | -fs.writeFile(dest, params.buffer, { mode: MEDIA_FILE_MODE }), |
350 | | -); |
| 348 | +await retryAfterRecreatingDir(params.dir, async () => { |
| 349 | +const tempDest = path.join(params.dir, `.${params.id}.${crypto.randomUUID()}.tmp`); |
| 350 | +try { |
| 351 | +await fs.writeFile(tempDest, params.buffer, { mode: MEDIA_FILE_MODE }); |
| 352 | +const handle = await fs.open(tempDest, "r"); |
| 353 | +try { |
| 354 | +await handle.sync(); |
| 355 | +} finally { |
| 356 | +await handle.close(); |
| 357 | +} |
| 358 | +await fs.rename(tempDest, dest); |
| 359 | +} catch (err) { |
| 360 | +await fs.rm(tempDest, { force: true }).catch(() => {}); |
| 361 | +throw err; |
| 362 | +} |
| 363 | +}); |
351 | 364 | return dest; |
352 | 365 | } |
353 | 366 | |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。