@vertesia/memory 0.43.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/LICENSE +201 -0
- package/README.md +421 -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 +40 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Builder } from "./Builder.js";
|
|
2
|
+
import { ContentSource } from "./ContentSource.js";
|
|
3
|
+
import { MEMORY_METADATA_ENTRY, MemoryPack, ProjectionProperties } from "./MemoryPack.js";
|
|
4
|
+
import { normalizePath, TarBuilder } from "./utils/tar.js";
|
|
5
|
+
|
|
6
|
+
export interface FromOptions {
|
|
7
|
+
files?: string[];
|
|
8
|
+
projection?: ProjectionProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class MemoryPackBuilder {
|
|
12
|
+
baseMetadata: Record<string, any> = {};
|
|
13
|
+
entries: { [path: string]: ContentSource } = {};
|
|
14
|
+
|
|
15
|
+
constructor(public builder: Builder) {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async load(memory: MemoryPack, options: FromOptions = {}) {
|
|
19
|
+
const files = options.files || [];
|
|
20
|
+
// do not fetch the context entry an the metadata.json file
|
|
21
|
+
files.push(`!${MEMORY_METADATA_ENTRY}`);
|
|
22
|
+
const entries = memory.getEntries(files);
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
this.add(entry.name, entry);
|
|
25
|
+
}
|
|
26
|
+
this.baseMetadata = await memory.getMetadata(options.projection);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
add(path: string, content: ContentSource) {
|
|
30
|
+
path = normalizePath(path);
|
|
31
|
+
this.entries[path] = content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async build(file: string, metadata: object) {
|
|
35
|
+
metadata = { ...this.baseMetadata, ...metadata };
|
|
36
|
+
if (!file.endsWith('.tar')) {
|
|
37
|
+
file += '.tar';
|
|
38
|
+
}
|
|
39
|
+
if (this.builder.options.gzip) {
|
|
40
|
+
file += ".gz";
|
|
41
|
+
}
|
|
42
|
+
const tar = new TarBuilder(file);
|
|
43
|
+
const keys = Object.keys(this.entries).sort();
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const source = this.entries[key];
|
|
46
|
+
tar.add(key, await source.getContent());
|
|
47
|
+
}
|
|
48
|
+
tar.add(MEMORY_METADATA_ENTRY, Buffer.from(
|
|
49
|
+
JSON.stringify(metadata, undefined, this.builder.options.indent || undefined),
|
|
50
|
+
"utf-8")
|
|
51
|
+
);
|
|
52
|
+
await tar.build();
|
|
53
|
+
return file;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, statSync } from "fs";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
3
|
+
import { Builder } from "./Builder";
|
|
4
|
+
import { loadTarIndex } from "./utils/tar";
|
|
5
|
+
import { loadMemoryPack, MEMORY_METADATA_ENTRY } from "./MemoryPack";
|
|
6
|
+
|
|
7
|
+
const memoryBaseFile = 'test-base-memory.tar';
|
|
8
|
+
const memoryFile = 'test-memory.tar';
|
|
9
|
+
const tmpdir = mkdtempSync("composable-memory-pack-test-");
|
|
10
|
+
|
|
11
|
+
afterAll(() => {
|
|
12
|
+
rmSync(memoryBaseFile);
|
|
13
|
+
rmSync(memoryFile);
|
|
14
|
+
rmSync(tmpdir, { recursive: true });
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe("Builder", () => {
|
|
18
|
+
test("create base memory pack", async () => {
|
|
19
|
+
const builder = new Builder({
|
|
20
|
+
out: memoryBaseFile,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await builder.exec(`printf 'file1 from base memory' > ${tmpdir}/file1.txt`);
|
|
24
|
+
const file2 = await builder.exec('printf "file2 from base memory"', { quiet: true });
|
|
25
|
+
builder.copy(`${tmpdir}/file1.txt`, "file1.txt");
|
|
26
|
+
builder.copyText(file2 || '', "file2.txt");
|
|
27
|
+
builder.copyText("file3 from base memory", "file3.txt");
|
|
28
|
+
|
|
29
|
+
await builder.build({
|
|
30
|
+
baseProp1: "baseProp1",
|
|
31
|
+
baseProp2: "baseProp2",
|
|
32
|
+
baseProp3: "baseProp3",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const stats = statSync(memoryBaseFile);
|
|
36
|
+
expect(stats.isFile()).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("create memory pack override", async () => {
|
|
40
|
+
const builder = new Builder({
|
|
41
|
+
out: memoryFile,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await builder.from(memoryBaseFile, {
|
|
45
|
+
files: ["!file1.txt"], // remove file1
|
|
46
|
+
projection: {
|
|
47
|
+
baseProp1: false, // remove baseProp1
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
// override file2
|
|
51
|
+
builder.copyText(`file2 from new memory`, "file2.txt");
|
|
52
|
+
builder.copyText(`file4 from new memory`, "file4.txt");
|
|
53
|
+
|
|
54
|
+
await builder.build({
|
|
55
|
+
baseProp2: "baseProp2", // override baseProp2
|
|
56
|
+
prop4: "prop4",
|
|
57
|
+
parent: { child: 123 }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const stats = statSync(memoryFile);
|
|
61
|
+
expect(stats.isFile()).toBeTruthy();
|
|
62
|
+
|
|
63
|
+
// check the content of the tar
|
|
64
|
+
|
|
65
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
66
|
+
const entries = memory.getEntries();
|
|
67
|
+
expect(entries.length).toBe(4);
|
|
68
|
+
expect(entries.map(e => e.name).sort()).toStrictEqual(["file2.txt", "file3.txt", "file4.txt", MEMORY_METADATA_ENTRY].sort());
|
|
69
|
+
|
|
70
|
+
const entry1 = memory.getEntry("file1.txt");
|
|
71
|
+
expect(entry1).toBeNull();
|
|
72
|
+
const entry2 = memory.getEntry("file2.txt");
|
|
73
|
+
expect(entry2).not.toBeNull();
|
|
74
|
+
const entry3 = memory.getEntry("file3.txt");
|
|
75
|
+
expect(entry3).not.toBeNull();
|
|
76
|
+
const entry4 = memory.getEntry("file4.txt");
|
|
77
|
+
expect(entry4).not.toBeNull();
|
|
78
|
+
|
|
79
|
+
const content2 = (await entry2?.getContent())?.toString("utf-8");
|
|
80
|
+
const content3 = (await entry3?.getContent())?.toString("utf-8");
|
|
81
|
+
const content4 = (await entry4?.getContent())?.toString("utf-8");
|
|
82
|
+
|
|
83
|
+
expect(content2).toBe("file2 from new memory");
|
|
84
|
+
expect(content3).toBe("file3 from base memory");
|
|
85
|
+
expect(content4).toBe("file4 from new memory");
|
|
86
|
+
|
|
87
|
+
const context = await memory.getMetadata();
|
|
88
|
+
expect(context).toStrictEqual({
|
|
89
|
+
baseProp2: "baseProp2",
|
|
90
|
+
baseProp3: "baseProp3",
|
|
91
|
+
prop4: "prop4",
|
|
92
|
+
parent: { child: 123 }
|
|
93
|
+
});
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("MemoryPack.exportObject", async () => {
|
|
97
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
98
|
+
let obj = await memory.exportObject({
|
|
99
|
+
"@": "@",
|
|
100
|
+
"file2": "@content:file2.txt",
|
|
101
|
+
});
|
|
102
|
+
expect(obj).toStrictEqual({
|
|
103
|
+
"file2": "file2 from new memory",
|
|
104
|
+
baseProp2: "baseProp2",
|
|
105
|
+
baseProp3: "baseProp3",
|
|
106
|
+
prop4: "prop4",
|
|
107
|
+
parent: { child: 123 }
|
|
108
|
+
})
|
|
109
|
+
obj = await memory.exportObject({
|
|
110
|
+
"prop4": "@prop4",
|
|
111
|
+
"file2": "@content:file2.txt",
|
|
112
|
+
"manifest": "@",
|
|
113
|
+
});
|
|
114
|
+
expect(obj).toStrictEqual({
|
|
115
|
+
prop4: "prop4",
|
|
116
|
+
"file2": "file2 from new memory",
|
|
117
|
+
manifest: {
|
|
118
|
+
baseProp2: "baseProp2",
|
|
119
|
+
baseProp3: "baseProp3",
|
|
120
|
+
prop4: "prop4",
|
|
121
|
+
parent: { child: 123 }
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
obj = await memory.exportObject({
|
|
125
|
+
"content": "@content:*.txt",
|
|
126
|
+
});
|
|
127
|
+
expect(obj).toStrictEqual({
|
|
128
|
+
content: [
|
|
129
|
+
"file2 from new memory",
|
|
130
|
+
"file3 from base memory",
|
|
131
|
+
"file4 from new memory"
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
obj = await memory.exportObject({
|
|
136
|
+
"content": "@content:file2.txt,file3.txt",
|
|
137
|
+
});
|
|
138
|
+
expect(obj).toStrictEqual({
|
|
139
|
+
content: [
|
|
140
|
+
"file2 from new memory",
|
|
141
|
+
"file3 from base memory",
|
|
142
|
+
]
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
obj = await memory.exportObject({
|
|
146
|
+
"content": "@content:file2.txt,*4.txt",
|
|
147
|
+
});
|
|
148
|
+
expect(obj).toStrictEqual({
|
|
149
|
+
content: [
|
|
150
|
+
"file2 from new memory",
|
|
151
|
+
"file4 from new memory"
|
|
152
|
+
]
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
obj = await memory.exportObject({
|
|
156
|
+
"files": "@file:*.txt",
|
|
157
|
+
});
|
|
158
|
+
expect(obj).toStrictEqual({
|
|
159
|
+
files: [
|
|
160
|
+
{ name: "file2.txt", content: "file2 from new memory" },
|
|
161
|
+
{ name: "file3.txt", content: "file3 from base memory" },
|
|
162
|
+
{ name: "file4.txt", content: "file4 from new memory" }
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
obj = await memory.exportObject({
|
|
167
|
+
"files": "@file:file2.txt,file3.txt",
|
|
168
|
+
});
|
|
169
|
+
expect(obj).toStrictEqual({
|
|
170
|
+
files: [
|
|
171
|
+
{ name: "file2.txt", content: "file2 from new memory" },
|
|
172
|
+
{ name: "file3.txt", content: "file3 from base memory" },
|
|
173
|
+
]
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
obj = await memory.exportObject({
|
|
177
|
+
"files": "@file:*2.txt,*4.txt",
|
|
178
|
+
});
|
|
179
|
+
expect(obj).toStrictEqual({
|
|
180
|
+
files: [
|
|
181
|
+
{ name: "file2.txt", content: "file2 from new memory" },
|
|
182
|
+
{ name: "file4.txt", content: "file4 from new memory" },
|
|
183
|
+
]
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("MemoryPack.exportObject with nested props", async () => {
|
|
189
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
190
|
+
let obj = await memory.exportObject({
|
|
191
|
+
"child": "@parent.child",
|
|
192
|
+
});
|
|
193
|
+
expect(obj).toStrictEqual({
|
|
194
|
+
child: 123
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("MemoryPack.exportObject with runtime values", async () => {
|
|
199
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
200
|
+
let obj = await memory.exportObject({
|
|
201
|
+
"father": "@parent",
|
|
202
|
+
"instruction": "Use this runtime instruction",
|
|
203
|
+
someNullValue: null,
|
|
204
|
+
someObject: { msg: "Hello" }
|
|
205
|
+
});
|
|
206
|
+
expect(obj).toStrictEqual({
|
|
207
|
+
father: { child: 123 },
|
|
208
|
+
"instruction": "Use this runtime instruction",
|
|
209
|
+
someNullValue: null,
|
|
210
|
+
someObject: { msg: "Hello" }
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
});
|