mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(google-meet): publish exports atomically (#122306)
This commit is contained in:
committed by
GitHub
parent
b350f76484
commit
c4fd5ad551
@@ -0,0 +1,88 @@
|
||||
import fs from "node:fs";
|
||||
import fsp from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import JSZip from "jszip";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { writeMeetExportBundle } from "./cli-export.js";
|
||||
import type { GoogleMeetArtifactsResult, GoogleMeetAttendanceResult } from "./meet-api.js";
|
||||
|
||||
const emptyArtifacts: GoogleMeetArtifactsResult = {
|
||||
conferenceRecords: [],
|
||||
artifacts: [],
|
||||
};
|
||||
|
||||
const emptyAttendance: GoogleMeetAttendanceResult = {
|
||||
conferenceRecords: [],
|
||||
attendance: [],
|
||||
};
|
||||
|
||||
describe("Google Meet export publication", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-export-publication-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("keeps an existing bundle member when replacement fails", async () => {
|
||||
const outputDir = path.join(tempDir, "bundle");
|
||||
const summaryPath = path.join(outputDir, "summary.md");
|
||||
fs.mkdirSync(outputDir);
|
||||
fs.writeFileSync(summaryPath, "previous summary\n");
|
||||
const priorBytes = fs.readFileSync(summaryPath);
|
||||
|
||||
vi.spyOn(fsp, "writeFile").mockImplementationOnce(async (file) => {
|
||||
expect(typeof file).toBe("string");
|
||||
fs.writeFileSync(file as string, "partial replacement");
|
||||
throw new Error("injected write failure");
|
||||
});
|
||||
|
||||
await expect(
|
||||
writeMeetExportBundle({
|
||||
outputDir,
|
||||
artifacts: emptyArtifacts,
|
||||
attendance: emptyAttendance,
|
||||
}),
|
||||
).rejects.toThrow("injected write failure");
|
||||
|
||||
expect(fs.readFileSync(summaryPath)).toEqual(priorBytes);
|
||||
expect(fs.readdirSync(outputDir)).toEqual(["summary.md"]);
|
||||
});
|
||||
|
||||
it("keeps an existing ZIP when replacement fails", async () => {
|
||||
const outputDir = path.join(tempDir, "bundle");
|
||||
const zipPath = `${outputDir}.zip`;
|
||||
const priorZip = await new JSZip()
|
||||
.file("previous.txt", "previous export")
|
||||
.generateAsync({ type: "nodebuffer" });
|
||||
fs.writeFileSync(zipPath, priorZip);
|
||||
const realWriteFile = fsp.writeFile;
|
||||
|
||||
vi.spyOn(fsp, "writeFile").mockImplementation(async (...args) => {
|
||||
const [file, data] = args;
|
||||
if (Buffer.isBuffer(data)) {
|
||||
expect(typeof file).toBe("string");
|
||||
fs.writeFileSync(file as string, "partial replacement");
|
||||
throw new Error("injected ZIP write failure");
|
||||
}
|
||||
await Reflect.apply(realWriteFile, fsp, args);
|
||||
});
|
||||
|
||||
await expect(
|
||||
writeMeetExportBundle({
|
||||
outputDir,
|
||||
artifacts: emptyArtifacts,
|
||||
attendance: emptyAttendance,
|
||||
zip: true,
|
||||
}),
|
||||
).rejects.toThrow("injected ZIP write failure");
|
||||
|
||||
expect(fs.readFileSync(zipPath)).toEqual(priorZip);
|
||||
expect(fs.readdirSync(tempDir).toSorted()).toEqual(["bundle", "bundle.zip"]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import fsp from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import JSZip from "jszip";
|
||||
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { listGoogleMeetCalendarEvents, type GoogleMeetCalendarLookupResult } from "./calendar.js";
|
||||
import {
|
||||
formatDuration,
|
||||
@@ -522,6 +523,17 @@ function defaultExportDirectory(): string {
|
||||
return `google-meet-export-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
||||
}
|
||||
|
||||
async function publishMeetExportFile(outputPath: string, content: string | Buffer): Promise<void> {
|
||||
const absolutePath = path.resolve(outputPath);
|
||||
await writeExternalFileWithinRoot({
|
||||
rootDir: path.dirname(absolutePath),
|
||||
path: path.basename(absolutePath),
|
||||
write: async (tempPath) => {
|
||||
await fsp.writeFile(tempPath, content);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function writeMeetExportBundle(params: {
|
||||
outputDir?: string;
|
||||
artifacts: GoogleMeetArtifactsResult;
|
||||
@@ -532,7 +544,7 @@ export async function writeMeetExportBundle(params: {
|
||||
calendarEvent?: GoogleMeetCalendarLookupResult;
|
||||
}): Promise<{ outputDir: string; files: string[]; zipFile?: string }> {
|
||||
const outputDir = params.outputDir?.trim() || defaultExportDirectory();
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await fsp.mkdir(outputDir, { recursive: true });
|
||||
const zipFile = params.zip ? `${outputDir.replace(/\/$/, "")}.zip` : undefined;
|
||||
const fileNames = googleMeetExportFileNames();
|
||||
const files = [
|
||||
@@ -562,7 +574,7 @@ export async function writeMeetExportBundle(params: {
|
||||
},
|
||||
];
|
||||
for (const file of files) {
|
||||
await writeFile(path.join(outputDir, file.name), file.content, "utf8");
|
||||
await publishMeetExportFile(path.join(outputDir, file.name), file.content);
|
||||
}
|
||||
const result: { outputDir: string; files: string[]; zipFile?: string } = {
|
||||
outputDir,
|
||||
@@ -573,7 +585,7 @@ export async function writeMeetExportBundle(params: {
|
||||
for (const file of files) {
|
||||
zip.file(file.name, file.content);
|
||||
}
|
||||
await writeFile(zipFile, await zip.generateAsync({ type: "nodebuffer" }));
|
||||
await publishMeetExportFile(zipFile, await zip.generateAsync({ type: "nodebuffer" }));
|
||||
result.zipFile = zipFile;
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user