@vertesia/memory 0.24.0-dev.202601221707
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/LICENSE +13 -0
- package/README.md +473 -0
- package/lib/cjs/Builder.js +186 -0
- package/lib/cjs/Builder.js.map +1 -0
- package/lib/cjs/ContentObject.js +114 -0
- package/lib/cjs/ContentObject.js.map +1 -0
- package/lib/cjs/ContentSource.js +82 -0
- package/lib/cjs/ContentSource.js.map +1 -0
- package/lib/cjs/MemoryPack.js +228 -0
- package/lib/cjs/MemoryPack.js.map +1 -0
- package/lib/cjs/MemoryPackBuilder.js +47 -0
- package/lib/cjs/MemoryPackBuilder.js.map +1 -0
- package/lib/cjs/commands/copy.js +53 -0
- package/lib/cjs/commands/copy.js.map +1 -0
- package/lib/cjs/commands/exec.js +82 -0
- package/lib/cjs/commands/exec.js.map +1 -0
- package/lib/cjs/index.js +28 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/package.json +3 -0
- package/lib/cjs/utils/cmdline.js +90 -0
- package/lib/cjs/utils/cmdline.js.map +1 -0
- package/lib/cjs/utils/rewrite.js +166 -0
- package/lib/cjs/utils/rewrite.js.map +1 -0
- package/lib/cjs/utils/stream.js +27 -0
- package/lib/cjs/utils/stream.js.map +1 -0
- package/lib/cjs/utils/tar.js +185 -0
- package/lib/cjs/utils/tar.js.map +1 -0
- package/lib/esm/Builder.js +178 -0
- package/lib/esm/Builder.js.map +1 -0
- package/lib/esm/ContentObject.js +103 -0
- package/lib/esm/ContentObject.js.map +1 -0
- package/lib/esm/ContentSource.js +75 -0
- package/lib/esm/ContentSource.js.map +1 -0
- package/lib/esm/MemoryPack.js +218 -0
- package/lib/esm/MemoryPack.js.map +1 -0
- package/lib/esm/MemoryPackBuilder.js +43 -0
- package/lib/esm/MemoryPackBuilder.js.map +1 -0
- package/lib/esm/commands/copy.js +50 -0
- package/lib/esm/commands/copy.js.map +1 -0
- package/lib/esm/commands/exec.js +75 -0
- package/lib/esm/commands/exec.js.map +1 -0
- package/lib/esm/index.js +7 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/utils/cmdline.js +86 -0
- package/lib/esm/utils/cmdline.js.map +1 -0
- package/lib/esm/utils/rewrite.js +161 -0
- package/lib/esm/utils/rewrite.js.map +1 -0
- package/lib/esm/utils/stream.js +23 -0
- package/lib/esm/utils/stream.js.map +1 -0
- package/lib/esm/utils/tar.js +175 -0
- package/lib/esm/utils/tar.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/Builder.d.ts +72 -0
- package/lib/types/Builder.d.ts.map +1 -0
- package/lib/types/ContentObject.d.ts +43 -0
- package/lib/types/ContentObject.d.ts.map +1 -0
- package/lib/types/ContentSource.d.ts +32 -0
- package/lib/types/ContentSource.d.ts.map +1 -0
- package/lib/types/MemoryPack.d.ts +46 -0
- package/lib/types/MemoryPack.d.ts.map +1 -0
- package/lib/types/MemoryPackBuilder.d.ts +18 -0
- package/lib/types/MemoryPackBuilder.d.ts.map +1 -0
- package/lib/types/commands/copy.d.ts +8 -0
- package/lib/types/commands/copy.d.ts.map +1 -0
- package/lib/types/commands/exec.d.ts +7 -0
- package/lib/types/commands/exec.d.ts.map +1 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/utils/cmdline.d.ts +10 -0
- package/lib/types/utils/cmdline.d.ts.map +1 -0
- package/lib/types/utils/rewrite.d.ts +38 -0
- package/lib/types/utils/rewrite.d.ts.map +1 -0
- package/lib/types/utils/stream.d.ts +9 -0
- package/lib/types/utils/stream.d.ts.map +1 -0
- package/lib/types/utils/tar.d.ts +40 -0
- package/lib/types/utils/tar.d.ts.map +1 -0
- package/package.json +53 -0
- package/src/Builder.ts +239 -0
- package/src/ContentObject.ts +114 -0
- package/src/ContentSource.ts +88 -0
- package/src/MemoryPack.ts +233 -0
- package/src/MemoryPackBuilder.ts +55 -0
- package/src/builder.test.ts +214 -0
- package/src/commands/copy.ts +53 -0
- package/src/commands/exec.test.ts +22 -0
- package/src/commands/exec.ts +83 -0
- package/src/index.ts +14 -0
- package/src/utils/cmdline.test.ts +32 -0
- package/src/utils/cmdline.ts +92 -0
- package/src/utils/rewrite.test.ts +65 -0
- package/src/utils/rewrite.ts +167 -0
- package/src/utils/stream.test.ts +13 -0
- package/src/utils/stream.ts +27 -0
- package/src/utils/tar.test.ts +48 -0
- package/src/utils/tar.ts +203 -0
package/src/Builder.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { AsyncObjectWalker } from "@vertesia/json";
|
|
2
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
import { copy, CopyOptions } from "./commands/copy.js";
|
|
6
|
+
import { exec, ExecOptions } from "./commands/exec.js";
|
|
7
|
+
import { ContentObject, DocxObject, JsonObject, MediaObject, MediaOptions, PdfObject } from "./ContentObject.js";
|
|
8
|
+
import { AbstractContentSource, ContentSource, SourceSpec, TextSource } from "./ContentSource.js";
|
|
9
|
+
import { loadMemoryPack } from "./MemoryPack.js";
|
|
10
|
+
import { FromOptions, MemoryPackBuilder } from "./MemoryPackBuilder.js";
|
|
11
|
+
|
|
12
|
+
export interface BuildOptions {
|
|
13
|
+
indent?: number;
|
|
14
|
+
/**
|
|
15
|
+
* the path to save the output. Defaults to 'memory.tar'.
|
|
16
|
+
* If no .tar extension is present it will be added
|
|
17
|
+
*/
|
|
18
|
+
out?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* If set, suppress logs. Defaults to false.
|
|
22
|
+
*/
|
|
23
|
+
quiet?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* If true, compress the output (tar or json) with gzip. Defaults to false.
|
|
27
|
+
*/
|
|
28
|
+
gzip?: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Vars to be injected into the script context as the vars object
|
|
32
|
+
*/
|
|
33
|
+
vars?: Record<string, any>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Optional publish action
|
|
37
|
+
* @param file
|
|
38
|
+
* @returns the URI of the published memory
|
|
39
|
+
*/
|
|
40
|
+
publish?: (file: string, name: string) => Promise<string>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The directory where to transpile the recipe ts file
|
|
44
|
+
*/
|
|
45
|
+
transpileDir?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Commands {
|
|
49
|
+
vars: () => Record<string, any>;
|
|
50
|
+
tmpdir: () => string;
|
|
51
|
+
exec: (cmd: string, options?: ExecOptions) => Promise<void | string>;
|
|
52
|
+
from: (location: string, options?: FromOptions) => Promise<void>;
|
|
53
|
+
content: (location: string, encoding?: BufferEncoding) => ContentObject | ContentObject[];
|
|
54
|
+
json: (location: string) => ContentObject | ContentObject[];
|
|
55
|
+
pdf: (location: string) => PdfObject | PdfObject[];
|
|
56
|
+
docx: (location: string) => DocxObject | DocxObject[];
|
|
57
|
+
media: (location: string, options?: MediaOptions) => MediaObject | MediaObject[];
|
|
58
|
+
copy: (location: SourceSpec, path: string, options?: CopyOptions) => void;
|
|
59
|
+
copyText: (text: string, path: string) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class Builder implements Commands {
|
|
63
|
+
static instance?: Builder;
|
|
64
|
+
|
|
65
|
+
static getInstance() {
|
|
66
|
+
if (!Builder.instance) {
|
|
67
|
+
throw new Error("No builder instance found");
|
|
68
|
+
}
|
|
69
|
+
return Builder.instance;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_vars: Record<string, any>;
|
|
73
|
+
_tmpdir?: string;
|
|
74
|
+
memory: MemoryPackBuilder;
|
|
75
|
+
|
|
76
|
+
constructor(public options: BuildOptions = {}) {
|
|
77
|
+
this.memory = new MemoryPackBuilder(this);
|
|
78
|
+
this._vars = options.vars || {};
|
|
79
|
+
Builder.instance = this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
vars(): Record<string, any> {
|
|
83
|
+
return this._vars;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
tmpdir() {
|
|
87
|
+
if (!this._tmpdir) {
|
|
88
|
+
this._tmpdir = mkdtempSync(join(os.tmpdir(), 'becomposable-memo-'));
|
|
89
|
+
}
|
|
90
|
+
return this._tmpdir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
from(location: string, options?: FromOptions) {
|
|
94
|
+
return loadMemoryPack(location).then((memory) => {
|
|
95
|
+
return this.memory.load(memory, options);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
addEntry(path: string, source: ContentSource) {
|
|
100
|
+
this.memory.add(path, source);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
copy(location: SourceSpec, path: string, options: CopyOptions = {}) {
|
|
104
|
+
copy(this, location, path, options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
copyText(text: string, path: string) {
|
|
108
|
+
copy(this, new TextSource(text), path);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
exec(cmd: string, options?: ExecOptions) {
|
|
112
|
+
return exec(cmd, options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
content(location: string, encoding?: BufferEncoding) {
|
|
116
|
+
const source = AbstractContentSource.resolve(location);
|
|
117
|
+
if (Array.isArray(source)) {
|
|
118
|
+
return source.map(s => new ContentObject(this, s, encoding));
|
|
119
|
+
} else {
|
|
120
|
+
return new ContentObject(this, source, encoding);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
json(location: string) {
|
|
125
|
+
const source = AbstractContentSource.resolve(location);
|
|
126
|
+
if (Array.isArray(source)) {
|
|
127
|
+
return source.map(s => new JsonObject(this, s));
|
|
128
|
+
} else {
|
|
129
|
+
return new JsonObject(this, source);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pdf(location: string) {
|
|
134
|
+
const source = AbstractContentSource.resolve(location);
|
|
135
|
+
if (Array.isArray(source)) {
|
|
136
|
+
return source.map(s => new PdfObject(this, s));
|
|
137
|
+
} else {
|
|
138
|
+
return new PdfObject(this, source);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
docx(location: string) {
|
|
143
|
+
const source = AbstractContentSource.resolve(location);
|
|
144
|
+
if (Array.isArray(source)) {
|
|
145
|
+
return source.map(s => new DocxObject(this, s));
|
|
146
|
+
} else {
|
|
147
|
+
return new DocxObject(this, source);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
media(location: string, options?: MediaOptions) {
|
|
152
|
+
const source = AbstractContentSource.resolve(location);
|
|
153
|
+
if (Array.isArray(source)) {
|
|
154
|
+
return source.map(s => new MediaObject(this, s, options));
|
|
155
|
+
} else {
|
|
156
|
+
return new MediaObject(this, source, options);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async build(object: Record<string, any>) {
|
|
161
|
+
try {
|
|
162
|
+
let { fileName, publishName } = this._getOutputNames();
|
|
163
|
+
// resolve all content objects values from the context object
|
|
164
|
+
object = await resolveContextObject(object);
|
|
165
|
+
// write the memory to a file
|
|
166
|
+
fileName = await this.memory.build(fileName, object);
|
|
167
|
+
let target: string = fileName;
|
|
168
|
+
if (publishName) {
|
|
169
|
+
try {
|
|
170
|
+
target = await this.options.publish!(fileName, publishName);
|
|
171
|
+
} finally {
|
|
172
|
+
rmSync(fileName);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.options.quiet || console.log(`Memory saved to ${target}`);
|
|
176
|
+
return target;
|
|
177
|
+
} finally {
|
|
178
|
+
if (this._tmpdir) {
|
|
179
|
+
rmSync(this._tmpdir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _getOutputNames(): { fileName: string, publishName: string | undefined } {
|
|
185
|
+
const options = this.options;
|
|
186
|
+
if (!options.out) {
|
|
187
|
+
options.out = "memory.tar";
|
|
188
|
+
}
|
|
189
|
+
let fileName: string;
|
|
190
|
+
let publishName: string | undefined;
|
|
191
|
+
const out = options.out;
|
|
192
|
+
if (out.startsWith("memory:")) {
|
|
193
|
+
if (!options.publish) {
|
|
194
|
+
throw new Error(`The publish option is required for "${out}" output`);
|
|
195
|
+
}
|
|
196
|
+
// force gzip when publishing
|
|
197
|
+
options.gzip = true;
|
|
198
|
+
// create a temporary path for the output
|
|
199
|
+
fileName = createTmpBaseName(this.tmpdir());
|
|
200
|
+
publishName = out.substring("memory:".length);
|
|
201
|
+
} else {
|
|
202
|
+
fileName = resolve(out);
|
|
203
|
+
}
|
|
204
|
+
return { fileName, publishName };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveContextObject(object: Record<string, any>): Promise<Record<string, any>> {
|
|
210
|
+
return new AsyncObjectWalker().map(object, async (_key, value) => {
|
|
211
|
+
if (value instanceof ContentObject) {
|
|
212
|
+
return await value.getText();
|
|
213
|
+
} else {
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function createTmpBaseName(tmpdir: string) {
|
|
220
|
+
return join(tmpdir, `.composable-memory-${Date.now()}.tar`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
export function buildMemoryPack(recipeFn: (commands: Commands) => Promise<Record<string, any>>, options: BuildOptions): Promise<string> {
|
|
225
|
+
const builder = new Builder(options);
|
|
226
|
+
return recipeFn({
|
|
227
|
+
vars: builder.vars.bind(builder),
|
|
228
|
+
tmpdir: builder.tmpdir.bind(builder),
|
|
229
|
+
exec: builder.exec.bind(builder),
|
|
230
|
+
from: builder.from.bind(builder),
|
|
231
|
+
content: builder.content.bind(builder),
|
|
232
|
+
json: builder.json.bind(builder),
|
|
233
|
+
pdf: builder.pdf.bind(builder),
|
|
234
|
+
docx: builder.docx.bind(builder),
|
|
235
|
+
media: builder.media.bind(builder),
|
|
236
|
+
copy: builder.copy.bind(builder),
|
|
237
|
+
copyText: builder.copyText.bind(builder),
|
|
238
|
+
}).then(metadata => builder.build(metadata));
|
|
239
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { manyToMarkdown, pdfToText, pdfToTextBuffer, transformImage, transformImageToBuffer } from "@vertesia/converters";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { PassThrough, Readable } from "stream";
|
|
4
|
+
import { Builder } from "./Builder.js";
|
|
5
|
+
import { ContentSource } from "./ContentSource.js";
|
|
6
|
+
|
|
7
|
+
export class ContentObject implements ContentSource {
|
|
8
|
+
constructor(public builder: Builder, public source: ContentSource, public encoding: BufferEncoding = "utf-8") { }
|
|
9
|
+
|
|
10
|
+
getContent(): Promise<Buffer> {
|
|
11
|
+
return this.source.getContent();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getStream(): Promise<NodeJS.ReadableStream> {
|
|
15
|
+
return this.source.getStream();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
copyTo(entry: string) {
|
|
19
|
+
this.builder.addEntry(entry, this);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async copyToFile(file: string): Promise<fs.WriteStream> {
|
|
23
|
+
|
|
24
|
+
const input = await this.getStream();
|
|
25
|
+
const out = fs.createWriteStream(file);
|
|
26
|
+
const stream = input.pipe(out);
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const handleError = (err: any) => {
|
|
29
|
+
reject(err);
|
|
30
|
+
out.close();
|
|
31
|
+
}
|
|
32
|
+
stream.on('finish', () => {
|
|
33
|
+
resolve(stream);
|
|
34
|
+
out.close();
|
|
35
|
+
});
|
|
36
|
+
input.on('error', handleError);
|
|
37
|
+
stream.on('error', handleError);
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get a text representation of the object
|
|
43
|
+
*/
|
|
44
|
+
async getText(encoding?: BufferEncoding): Promise<string> {
|
|
45
|
+
const t = (await this.getContent()).toString(encoding || this.encoding);
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getJsonValue(): Promise<any> {
|
|
50
|
+
return this.getText();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class JsonObject extends ContentObject {
|
|
55
|
+
async getJsonValue(): Promise<any> {
|
|
56
|
+
return JSON.parse(await this.getText());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MediaOptions {
|
|
61
|
+
max_hw?: number;
|
|
62
|
+
format?: "jpeg" | "png";
|
|
63
|
+
}
|
|
64
|
+
export class MediaObject extends ContentObject {
|
|
65
|
+
constructor(builder: Builder, source: ContentSource, public options: MediaOptions = {}) {
|
|
66
|
+
super(builder, source);
|
|
67
|
+
this.encoding = "base64";
|
|
68
|
+
}
|
|
69
|
+
async getStream(): Promise<NodeJS.ReadableStream> {
|
|
70
|
+
const stream = await super.getStream();
|
|
71
|
+
const out = new PassThrough();
|
|
72
|
+
await transformImage(stream, out, this.options);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
async getContent(): Promise<Buffer> {
|
|
76
|
+
const stream = await super.getStream();
|
|
77
|
+
return await transformImageToBuffer(stream, this.options);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
export class PdfObject extends ContentObject {
|
|
83
|
+
constructor(builder: Builder, source: ContentSource) {
|
|
84
|
+
super(builder, source);
|
|
85
|
+
}
|
|
86
|
+
async getStream(): Promise<NodeJS.ReadableStream> {
|
|
87
|
+
return Readable.from(await this.getContent());
|
|
88
|
+
}
|
|
89
|
+
async getContent(): Promise<Buffer> {
|
|
90
|
+
return pdfToTextBuffer(await super.getContent());
|
|
91
|
+
}
|
|
92
|
+
async getText(): Promise<string> {
|
|
93
|
+
return await pdfToText(await super.getContent());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
export class DocxObject extends ContentObject {
|
|
99
|
+
constructor(builder: Builder, source: ContentSource) {
|
|
100
|
+
super(builder, source);
|
|
101
|
+
}
|
|
102
|
+
async getBuffer(): Promise<Buffer> {
|
|
103
|
+
return Buffer.from(await manyToMarkdown(await this.getStream(), "docx"), "utf-8");
|
|
104
|
+
}
|
|
105
|
+
async getStream(): Promise<NodeJS.ReadableStream> {
|
|
106
|
+
const stream = new PassThrough();
|
|
107
|
+
stream.end(await this.getBuffer());
|
|
108
|
+
return stream;
|
|
109
|
+
}
|
|
110
|
+
async getText(): Promise<string> {
|
|
111
|
+
return await manyToMarkdown(await this.getStream(), "docx");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createReadStream } from "fs";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { globSync } from 'glob';
|
|
4
|
+
import { basename, extname, resolve } from "path";
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
|
|
7
|
+
export interface ContentSource {
|
|
8
|
+
getContent(): Promise<Buffer>;
|
|
9
|
+
getStream(): Promise<NodeJS.ReadableStream>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SourceSpec = string | ContentSource;
|
|
13
|
+
export abstract class AbstractContentSource implements ContentSource {
|
|
14
|
+
abstract getContent(): Promise<Buffer>
|
|
15
|
+
|
|
16
|
+
async getStream(): Promise<NodeJS.ReadableStream> {
|
|
17
|
+
return Readable.from(await this.getContent());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static resolve(source: SourceSpec): ContentSource | ContentSource[] {
|
|
21
|
+
if (typeof source === 'string') {
|
|
22
|
+
return FileSource.resolve(source);
|
|
23
|
+
} else if (source instanceof AbstractContentSource) {
|
|
24
|
+
return source;
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Unsupported content source: " + source);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class BufferSource extends AbstractContentSource {
|
|
31
|
+
constructor(public buffer: Buffer) {
|
|
32
|
+
super()
|
|
33
|
+
}
|
|
34
|
+
getContent(): Promise<Buffer> {
|
|
35
|
+
return Promise.resolve(this.buffer);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class TextSource extends AbstractContentSource {
|
|
40
|
+
constructor(public value: string, public encoding: BufferEncoding = "utf-8") {
|
|
41
|
+
super();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getContent() {
|
|
45
|
+
return Promise.resolve(Buffer.from(this.value, this.encoding));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class FileSource extends AbstractContentSource {
|
|
50
|
+
file: string;
|
|
51
|
+
constructor(file: string, resolvePath = true) {
|
|
52
|
+
super();
|
|
53
|
+
this.file = resolvePath ? resolve(file) : file;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get path() {
|
|
57
|
+
return this.file;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get name() {
|
|
61
|
+
return basename(this.file);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get extname() {
|
|
65
|
+
return extname(this.file);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get nameWithoutExt() {
|
|
69
|
+
return this.name.slice(0, -this.extname.length);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getContent() {
|
|
73
|
+
return readFile(this.file);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getStream(): Promise<NodeJS.ReadableStream> {
|
|
77
|
+
return createReadStream(this.file);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static resolve(location: string): FileSource | FileSource[] {
|
|
81
|
+
if (location.includes('*')) {
|
|
82
|
+
return globSync(location, { absolute: true, withFileTypes: false }).map(f => new FileSource(f));
|
|
83
|
+
} else {
|
|
84
|
+
return new FileSource(location);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import micromatch from 'micromatch';
|
|
2
|
+
import { extname } from "path";
|
|
3
|
+
import { AbstractContentSource } from "./ContentSource.js";
|
|
4
|
+
import { loadTarIndex, TarEntryIndex, TarIndex } from "./utils/tar.js";
|
|
5
|
+
|
|
6
|
+
export const MEMORY_METADATA_ENTRY = "metadata.json";
|
|
7
|
+
|
|
8
|
+
const EXPORT_CONTENT_KEY = '@content:';
|
|
9
|
+
const EXPORT_ENTRY_KEY = '@file:';
|
|
10
|
+
const EXPORT_PROPERTY_KEY = '@';
|
|
11
|
+
const mediaExtensions = new Set([".jpg", ".jpeg", ".png"])
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Projection cannot contains both include and exclude keys
|
|
15
|
+
*/
|
|
16
|
+
export interface ProjectionProperties {
|
|
17
|
+
[key: string]: boolean | 0 | 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function applyProjection(projection: ProjectionProperties, object: any) {
|
|
21
|
+
const keys = Object.keys(projection);
|
|
22
|
+
if (keys.length < 1) {
|
|
23
|
+
return object;
|
|
24
|
+
}
|
|
25
|
+
const isInclusion = !!projection[keys[0]];
|
|
26
|
+
const out: any = {};
|
|
27
|
+
if (isInclusion) {
|
|
28
|
+
for (const key of Object.keys(object)) {
|
|
29
|
+
if (projection[key]) {
|
|
30
|
+
out[key] = object[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
for (const key of Object.keys(object)) {
|
|
35
|
+
const value = projection[key];
|
|
36
|
+
if (value === undefined || value) {
|
|
37
|
+
out[key] = object[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
export abstract class MemoryPack {
|
|
46
|
+
abstract readonly file: string;
|
|
47
|
+
abstract getMetadata(projection?: ProjectionProperties): Promise<Record<string, any>>;
|
|
48
|
+
abstract getEntry(path: string): MemoryEntry | null;
|
|
49
|
+
abstract getEntryContent(path: string): Promise<Buffer | null>;
|
|
50
|
+
abstract getEntryText(path: string, encoding?: BufferEncoding): Promise<string | null>;
|
|
51
|
+
abstract getPaths(filters?: string[]): string[];
|
|
52
|
+
abstract getEntries(filters?: string[]): MemoryEntry[];
|
|
53
|
+
abstract getEntriesContent(filters?: string[]): Promise<Buffer[]>;
|
|
54
|
+
abstract getEntriesText(filters?: string[], encoding?: BufferEncoding): Promise<string[]>;
|
|
55
|
+
|
|
56
|
+
async exportObject(mapping: Record<string, any>): Promise<Record<string, any>> {
|
|
57
|
+
let metadata: any;
|
|
58
|
+
const result: Record<string, any> = {};
|
|
59
|
+
for (const key of Object.keys(mapping)) {
|
|
60
|
+
const value = mapping[key];
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
if (value === '@') {
|
|
63
|
+
if (!metadata) {
|
|
64
|
+
metadata = await this.getMetadata();
|
|
65
|
+
}
|
|
66
|
+
if (key === '@') {
|
|
67
|
+
Object.assign(result, metadata);
|
|
68
|
+
} else {
|
|
69
|
+
result[key] = metadata;
|
|
70
|
+
}
|
|
71
|
+
} else if (value.startsWith(EXPORT_CONTENT_KEY)) {
|
|
72
|
+
const selector = value.slice(EXPORT_CONTENT_KEY.length);
|
|
73
|
+
const isMulti = selector.includes('*') || selector.includes(',');
|
|
74
|
+
if (isMulti) {
|
|
75
|
+
result[key] = await this.getEntriesText(selector.split(','));
|
|
76
|
+
} else {
|
|
77
|
+
result[key] = await this.getEntryText(selector);
|
|
78
|
+
}
|
|
79
|
+
} else if (value.startsWith(EXPORT_ENTRY_KEY)) {
|
|
80
|
+
const selector = value.slice(EXPORT_ENTRY_KEY.length);
|
|
81
|
+
const isMulti = selector.includes('*') || selector.includes(',');
|
|
82
|
+
if (isMulti) {
|
|
83
|
+
const entries = this.getEntries(selector.split(','));
|
|
84
|
+
result[key] = await Promise.all(entries.map(entry => entry.getText().then(content => ({ name: entry.name, content }))));
|
|
85
|
+
} else {
|
|
86
|
+
const entry = this.getEntry(selector);
|
|
87
|
+
result[key] = entry ? { name: entry.name, content: await entry.getText() } : null;
|
|
88
|
+
}
|
|
89
|
+
} else if (value.startsWith(EXPORT_PROPERTY_KEY)) {
|
|
90
|
+
if (!metadata) {
|
|
91
|
+
metadata = await this.getMetadata();
|
|
92
|
+
}
|
|
93
|
+
const accessor = value.substring(EXPORT_PROPERTY_KEY.length);
|
|
94
|
+
result[key] = resolveProperty(metadata, accessor);
|
|
95
|
+
} else {
|
|
96
|
+
result[key] = value;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
result[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
export class MemoryEntry extends AbstractContentSource {
|
|
108
|
+
constructor(public index: TarIndex, public name: string, public offset: number, public size: number) {
|
|
109
|
+
super();
|
|
110
|
+
}
|
|
111
|
+
getContent(): Promise<Buffer> {
|
|
112
|
+
return this.index.getContentAt(this.offset, this.size);
|
|
113
|
+
}
|
|
114
|
+
getText(encoding: BufferEncoding = "utf-8"): Promise<string> {
|
|
115
|
+
return this.getContent().then((b) => b.toString(encoding));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class TarMemoryPack extends MemoryPack {
|
|
120
|
+
constructor(public file: string, public index: TarIndex) {
|
|
121
|
+
super();
|
|
122
|
+
if (!index.get(MEMORY_METADATA_ENTRY)) {
|
|
123
|
+
throw new Error("Invalid memory tar file. Context entry not found");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async getMetadata(projection?: ProjectionProperties): Promise<Record<string, any>> {
|
|
127
|
+
const content = await this.index.getContent(MEMORY_METADATA_ENTRY);
|
|
128
|
+
if (content) {
|
|
129
|
+
let metadata = JSON.parse(content.toString('utf-8'));
|
|
130
|
+
if (projection) {
|
|
131
|
+
metadata = applyProjection(projection, metadata);
|
|
132
|
+
}
|
|
133
|
+
return metadata;
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error("Invalid memory tar file. Context entry not found");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getPaths(filters?: string[]) {
|
|
140
|
+
let paths = this.index.getSortedPaths();
|
|
141
|
+
if (filters && filters.length > 0) {
|
|
142
|
+
paths = micromatch(paths, filters);
|
|
143
|
+
}
|
|
144
|
+
return paths;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getEntries(filters?: string[]) {
|
|
148
|
+
return this._getEntries(this.getPaths(filters));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private _getEntries(paths: string[]) {
|
|
152
|
+
const entries: MemoryEntry[] = [];
|
|
153
|
+
const index = this.index;
|
|
154
|
+
for (const path of paths) {
|
|
155
|
+
const entry: TarEntryIndex = index.get(path);
|
|
156
|
+
entries.push(new MemoryEntry(index, path, entry.offset, entry.size));
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getEntriesContent(filters?: string[]) {
|
|
162
|
+
const paths = this.getPaths(filters);
|
|
163
|
+
const promises: Promise<Buffer>[] = [];
|
|
164
|
+
for (const path of paths) {
|
|
165
|
+
promises.push(this.getEntryContent(path) as Promise<Buffer>);
|
|
166
|
+
}
|
|
167
|
+
return Promise.all(promises);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getEntriesText(filters?: string[], encoding?: BufferEncoding) {
|
|
171
|
+
const paths = this.getPaths(filters);
|
|
172
|
+
const promises: Promise<string>[] = [];
|
|
173
|
+
for (const path of paths) {
|
|
174
|
+
promises.push(this.getEntryText(path, encoding) as Promise<string>);
|
|
175
|
+
}
|
|
176
|
+
return Promise.all(promises);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getEntry(path: string) {
|
|
180
|
+
const entry = this.index.get(path);
|
|
181
|
+
return entry ? new MemoryEntry(this.index, path, entry.offset, entry.size) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getEntryContent(path: string): Promise<Buffer | null> {
|
|
185
|
+
const entry = this.index.get(path);
|
|
186
|
+
if (!entry) {
|
|
187
|
+
return Promise.resolve(null);
|
|
188
|
+
} else {
|
|
189
|
+
return this.index.getContentAt(entry.offset, entry.size);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
getEntryText(path: string, encoding?: BufferEncoding): Promise<string | null> {
|
|
194
|
+
return this.getEntryContent(path).then((b) => b?.toString(encoding || getTextEncodingForPath(path)) || null);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static async loadFile(file: string) {
|
|
198
|
+
const index = await loadTarIndex(file);
|
|
199
|
+
if (!index) {
|
|
200
|
+
throw new Error("Invalid memory tar file. Cannot load index");
|
|
201
|
+
}
|
|
202
|
+
return new TarMemoryPack(file, index);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
export function loadMemoryPack(location: string): Promise<MemoryPack> {
|
|
209
|
+
// TODO we only support file paths as location for now
|
|
210
|
+
return TarMemoryPack.loadFile(location);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getTextEncodingForPath(path: string) {
|
|
214
|
+
const ext = extname(path);
|
|
215
|
+
return mediaExtensions.has(ext) ? "base64" : "utf-8";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
function resolveProperty(obj: Record<string, any>, key: string) {
|
|
220
|
+
if (key.includes('.')) {
|
|
221
|
+
const keys = key.split('.');
|
|
222
|
+
let value = obj;
|
|
223
|
+
for (const k of keys) {
|
|
224
|
+
value = value[k];
|
|
225
|
+
if (value == undefined) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
} else {
|
|
231
|
+
return obj[key];
|
|
232
|
+
}
|
|
233
|
+
}
|