Skip to content

Commit dfe28aa

Browse files
Bryan Powellsfsholden
authored andcommitted
feat: add zip tree container and stream() method to tree container interface (#154)
1 parent 42f991d commit dfe28aa

File tree

5 files changed

+268
-28
lines changed

5 files changed

+268
-28
lines changed

scripts/repl.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#!/usr/bin/env node
22

33
const repl = require('repl');
4-
const { RegistryAccess, MetadataConverter } = require('../lib/src');
4+
const { Connection, AuthInfo } = require('@salesforce/core');
5+
const { RegistryAccess, MetadataConverter, SourceClient } = require('../lib/src');
56

67
const startMessage = `
78
Usage:
89
registryAccess: RegistryAccess instance
910
converter: MetadataConverter instance
1011
resolve(path): resolve components from a path
12+
async client(username): create a SourceClient
1113
`
1214
console.log(startMessage);
1315

@@ -16,3 +18,8 @@ replServer.setupHistory('.repl_history', (err, repl) => {});
1618
replServer.context.registryAccess = new RegistryAccess();
1719
replServer.context.converter = new MetadataConverter();
1820
replServer.context.resolve = (path) => replServer.context.registryAccess.getComponentsFromPath(path)
21+
replServer.context.client = async (username) => {
22+
return new SourceClient(await Connection.create({
23+
authInfo: await AuthInfo.create({ username })
24+
}));
25+
}

src/i18n/i18n.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export const messages = {
4949
error_updating_metadata_type: "Unexpected error updating '%s'",
5050
error_parsing_metadata_file: 'Error parsing metadata file',
5151
error_parsing_xml: 'SourceComponent %s does not have an associated metadata xml to parse',
52+
error_expected_file_path: '%s: path is to a directory, expected a file',
53+
error_expected_directory_path: '%s: path is to a file, expected a directory',
54+
error_no_directory_stream: '%s does not support readable streams on directories',
5255
error_static_resource_expected_archive_type:
5356
'A StaticResource directory must have a content type of application/zip or application/jar - found %s for %s',
5457
tapi_deploy_component_limit_error:

src/metadata-registry/treeContainers.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { VirtualDirectory, TreeContainer } from '../metadata-registry';
8-
import { join, dirname, basename } from 'path';
8+
import { join, dirname, basename, normalize } from 'path';
99
import { baseName } from '../utils';
1010
import { parseMetadataXml } from '../utils/registry';
11-
import { lstatSync, existsSync, readdirSync, promises as fsPromises } from 'fs';
11+
import { lstatSync, existsSync, readdirSync, promises as fsPromises, createReadStream } from 'fs';
1212
import { LibraryError } from '../errors';
1313
import { SourcePath } from '../common';
14+
import * as unzipper from 'unzipper';
15+
import { Readable } from 'stream';
1416

1517
/**
1618
* An extendable base class for implementing the `TreeContainer` interface
@@ -35,6 +37,7 @@ export abstract class BaseTreeContainer implements TreeContainer {
3537
public abstract isDirectory(fsPath: SourcePath): boolean;
3638
public abstract readDirectory(fsPath: SourcePath): string[];
3739
public abstract readFile(fsPath: SourcePath): Promise<Buffer>;
40+
public abstract stream(fsPath: SourcePath): Readable;
3841
}
3942

4043
export class NodeFSTreeContainer extends BaseTreeContainer {
@@ -53,6 +56,83 @@ export class NodeFSTreeContainer extends BaseTreeContainer {
5356
public readFile(fsPath: SourcePath): Promise<Buffer> {
5457
return fsPromises.readFile(fsPath);
5558
}
59+
60+
public stream(fsPath: SourcePath): Readable {
61+
return createReadStream(fsPath);
62+
}
63+
}
64+
65+
interface ZipEntry {
66+
path: string;
67+
stream?: () => unzipper.Entry;
68+
buffer?: () => Promise<Buffer>;
69+
}
70+
71+
export class ZipTreeContainer extends BaseTreeContainer {
72+
private tree = new Map<SourcePath, ZipEntry[] | ZipEntry>();
73+
74+
private constructor(directory: unzipper.CentralDirectory) {
75+
super();
76+
this.populate(directory);
77+
}
78+
79+
public static async create(buffer: Buffer): Promise<ZipTreeContainer> {
80+
const directory = await unzipper.Open.buffer(buffer);
81+
return new ZipTreeContainer(directory);
82+
}
83+
84+
public exists(fsPath: string): boolean {
85+
return this.tree.has(fsPath);
86+
}
87+
88+
public isDirectory(fsPath: string): boolean {
89+
if (this.exists(fsPath)) {
90+
return Array.isArray(this.tree.get(fsPath));
91+
}
92+
throw new LibraryError('error_path_not_found', fsPath);
93+
}
94+
95+
public readDirectory(fsPath: string): string[] {
96+
if (this.isDirectory(fsPath)) {
97+
return (this.tree.get(fsPath) as ZipEntry[]).map((entry) => basename(entry.path));
98+
}
99+
throw new LibraryError('error_expected_directory_path', fsPath);
100+
}
101+
102+
public readFile(fsPath: string): Promise<Buffer> {
103+
if (!this.isDirectory(fsPath)) {
104+
return (this.tree.get(fsPath) as ZipEntry).buffer();
105+
}
106+
throw new LibraryError('error_expected_file_path', fsPath);
107+
}
108+
109+
public stream(fsPath: string): Readable {
110+
if (!this.isDirectory(fsPath)) {
111+
return (this.tree.get(fsPath) as ZipEntry).stream();
112+
}
113+
throw new LibraryError('error_no_directory_stream', this.constructor.name);
114+
}
115+
116+
private populate(directory: unzipper.CentralDirectory): void {
117+
for (const { path, stream, buffer } of directory.files) {
118+
// normalize path to use OS separator since zip entries always use forward slash
119+
const entry = { path: normalize(path), stream, buffer };
120+
this.tree.set(entry.path, entry);
121+
this.ensureDirPathExists(entry);
122+
}
123+
}
124+
125+
private ensureDirPathExists(entry: ZipEntry): void {
126+
const dirPath = dirname(entry.path);
127+
if (dirPath === entry.path) {
128+
return;
129+
} else if (!this.exists(dirPath)) {
130+
this.tree.set(dirPath, [entry]);
131+
this.ensureDirPathExists({ path: dirPath });
132+
} else {
133+
(this.tree.get(dirPath) as ZipEntry[]).push(entry);
134+
}
135+
}
56136
}
57137

58138
export class VirtualTreeContainer extends BaseTreeContainer {
@@ -93,6 +173,10 @@ export class VirtualTreeContainer extends BaseTreeContainer {
93173
throw new LibraryError('error_path_not_found', fsPath);
94174
}
95175

176+
public stream(fsPath: string): Readable {
177+
throw new Error('Method not implemented.');
178+
}
179+
96180
private populate(virtualFs: VirtualDirectory[]): void {
97181
for (const dir of virtualFs) {
98182
const { dirPath, children } = dir;

src/metadata-registry/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { MetadataType, SourcePath } from '../common/types';
99
import { SourceComponent } from '.';
10+
import { Readable } from 'stream';
1011

1112
/**
1213
* Metadata type definitions
@@ -105,4 +106,5 @@ export interface TreeContainer {
105106
readDirectory(path: SourcePath): string[];
106107
find(fileType: 'content' | 'metadata', fullName: string, dir: SourcePath): SourcePath | undefined;
107108
readFile(fsPath: SourcePath): Promise<Buffer>;
109+
stream(fsPath: SourcePath): Readable;
108110
}

0 commit comments

Comments
 (0)