-
Notifications
You must be signed in to change notification settings - Fork 157
Expand file tree
/
Copy pathexport-stream.ts
More file actions
118 lines (106 loc) · 3.29 KB
/
Copy pathexport-stream.ts
File metadata and controls
118 lines (106 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import crypto = require('crypto');
import fs = require('fs');
export interface ExportStream {
readonly fd?: number;
write(value: string): unknown;
}
export interface Destination {
close(): void;
write(value: string): void;
}
export interface RecordWriter {
readonly records: number;
finish(record: Record<string, unknown>): void;
write(record: Record<string, unknown>): void;
}
export function nullableString(value: unknown): string | null {
return typeof value === 'string' ? value : null;
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
export function compareText(left: string, right: string): number {
if (left < right) return -1;
return left > right ? 1 : 0;
}
export function sameFileSnapshot(before: fs.BigIntStats, after: fs.BigIntStats): boolean {
return (
before.dev === after.dev &&
before.ino === after.ino &&
before.size === after.size &&
before.mtimeNs === after.mtimeNs &&
before.ctimeNs === after.ctimeNs
);
}
function writeAll(fd: number, value: string, label: string): void {
const bytes = Buffer.from(value);
let offset = 0;
while (offset < bytes.length) {
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
if (!Number.isInteger(written) || written <= 0) {
throw new Error(`${label} export destination stopped accepting bytes`);
}
offset += written;
}
}
function streamDestination(stdout: ExportStream, label: string): Destination {
if (typeof stdout.fd === 'number' && Number.isInteger(stdout.fd)) {
const fd = stdout.fd;
return { close(): void {}, write: (value) => writeAll(fd, value, label) };
}
return {
close(): void {},
write(value): void {
stdout.write(value);
},
};
}
export function createExclusiveDestination(
outputPath: string | null | undefined,
stdout: ExportStream,
label: string
): Destination {
if (!outputPath) return streamDestination(stdout, label);
const flags =
fs.constants.O_WRONLY |
fs.constants.O_CREAT |
fs.constants.O_EXCL |
(fs.constants.O_NOFOLLOW || 0);
const fd = fs.openSync(outputPath, flags, 0o600);
try {
fs.fchmodSync(fd, 0o600);
} catch (error) {
fs.closeSync(fd);
throw error;
}
return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, label) };
}
export function createReplacingDestination(
outputPath: string | null | undefined,
stdout: ExportStream
): Destination {
if (!outputPath) return streamDestination(stdout, 'JSON');
const fd = fs.openSync(outputPath, 'w');
return { close: () => fs.closeSync(fd), write: (value) => writeAll(fd, value, 'JSON') };
}
export function createRecordWriter(destination: Destination): RecordWriter {
const digest = crypto.createHash('sha256');
let records = 0;
const encode = (record: Record<string, unknown>): string => `${JSON.stringify(record)}\n`;
return {
get records(): number {
return records;
},
write(record): void {
const line = encode(record);
digest.update(line);
destination.write(line);
records += 1;
},
finish(record): void {
destination.write(
encode({ ...record, preceding_records: records, records_sha256: digest.digest('hex') })
);
},
};
}