@wirecat/zoom-cli 0.3.2 → 0.5.0
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 +109 -7
- package/dist/archive-commands.d.ts +2 -1
- package/dist/archive-commands.d.ts.map +1 -1
- package/dist/archive-commands.js +54 -10
- package/dist/archive-commands.js.map +1 -1
- package/dist/archive-import.d.ts +6 -2
- package/dist/archive-import.d.ts.map +1 -1
- package/dist/archive-import.js +32 -8
- package/dist/archive-import.js.map +1 -1
- package/dist/bin/zm.js +2 -0
- package/dist/bin/zm.js.map +1 -1
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +4 -0
- package/dist/execution.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/local-transcription-commands.d.ts +9 -0
- package/dist/local-transcription-commands.d.ts.map +1 -0
- package/dist/local-transcription-commands.js +51 -0
- package/dist/local-transcription-commands.js.map +1 -0
- package/dist/local-transcription.d.ts +52 -0
- package/dist/local-transcription.d.ts.map +1 -0
- package/dist/local-transcription.js +161 -0
- package/dist/local-transcription.js.map +1 -0
- package/dist/meeting-integration-commands.d.ts +9 -0
- package/dist/meeting-integration-commands.d.ts.map +1 -0
- package/dist/meeting-integration-commands.js +64 -0
- package/dist/meeting-integration-commands.js.map +1 -0
- package/dist/meeting-integration.d.ts +12 -0
- package/dist/meeting-integration.d.ts.map +1 -0
- package/dist/meeting-integration.js +9 -0
- package/dist/meeting-integration.js.map +1 -0
- package/dist/meeting-search-commands.d.ts +33 -0
- package/dist/meeting-search-commands.d.ts.map +1 -0
- package/dist/meeting-search-commands.js +85 -0
- package/dist/meeting-search-commands.js.map +1 -0
- package/dist/ordinary-file.d.ts +1 -0
- package/dist/ordinary-file.d.ts.map +1 -1
- package/dist/ordinary-file.js +13 -0
- package/dist/ordinary-file.js.map +1 -1
- package/dist/program.d.ts +22 -4
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +155 -4
- package/dist/program.js.map +1 -1
- package/dist/recording-archive.d.ts +2 -0
- package/dist/recording-archive.d.ts.map +1 -1
- package/dist/recording-archive.js +151 -79
- package/dist/recording-archive.js.map +1 -1
- package/dist/recording-partial-store.d.ts +20 -0
- package/dist/recording-partial-store.d.ts.map +1 -0
- package/dist/recording-partial-store.js +225 -0
- package/dist/recording-partial-store.js.map +1 -0
- package/dist/recording-promotion-store.d.ts +12 -0
- package/dist/recording-promotion-store.d.ts.map +1 -0
- package/dist/recording-promotion-store.js +92 -0
- package/dist/recording-promotion-store.js.map +1 -0
- package/dist/recording-schedule-commands.d.ts +12 -0
- package/dist/recording-schedule-commands.d.ts.map +1 -0
- package/dist/recording-schedule-commands.js +80 -0
- package/dist/recording-schedule-commands.js.map +1 -0
- package/dist/recording-schedule-store.d.ts +4 -0
- package/dist/recording-schedule-store.d.ts.map +1 -0
- package/dist/recording-schedule-store.js +141 -0
- package/dist/recording-schedule-store.js.map +1 -0
- package/dist/recording-schedule-systemd.d.ts +14 -0
- package/dist/recording-schedule-systemd.d.ts.map +1 -0
- package/dist/recording-schedule-systemd.js +173 -0
- package/dist/recording-schedule-systemd.js.map +1 -0
- package/dist/recording-schedule.d.ts +93 -0
- package/dist/recording-schedule.d.ts.map +1 -0
- package/dist/recording-schedule.js +121 -0
- package/dist/recording-schedule.js.map +1 -0
- package/dist/recording-sync-store.d.ts +3 -0
- package/dist/recording-sync-store.d.ts.map +1 -0
- package/dist/recording-sync-store.js +187 -0
- package/dist/recording-sync-store.js.map +1 -0
- package/dist/recording-sync.d.ts +10 -1
- package/dist/recording-sync.d.ts.map +1 -1
- package/dist/recording-sync.js +72 -41
- package/dist/recording-sync.js.map +1 -1
- package/dist/recordings.d.ts +9 -10
- package/dist/recordings.d.ts.map +1 -1
- package/dist/recordings.js +50 -13
- package/dist/recordings.js.map +1 -1
- package/dist/remote-meetings.d.ts +15 -0
- package/dist/remote-meetings.d.ts.map +1 -1
- package/dist/remote-meetings.js +73 -65
- package/dist/remote-meetings.js.map +1 -1
- package/dist/store-connection.d.ts +5 -1
- package/dist/store-connection.d.ts.map +1 -1
- package/dist/store-connection.js +145 -9
- package/dist/store-connection.js.map +1 -1
- package/dist/wav-audio.d.ts +28 -0
- package/dist/wav-audio.d.ts.map +1 -0
- package/dist/wav-audio.js +191 -0
- package/dist/wav-audio.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, rename, unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { checkCancelled, ZoomError } from "./errors.js";
|
|
6
|
+
import { openOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
7
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
8
|
+
const identity = (locator) => JSON.stringify([locator.provider, locator.account, locator.occurrenceId, locator.assetId]);
|
|
9
|
+
const missing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
10
|
+
const fingerprint = async (file, signal, length) => {
|
|
11
|
+
checkCancelled(signal);
|
|
12
|
+
const hash = createHash("sha256");
|
|
13
|
+
let size = 0;
|
|
14
|
+
const buffer = Buffer.alloc(65536);
|
|
15
|
+
for (;;) {
|
|
16
|
+
checkCancelled(signal);
|
|
17
|
+
const wanted = length === undefined ? buffer.length : Math.min(buffer.length, length - size);
|
|
18
|
+
if (wanted === 0)
|
|
19
|
+
break;
|
|
20
|
+
const count = (await file.read(buffer, 0, wanted, size)).bytesRead;
|
|
21
|
+
if (count === 0)
|
|
22
|
+
break;
|
|
23
|
+
size += count;
|
|
24
|
+
hash.update(buffer.subarray(0, count));
|
|
25
|
+
}
|
|
26
|
+
return { offset: size, sha256: hash.copy().digest("hex"), hash };
|
|
27
|
+
};
|
|
28
|
+
/** The caller holds the archive lease throughout load, open and promotion. */
|
|
29
|
+
export class RecordingPartialStore {
|
|
30
|
+
root;
|
|
31
|
+
locator;
|
|
32
|
+
signal;
|
|
33
|
+
persistent;
|
|
34
|
+
maxBytes;
|
|
35
|
+
path;
|
|
36
|
+
completed = null;
|
|
37
|
+
metadataPath;
|
|
38
|
+
constructor(root, locator, signal, persistent = true, maxBytes = 20 * 1024 ** 3) {
|
|
39
|
+
this.root = root;
|
|
40
|
+
this.locator = locator;
|
|
41
|
+
this.signal = signal;
|
|
42
|
+
this.persistent = persistent;
|
|
43
|
+
this.maxBytes = maxBytes;
|
|
44
|
+
this.locator = Object.freeze({
|
|
45
|
+
provider: locator.provider,
|
|
46
|
+
account: locator.account,
|
|
47
|
+
occurrenceId: locator.occurrenceId,
|
|
48
|
+
assetId: locator.assetId,
|
|
49
|
+
});
|
|
50
|
+
const key = persistent ? digest(identity(this.locator)) : randomUUID();
|
|
51
|
+
this.path = join(root, `.zoom-partial-${key}.part`);
|
|
52
|
+
this.metadataPath = join(root, `.zoom-partial-${key}.json`);
|
|
53
|
+
}
|
|
54
|
+
async load(locator) {
|
|
55
|
+
if (identity(locator) !== identity(this.locator))
|
|
56
|
+
throw new ZoomError("validation_error", "recording partial scope changed");
|
|
57
|
+
checkCancelled(this.signal);
|
|
58
|
+
if (!this.persistent)
|
|
59
|
+
return null;
|
|
60
|
+
let metadata;
|
|
61
|
+
try {
|
|
62
|
+
metadata = await openOrdinaryFile(this.metadataPath);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (missing(error))
|
|
66
|
+
return null;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
if ((await metadata.stat()).size > 16384)
|
|
72
|
+
throw new Error("metadata limit");
|
|
73
|
+
const bytes = Buffer.alloc(16385);
|
|
74
|
+
const count = (await metadata.read(bytes, 0, bytes.length, 0)).bytesRead;
|
|
75
|
+
if (count > 16384)
|
|
76
|
+
throw new Error("metadata limit");
|
|
77
|
+
value = JSON.parse(bytes.subarray(0, count).toString("utf8"));
|
|
78
|
+
if (Object.keys(value).some((key) => !["locator", "offset", "sha256", "validator", "total"].includes(key)) ||
|
|
79
|
+
identity(value.locator) !== identity(locator) ||
|
|
80
|
+
Object.keys(value.locator).some((key) => !["provider", "account", "occurrenceId", "assetId"].includes(key)) ||
|
|
81
|
+
!Number.isSafeInteger(value.offset) ||
|
|
82
|
+
value.offset < 0 ||
|
|
83
|
+
value.offset > this.maxBytes ||
|
|
84
|
+
!/^[a-f0-9]{64}$/.test(value.sha256))
|
|
85
|
+
throw new Error("metadata shape");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new ZoomError("validation_error", "recording partial metadata is invalid");
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await metadata.close();
|
|
92
|
+
}
|
|
93
|
+
const file = await openOrdinaryFile(this.path);
|
|
94
|
+
try {
|
|
95
|
+
const actual = await fingerprint(file, this.signal, value.offset);
|
|
96
|
+
if (actual.offset !== value.offset || actual.sha256 !== value.sha256)
|
|
97
|
+
throw new ZoomError("validation_error", "recording partial checksum does not match");
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await file.close();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async open(locator, options) {
|
|
105
|
+
if (identity(locator) !== identity(this.locator))
|
|
106
|
+
throw new ZoomError("validation_error", "recording partial scope changed");
|
|
107
|
+
checkCancelled(this.signal);
|
|
108
|
+
let file;
|
|
109
|
+
if (options.resume) {
|
|
110
|
+
const witness = await openOrdinaryFile(this.path);
|
|
111
|
+
try {
|
|
112
|
+
file = await open(this.path, constants.O_RDWR |
|
|
113
|
+
(process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0)));
|
|
114
|
+
const a = await witness.stat({ bigint: true });
|
|
115
|
+
const b = await file.stat({ bigint: true });
|
|
116
|
+
const after = await lstat(this.path, { bigint: true });
|
|
117
|
+
if (!b.isFile() || !after.isFile() || a.ino !== b.ino || a.dev !== b.dev || after.ino !== a.ino) {
|
|
118
|
+
await file.close();
|
|
119
|
+
throw new ZoomError("validation_error", "recording partial changed while opening");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await witness.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Replace the owned partial name atomically without following an existing symlink.
|
|
128
|
+
const temp = join(this.root, `.zoom-partial-${randomUUID()}.tmp`);
|
|
129
|
+
file = await open(temp, "wx+", 0o600);
|
|
130
|
+
try {
|
|
131
|
+
await rename(temp, this.path);
|
|
132
|
+
await unlink(this.metadataPath).catch((error) => {
|
|
133
|
+
if (!missing(error))
|
|
134
|
+
throw error;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
await file.close();
|
|
139
|
+
await unlink(temp).catch(() => undefined);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let prefix;
|
|
144
|
+
try {
|
|
145
|
+
prefix = await fingerprint(file, this.signal, options.resume?.offset);
|
|
146
|
+
if (options.resume && (prefix.offset !== options.resume.offset || prefix.sha256 !== options.resume.sha256))
|
|
147
|
+
throw new ZoomError("validation_error", "recording partial changed before append");
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
await file.close();
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (options.resume && (await file.stat()).size > prefix.offset)
|
|
154
|
+
await file.truncate(prefix.offset);
|
|
155
|
+
let size = prefix.offset;
|
|
156
|
+
const hash = prefix.hash;
|
|
157
|
+
const checkpoint = async () => {
|
|
158
|
+
await file.sync();
|
|
159
|
+
if (!options.validator || !this.persistent) {
|
|
160
|
+
await syncDirectory(this.root);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const value = {
|
|
164
|
+
locator: {
|
|
165
|
+
provider: locator.provider,
|
|
166
|
+
account: locator.account,
|
|
167
|
+
occurrenceId: locator.occurrenceId,
|
|
168
|
+
assetId: locator.assetId,
|
|
169
|
+
},
|
|
170
|
+
offset: size,
|
|
171
|
+
sha256: hash.copy().digest("hex"),
|
|
172
|
+
validator: options.validator,
|
|
173
|
+
total: options.total,
|
|
174
|
+
};
|
|
175
|
+
const content = JSON.stringify(value);
|
|
176
|
+
if (Buffer.byteLength(content) > 16384)
|
|
177
|
+
throw new ZoomError("validation_error", "recording partial metadata exceeds its byte limit");
|
|
178
|
+
const temp = join(this.root, `.zoom-partial-${randomUUID()}.tmp`);
|
|
179
|
+
const metadata = await open(temp, "wx", 0o600);
|
|
180
|
+
try {
|
|
181
|
+
try {
|
|
182
|
+
await metadata.writeFile(content);
|
|
183
|
+
await metadata.sync();
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await metadata.close();
|
|
187
|
+
}
|
|
188
|
+
await rename(temp, this.metadataPath);
|
|
189
|
+
await syncDirectory(this.root);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await unlink(temp).catch(() => undefined);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
write: async (bytes) => {
|
|
197
|
+
let offset = 0;
|
|
198
|
+
while (offset < bytes.length) {
|
|
199
|
+
const written = (await file.write(bytes, offset, bytes.length - offset, size + offset)).bytesWritten;
|
|
200
|
+
if (written === 0)
|
|
201
|
+
throw new ZoomError("configuration_error", "recording partial write did not progress");
|
|
202
|
+
offset += written;
|
|
203
|
+
}
|
|
204
|
+
hash.update(bytes);
|
|
205
|
+
size += bytes.length;
|
|
206
|
+
},
|
|
207
|
+
checkpoint,
|
|
208
|
+
finish: async () => {
|
|
209
|
+
await checkpoint();
|
|
210
|
+
this.completed = options.validator
|
|
211
|
+
? {
|
|
212
|
+
locator: { ...this.locator },
|
|
213
|
+
offset: size,
|
|
214
|
+
sha256: hash.copy().digest("hex"),
|
|
215
|
+
validator: options.validator,
|
|
216
|
+
total: options.total,
|
|
217
|
+
}
|
|
218
|
+
: null;
|
|
219
|
+
return { size, sha256: hash.copy().digest("hex") };
|
|
220
|
+
},
|
|
221
|
+
close: () => file.close(),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=recording-partial-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-partial-store.js","sourceRoot":"","sources":["../src/recording-partial-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RecordingAssetLocator, type RecordingPartial } from "@wirecat/cli-meetings";
|
|
2
|
+
import type { ArchivedFile, ArchiveManifest } from "./recording-archive.js";
|
|
3
|
+
export interface RecordingPromotion {
|
|
4
|
+
version: 1;
|
|
5
|
+
locator: RecordingAssetLocator;
|
|
6
|
+
meeting: ArchiveManifest["meeting"];
|
|
7
|
+
file: ArchivedFile;
|
|
8
|
+
validator: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const writeRecordingPromotion: (root: string, locator: RecordingAssetLocator, meeting: ArchiveManifest["meeting"], file: ArchivedFile, completed: RecordingPartial) => Promise<void>;
|
|
11
|
+
export declare const readRecordingPromotion: (root: string, locator: RecordingAssetLocator, meeting: ArchiveManifest["meeting"], expected: Omit<ArchivedFile, "size" | "sha256">) => Promise<RecordingPromotion | null>;
|
|
12
|
+
//# sourceMappingURL=recording-promotion-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-promotion-store.d.ts","sourceRoot":"","sources":["../src/recording-promotion-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,qBAAqB,EAAE,KAAK,gBAAgB,EAA4B,MAAM,uBAAuB,CAAA;AAGnH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE3E,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,CAAA;IACV,OAAO,EAAE,qBAAqB,CAAA;IAC9B,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAA;IACnC,IAAI,EAAE,YAAY,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAiBD,eAAO,MAAM,uBAAuB,SAC5B,MAAM,WACH,qBAAqB,WACrB,eAAe,CAAC,SAAS,CAAC,QAC7B,YAAY,aACP,gBAAgB,kBA+B5B,CAAA;AACD,eAAO,MAAM,sBAAsB,SAC3B,MAAM,WACH,qBAAqB,WACrB,eAAe,CAAC,SAAS,CAAC,YACzB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC,KAC9C,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAqCnC,CAAA"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { open, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { strongRecordingValidator } from "@wirecat/cli-meetings";
|
|
5
|
+
import { ZoomError } from "./errors.js";
|
|
6
|
+
import { openOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
7
|
+
const missing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
8
|
+
const scope = (locator) => [
|
|
9
|
+
locator.provider,
|
|
10
|
+
locator.account,
|
|
11
|
+
locator.occurrenceId,
|
|
12
|
+
locator.assetId,
|
|
13
|
+
];
|
|
14
|
+
const name = (root, locator) => join(root, `.zoom-promotion-${createHash("sha256")
|
|
15
|
+
.update(JSON.stringify(scope(locator)))
|
|
16
|
+
.digest("hex")}.json`);
|
|
17
|
+
const keys = (value, allowed) => Object.keys(value).every((key) => allowed.includes(key));
|
|
18
|
+
export const writeRecordingPromotion = async (root, locator, meeting, file, completed) => {
|
|
19
|
+
const record = {
|
|
20
|
+
version: 1,
|
|
21
|
+
locator: {
|
|
22
|
+
provider: locator.provider,
|
|
23
|
+
account: locator.account,
|
|
24
|
+
occurrenceId: locator.occurrenceId,
|
|
25
|
+
assetId: locator.assetId,
|
|
26
|
+
},
|
|
27
|
+
meeting: { ...meeting },
|
|
28
|
+
file: { ...file },
|
|
29
|
+
validator: completed.validator,
|
|
30
|
+
};
|
|
31
|
+
const content = JSON.stringify(record);
|
|
32
|
+
if (Buffer.byteLength(content) > 32768)
|
|
33
|
+
throw new ZoomError("validation_error", "recording recovery record exceeds its metadata limit");
|
|
34
|
+
const temp = join(root, `.zoom-promotion-${randomUUID()}.tmp`);
|
|
35
|
+
const handle = await open(temp, "wx", 0o600);
|
|
36
|
+
try {
|
|
37
|
+
try {
|
|
38
|
+
await handle.writeFile(content);
|
|
39
|
+
await handle.sync();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await handle.close();
|
|
43
|
+
}
|
|
44
|
+
await rename(temp, name(root, locator));
|
|
45
|
+
await syncDirectory(root);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await unlink(temp).catch(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export const readRecordingPromotion = async (root, locator, meeting, expected) => {
|
|
52
|
+
let handle;
|
|
53
|
+
try {
|
|
54
|
+
handle = await openOrdinaryFile(name(root, locator));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (missing(error))
|
|
58
|
+
return null;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const buffer = Buffer.alloc(32769);
|
|
63
|
+
const count = (await handle.read(buffer, 0, buffer.length, 0)).bytesRead;
|
|
64
|
+
if (count > 32768)
|
|
65
|
+
throw new Error("metadata limit");
|
|
66
|
+
const record = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
67
|
+
if (record.version !== 1 ||
|
|
68
|
+
!keys(record, ["version", "locator", "meeting", "file", "validator"]) ||
|
|
69
|
+
!keys(record.locator, ["provider", "account", "occurrenceId", "assetId"]) ||
|
|
70
|
+
JSON.stringify(scope(record.locator)) !== JSON.stringify(scope(locator)) ||
|
|
71
|
+
!keys(record.meeting, ["id", "uuid", "title", "startedAt"]) ||
|
|
72
|
+
record.meeting.id !== meeting.id ||
|
|
73
|
+
record.meeting.uuid !== meeting.uuid ||
|
|
74
|
+
record.meeting.title !== meeting.title ||
|
|
75
|
+
record.meeting.startedAt !== meeting.startedAt ||
|
|
76
|
+
!keys(record.file, ["id", "path", "remoteSize", "type", "format", "startedAt", "endedAt", "size", "sha256"]) ||
|
|
77
|
+
Object.entries(expected).some(([key, value]) => record.file[key] !== value) ||
|
|
78
|
+
!Number.isSafeInteger(record.file.size) ||
|
|
79
|
+
record.file.size < 0 ||
|
|
80
|
+
!/^[a-f0-9]{64}$/.test(record.file.sha256) ||
|
|
81
|
+
!strongRecordingValidator(record.validator))
|
|
82
|
+
throw new Error("record shape");
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new ZoomError("validation_error", "recording recovery record does not match this scoped asset");
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await handle.close();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=recording-promotion-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-promotion-store.js","sourceRoot":"","sources":["../src/recording-promotion-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { RecordingScheduleCapture, RecordingScheduleService } from "./recording-schedule.js";
|
|
3
|
+
export declare const addRecordingScheduleCommands: (program: Command, deps: {
|
|
4
|
+
service: () => RecordingScheduleService;
|
|
5
|
+
capture: () => RecordingScheduleCapture;
|
|
6
|
+
offline: () => boolean;
|
|
7
|
+
signal: () => AbortSignal | undefined;
|
|
8
|
+
output: (value: unknown) => void;
|
|
9
|
+
receipt: (value: unknown) => Promise<void>;
|
|
10
|
+
outputIsTTY?: boolean;
|
|
11
|
+
}) => void;
|
|
12
|
+
//# sourceMappingURL=recording-schedule-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-schedule-commands.d.ts","sourceRoot":"","sources":["../src/recording-schedule-commands.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGxC,OAAO,KAAK,EACV,wBAAwB,EAExB,wBAAwB,EACzB,MAAM,yBAAyB,CAAA;AAEhC,eAAO,MAAM,4BAA4B,YAC9B,OAAO,QACV;IACJ,OAAO,EAAE,MAAM,wBAAwB,CAAA;IACvC,OAAO,EAAE,MAAM,wBAAwB,CAAA;IACvC,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,MAAM,EAAE,MAAM,WAAW,GAAG,SAAS,CAAA;IACrC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,KACA,IA6GF,CAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { annotate } from "@wirecat/cli-core/commands";
|
|
2
|
+
import { listed } from "@wirecat/cli-meetings";
|
|
3
|
+
import { ZoomError } from "./errors.js";
|
|
4
|
+
import { bytes } from "./execution.js";
|
|
5
|
+
export const addRecordingScheduleCommands = (program, deps) => {
|
|
6
|
+
const deliver = async (value) => {
|
|
7
|
+
if (value.applied)
|
|
8
|
+
await deps.receipt(value);
|
|
9
|
+
else
|
|
10
|
+
deps.output(value);
|
|
11
|
+
};
|
|
12
|
+
const recordings = program.commands.find((command) => command.name() === "recordings");
|
|
13
|
+
if (!recordings)
|
|
14
|
+
throw new ZoomError("configuration_error", "recording command group is unavailable");
|
|
15
|
+
const schedule = recordings.command("schedule").description("local archive jobs and Linux user systemd timers");
|
|
16
|
+
const label = (command, example, mutates = false) => annotate(command, { origin: "handwritten", mutates, ...(mutates ? { local: true } : {}), examples: [example] });
|
|
17
|
+
label(schedule
|
|
18
|
+
.command("create")
|
|
19
|
+
.description("preview a captured daily archive job; --yes installs its user timer")
|
|
20
|
+
.requiredOption("--name <name>", "local job name")
|
|
21
|
+
.requiredOption("--destination <directory>", "absolute archive directory")
|
|
22
|
+
.requiredOption("--from <date>", "first YYYY-MM-DD archive date")
|
|
23
|
+
.requiredOption("--at <time>", "daily HH:mm time")
|
|
24
|
+
.requiredOption("--timezone <zone>", "IANA timezone for the timer")
|
|
25
|
+
.option("--lookback-days <days>", "revisit the latest zero through 31 UTC days (default seven)", bytes)
|
|
26
|
+
.option("--max-bytes <bytes>", "maximum bytes per recording file (default 20 GiB)", bytes)
|
|
27
|
+
.option("--yes", "install and enable the captured timer")
|
|
28
|
+
.option("--dry-run", "preview even when --yes is present")
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
const offline = deps.offline();
|
|
31
|
+
if (options.yes && !options.dryRun && offline)
|
|
32
|
+
throw new ZoomError("configuration_error", "archive schedule installation requires online mode");
|
|
33
|
+
const capture = deps.capture();
|
|
34
|
+
const service = deps.service();
|
|
35
|
+
await deliver(await service.create({
|
|
36
|
+
name: options.name,
|
|
37
|
+
destination: options.destination,
|
|
38
|
+
from: options.from,
|
|
39
|
+
dailyAt: options.at,
|
|
40
|
+
timeZone: options.timezone,
|
|
41
|
+
lookbackDays: options.lookbackDays,
|
|
42
|
+
maxBytes: options.maxBytes,
|
|
43
|
+
}, capture, { apply: options.yes, dryRun: options.dryRun, offline, signal: deps.signal() }));
|
|
44
|
+
}), "zm recordings schedule create --name daily --destination /home/AliceExample/archive --from 2026-01-01 --at 03:15 --timezone UTC --json", true);
|
|
45
|
+
label(schedule
|
|
46
|
+
.command("list")
|
|
47
|
+
.description("list saved local jobs; live timer state is not checked")
|
|
48
|
+
.action(async () => {
|
|
49
|
+
deps.output(listed(await deps.service().list({ signal: deps.signal() })));
|
|
50
|
+
}), "zm recordings schedule list --offline --json");
|
|
51
|
+
label(schedule
|
|
52
|
+
.command("show")
|
|
53
|
+
.description("show one saved local job and its captured settings")
|
|
54
|
+
.argument("<id>", "schedule id")
|
|
55
|
+
.action(async (id) => {
|
|
56
|
+
deps.output(await deps.service().show(id, { signal: deps.signal() }));
|
|
57
|
+
}), "zm recordings schedule show <schedule-id> --offline --json");
|
|
58
|
+
label(schedule
|
|
59
|
+
.command("run")
|
|
60
|
+
.description("run the captured job once with durable archive progress")
|
|
61
|
+
.argument("<id>", "schedule id")
|
|
62
|
+
.action(async (id) => {
|
|
63
|
+
if (program.opts().json || (!program.opts().jsonl && !deps.outputIsTTY))
|
|
64
|
+
throw new ZoomError("validation_error", "headless archive schedule runs stream receipts; use --jsonl");
|
|
65
|
+
if (deps.offline())
|
|
66
|
+
throw new ZoomError("configuration_error", "archive schedule run requires online mode");
|
|
67
|
+
for await (const receipt of deps.service().run(id, { offline: deps.offline(), signal: deps.signal() }))
|
|
68
|
+
await deps.receipt(receipt);
|
|
69
|
+
}), "zm recordings schedule run <schedule-id> --jsonl", true);
|
|
70
|
+
label(schedule
|
|
71
|
+
.command("disable")
|
|
72
|
+
.description("preview disabling a saved timer; --yes disables it without deleting files")
|
|
73
|
+
.argument("<id>", "schedule id")
|
|
74
|
+
.option("--yes", "disable and stop the timer")
|
|
75
|
+
.option("--dry-run", "preview even when --yes is present")
|
|
76
|
+
.action(async (id, options) => {
|
|
77
|
+
await deliver(await deps.service().disable(id, { apply: options.yes, dryRun: options.dryRun, signal: deps.signal() }));
|
|
78
|
+
}), "zm recordings schedule disable <schedule-id> --yes --json", true);
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=recording-schedule-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-schedule-commands.js","sourceRoot":"","sources":["../src/recording-schedule-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RecordingScheduleStore } from "./recording-schedule.js";
|
|
2
|
+
export declare const recordingScheduleDirectory: (directory: string, create: boolean) => Promise<boolean>;
|
|
3
|
+
export declare const fileRecordingScheduleStore: (directory: string) => RecordingScheduleStore;
|
|
4
|
+
//# sourceMappingURL=recording-schedule-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-schedule-store.d.ts","sourceRoot":"","sources":["../src/recording-schedule-store.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,sBAAsB,EAE5B,MAAM,yBAAyB,CAAA;AAKhC,eAAO,MAAM,0BAA0B,cAAqB,MAAM,UAAU,OAAO,KAAG,OAAO,CAAC,OAAO,CA0BpG,CAAA;AA4BD,eAAO,MAAM,0BAA0B,cAAe,MAAM,KAAG,sBAoE9D,CAAA"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, lstat, mkdir, open, readdir, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, parse, relative, sep } from "node:path";
|
|
4
|
+
import { checkCancelled, ZoomError } from "./errors.js";
|
|
5
|
+
import { openOrdinaryFile, syncDirectory } from "./ordinary-file.js";
|
|
6
|
+
import { parseRecordingScheduleJob, validateRecordingScheduleId, } from "./recording-schedule.js";
|
|
7
|
+
const maximumBytes = 32 * 1024;
|
|
8
|
+
const missing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
9
|
+
export const recordingScheduleDirectory = async (directory, create) => {
|
|
10
|
+
if (!isAbsolute(directory) || /[\0\r\n]/.test(directory))
|
|
11
|
+
throw new ZoomError("validation_error", "archive schedule directory must be absolute");
|
|
12
|
+
const root = parse(directory).root;
|
|
13
|
+
let path = root;
|
|
14
|
+
for (const segment of relative(root, directory).split(sep).filter(Boolean)) {
|
|
15
|
+
path = join(path, segment);
|
|
16
|
+
try {
|
|
17
|
+
const stat = await lstat(path);
|
|
18
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
19
|
+
throw new ZoomError("validation_error", "archive schedule directories must not contain symlinks or foreign files");
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!missing(error))
|
|
23
|
+
throw error;
|
|
24
|
+
if (!create)
|
|
25
|
+
return false;
|
|
26
|
+
await mkdir(path, { mode: 0o700 }).catch(async (cause) => {
|
|
27
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause) || cause.code !== "EEXIST")
|
|
28
|
+
throw cause;
|
|
29
|
+
const stat = await lstat(path);
|
|
30
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
31
|
+
throw new ZoomError("validation_error", "archive schedule directory changed while creating it");
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
const read = async (path, signal) => {
|
|
38
|
+
checkCancelled(signal);
|
|
39
|
+
let handle;
|
|
40
|
+
try {
|
|
41
|
+
handle = await openOrdinaryFile(path);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (missing(error))
|
|
45
|
+
return null;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const stat = await handle.stat({ bigint: true });
|
|
50
|
+
if (stat.size > BigInt(maximumBytes))
|
|
51
|
+
throw new ZoomError("validation_error", "archive schedule metadata exceeds its byte limit");
|
|
52
|
+
const bytes = Buffer.alloc(maximumBytes + 1);
|
|
53
|
+
const size = (await handle.read(bytes, 0, bytes.length, 0)).bytesRead;
|
|
54
|
+
checkCancelled(signal);
|
|
55
|
+
if (size > maximumBytes)
|
|
56
|
+
throw new ZoomError("validation_error", "archive schedule metadata exceeds its byte limit");
|
|
57
|
+
const job = parseRecordingScheduleJob(JSON.parse(bytes.subarray(0, size).toString("utf8")));
|
|
58
|
+
return { job, stat };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof ZoomError)
|
|
62
|
+
throw error;
|
|
63
|
+
checkCancelled(signal);
|
|
64
|
+
throw new ZoomError("validation_error", "archive schedule metadata is invalid");
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await handle.close();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
export const fileRecordingScheduleStore = (directory) => {
|
|
71
|
+
const path = (id) => join(directory, `${validateRecordingScheduleId(id)}.json`);
|
|
72
|
+
return {
|
|
73
|
+
async list(options) {
|
|
74
|
+
checkCancelled(options.signal);
|
|
75
|
+
if (!(await recordingScheduleDirectory(directory, false)))
|
|
76
|
+
return [];
|
|
77
|
+
const result = [];
|
|
78
|
+
for (const name of (await readdir(directory)).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort()) {
|
|
79
|
+
const saved = await read(join(directory, name), options.signal);
|
|
80
|
+
if (!saved || `${saved.job.schedule.id}.json` !== name)
|
|
81
|
+
throw new ZoomError("validation_error", "archive schedule filename does not match its identity");
|
|
82
|
+
result.push(saved.job);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
async load(id, options) {
|
|
87
|
+
const target = path(id);
|
|
88
|
+
checkCancelled(options.signal);
|
|
89
|
+
if (!(await recordingScheduleDirectory(directory, false)))
|
|
90
|
+
return null;
|
|
91
|
+
const saved = await read(target, options.signal);
|
|
92
|
+
if (saved && saved.job.schedule.id !== id)
|
|
93
|
+
throw new ZoomError("validation_error", "archive schedule filename does not match its identity");
|
|
94
|
+
return saved?.job ?? null;
|
|
95
|
+
},
|
|
96
|
+
async save(value, options) {
|
|
97
|
+
const job = parseRecordingScheduleJob(value);
|
|
98
|
+
const content = `${JSON.stringify(job)}\n`;
|
|
99
|
+
if (Buffer.byteLength(content) > maximumBytes)
|
|
100
|
+
throw new ZoomError("validation_error", "archive schedule metadata exceeds its byte limit");
|
|
101
|
+
checkCancelled(options.signal);
|
|
102
|
+
await recordingScheduleDirectory(directory, true);
|
|
103
|
+
const target = path(job.schedule.id);
|
|
104
|
+
const existing = await read(target, options.signal);
|
|
105
|
+
if (existing &&
|
|
106
|
+
(existing.job.schedule.id !== job.schedule.id ||
|
|
107
|
+
JSON.stringify(existing.job.capture) !== JSON.stringify(job.capture)))
|
|
108
|
+
throw new ZoomError("validation_error", "archive schedule file belongs to another captured job");
|
|
109
|
+
const temp = join(directory, `.zoom-schedule-${randomUUID()}.tmp`);
|
|
110
|
+
const handle = await open(temp, "wx", 0o600);
|
|
111
|
+
try {
|
|
112
|
+
try {
|
|
113
|
+
await handle.writeFile(content);
|
|
114
|
+
await handle.sync();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await handle.close();
|
|
118
|
+
}
|
|
119
|
+
checkCancelled(options.signal);
|
|
120
|
+
if (existing) {
|
|
121
|
+
const current = await lstat(target, { bigint: true });
|
|
122
|
+
if (!current.isFile() ||
|
|
123
|
+
current.isSymbolicLink() ||
|
|
124
|
+
current.ino !== existing.stat.ino ||
|
|
125
|
+
current.size !== existing.stat.size ||
|
|
126
|
+
current.mtimeNs !== existing.stat.mtimeNs ||
|
|
127
|
+
(current.dev !== 0n && current.dev !== existing.stat.dev && process.platform !== "win32"))
|
|
128
|
+
throw new ZoomError("validation_error", "archive schedule file changed before replacement");
|
|
129
|
+
await rename(temp, target);
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
await link(temp, target);
|
|
133
|
+
await syncDirectory(directory);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await unlink(temp).catch(() => undefined);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=recording-schedule-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-schedule-store.js","sourceRoot":"","sources":["../src/recording-schedule-store.ts"],"names":[],"mappings":"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"}
|