Skip to content

Commit ecd7379

Browse files
committed
perf: replace archiver and tar-fs with modern-tar
1 parent 96013d8 commit ecd7379

File tree

8 files changed

+198
-115
lines changed

8 files changed

+198
-115
lines changed

package-lock.json

Lines changed: 109 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/modules/selenium/src/selenium-container.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { copyFile } from "fs/promises";
22
import path from "path";
3-
import tar from "tar-fs";
3+
import { pipeline } from "stream/promises";
44
import {
55
AbstractStartedContainer,
66
AbstractStoppedContainer,
@@ -128,19 +128,12 @@ export class StoppedSeleniumRecordingContainer extends StoppedSeleniumContainer
128128

129129
log.debug("Unpacking archive...", { containerId: ffmpegContainerId });
130130
const destinationDir = tmp.dirSync({ keep: false });
131-
await this.extractTarStreamToDest(archiveStream, destinationDir.name);
131+
const { unpackTar } = await import("modern-tar/fs");
132+
await pipeline(archiveStream, unpackTar(destinationDir.name));
132133
log.debug("Unpacked archive", { containerId: ffmpegContainerId });
133134

134135
const videoFile = path.resolve(destinationDir.name, "video.mp4");
135136
await copyFile(videoFile, target);
136137
log.debug(`Extracted video to "${target}"`, { containerId: ffmpegContainerId });
137138
}
138-
139-
private async extractTarStreamToDest(tarStream: NodeJS.ReadableStream, dest: string): Promise<void> {
140-
await new Promise<void>((resolve) => {
141-
const destination = tar.extract(dest);
142-
tarStream.pipe(destination);
143-
destination.on("finish", resolve);
144-
});
145-
}
146139
}

packages/testcontainers/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,25 @@
3232
"dependencies": {
3333
"@balena/dockerignore": "^1.0.2",
3434
"@types/dockerode": "^3.3.44",
35-
"archiver": "^7.0.1",
3635
"async-lock": "^1.4.1",
3736
"byline": "^5.0.0",
3837
"debug": "^4.4.3",
3938
"docker-compose": "^1.3.0",
4039
"dockerode": "^4.0.8",
4140
"get-port": "^7.1.0",
41+
"modern-tar": "^0.5.3",
4242
"proper-lockfile": "^4.1.2",
4343
"properties-reader": "^2.3.0",
4444
"ssh-remote-port-forward": "^1.0.4",
45-
"tar-fs": "^3.1.1",
4645
"tmp": "^0.2.5",
4746
"undici": "^7.16.0"
4847
},
4948
"devDependencies": {
50-
"@types/archiver": "^6.0.3",
5149
"@types/async-lock": "^1.4.2",
5250
"@types/byline": "^4.2.36",
5351
"@types/debug": "^4.1.12",
5452
"@types/proper-lockfile": "^4.1.4",
5553
"@types/properties-reader": "^2.1.3",
56-
"@types/tar-fs": "^2.0.4",
5754
"@types/tmp": "^0.2.6"
5855
}
5956
}

packages/testcontainers/src/container-runtime/clients/image/docker-image-client.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import byline from "byline";
44
import Dockerode, { ImageBuildOptions, ImageInspectInfo } from "dockerode";
55
import { existsSync, promises as fs } from "fs";
66
import path from "path";
7-
import tar from "tar-fs";
87
import { buildLog, log, pullLog } from "../../../common";
98
import { getAuthConfig } from "../../auth/get-auth-config";
109
import { ImageName } from "../../image-name";
@@ -23,14 +22,14 @@ export class DockerImageClient implements ImageClient {
2322
try {
2423
log.debug(`Building image "${opts.t}" with context "${context}"...`);
2524
const isDockerIgnored = await this.createIsDockerIgnoredFunction(context);
26-
const tarStream = tar.pack(context, {
27-
ignore: (aPath) => {
28-
const relativePath = path.relative(context, aPath);
29-
if (relativePath === opts.dockerfile) {
30-
return false;
31-
} else {
32-
return isDockerIgnored(relativePath);
25+
const { packTar } = await import("modern-tar/fs");
26+
const tarStream = packTar(context, {
27+
filter: (path) => {
28+
if (path === opts.dockerfile) {
29+
return true;
3330
}
31+
32+
return !isDockerIgnored(path);
3433
},
3534
});
3635
await new Promise<void>((resolve) => {

packages/testcontainers/src/generic-container/generic-container.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,11 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
411411
.withExposedPorts(8080)
412412
.start();
413413

414-
expect((await container.exec(`stat -c "%a %n" /tmp/newdir/test.txt`)).output).toContain("777");
414+
// Check directory permissions
415+
expect((await container.exec(`stat -c "%a %n" /tmp/newdir`)).output).toContain("777");
416+
417+
// Verify files retain their original permissions
418+
expect((await container.exec(`stat -c "%a %n" /tmp/newdir/test.txt`)).output).toContain("644");
415419
});
416420

417421
it("should copy directory to started container", async () => {

packages/testcontainers/src/generic-container/generic-container.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import archiver from "archiver";
21
import AsyncLock from "async-lock";
32
import { Container, ContainerCreateOptions, HostConfig } from "dockerode";
43
import { Readable } from "stream";
4+
import { buffer } from "stream/consumers";
55
import { containerLog, hash, log, toNanos } from "../common";
66
import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime";
77
import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types";
@@ -178,9 +178,30 @@ export class GenericContainer implements TestContainer {
178178
await client.container.connectToNetwork(container, network, this.networkAliases);
179179
}
180180

181-
if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) {
182-
const archive = this.createArchiveToCopyToContainer();
183-
archive.finalize();
181+
const sources = [
182+
...this.filesToCopy.map((file) => ({
183+
type: "file" as const,
184+
source: file.source,
185+
target: file.target,
186+
mode: file.mode,
187+
})),
188+
...this.directoriesToCopy.map((dir) => ({
189+
type: "directory" as const,
190+
source: dir.source,
191+
target: dir.target,
192+
mode: dir.mode,
193+
})),
194+
...(await Promise.all(
195+
this.contentsToCopy.map(async ({ content, target, mode }) => {
196+
const data = content instanceof Readable ? await buffer(content) : content;
197+
return { type: "content" as const, content: data, target, mode };
198+
})
199+
)),
200+
];
201+
202+
if (sources.length > 0) {
203+
const { packTar } = await import("modern-tar/fs");
204+
const archive = packTar(sources);
184205
await client.container.putArchive(container, archive, "/");
185206
}
186207

@@ -255,22 +276,6 @@ export class GenericContainer implements TestContainer {
255276
}
256277
}
257278

258-
private createArchiveToCopyToContainer(): archiver.Archiver {
259-
const tar = archiver("tar");
260-
261-
for (const { source, target, mode } of this.filesToCopy) {
262-
tar.file(source, { name: target, mode });
263-
}
264-
for (const { source, target, mode } of this.directoriesToCopy) {
265-
tar.directory(source, target, { mode });
266-
}
267-
for (const { content, target, mode } of this.contentsToCopy) {
268-
tar.append(content, { name: target, mode });
269-
}
270-
271-
return tar;
272-
}
273-
274279
protected containerStarted?(
275280
container: StartedTestContainer,
276281
inspectResult: InspectResult,

packages/testcontainers/src/generic-container/started-generic-container.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import archiver from "archiver";
21
import AsyncLock from "async-lock";
32
import Dockerode, { ContainerInspectInfo } from "dockerode";
43
import { Readable } from "stream";
4+
import { buffer } from "stream/consumers";
55
import { containerLog, log } from "../common";
66
import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime";
77
import { getReaper } from "../reaper/reaper";
@@ -181,31 +181,64 @@ export class StartedGenericContainer implements StartedTestContainer {
181181

182182
public async copyFilesToContainer(filesToCopy: FileToCopy[]): Promise<void> {
183183
log.debug(`Copying files to container...`, { containerId: this.container.id });
184+
if (filesToCopy.length === 0) {
185+
return;
186+
}
187+
184188
const client = await getContainerRuntimeClient();
185-
const tar = archiver("tar");
186-
filesToCopy.forEach(({ source, target }) => tar.file(source, { name: target }));
187-
tar.finalize();
188-
await client.container.putArchive(this.container, tar, "/");
189+
const { packTar } = await import("modern-tar/fs");
190+
191+
const sources = filesToCopy.map((file) => ({
192+
type: "file" as const,
193+
source: file.source,
194+
target: file.target,
195+
mode: file.mode,
196+
}));
197+
const archive = packTar(sources);
198+
199+
await client.container.putArchive(this.container, archive, "/");
189200
log.debug(`Copied files to container`, { containerId: this.container.id });
190201
}
191202

192203
public async copyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): Promise<void> {
193204
log.debug(`Copying directories to container...`, { containerId: this.container.id });
205+
if (directoriesToCopy.length === 0) {
206+
return;
207+
}
208+
194209
const client = await getContainerRuntimeClient();
195-
const tar = archiver("tar");
196-
directoriesToCopy.forEach(({ source, target }) => tar.directory(source, target));
197-
tar.finalize();
198-
await client.container.putArchive(this.container, tar, "/");
210+
const { packTar } = await import("modern-tar/fs");
211+
212+
const sources = directoriesToCopy.map((dir) => ({
213+
type: "directory" as const,
214+
source: dir.source,
215+
target: dir.target,
216+
mode: dir.mode,
217+
}));
218+
const archive = packTar(sources);
219+
220+
await client.container.putArchive(this.container, archive, "/");
199221
log.debug(`Copied directories to container`, { containerId: this.container.id });
200222
}
201223

202224
public async copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise<void> {
203225
log.debug(`Copying content to container...`, { containerId: this.container.id });
226+
if (contentsToCopy.length === 0) {
227+
return;
228+
}
229+
230+
const sources = await Promise.all(
231+
contentsToCopy.map(async ({ content, target, mode }) => {
232+
const processedContent = content instanceof Readable ? await buffer(content) : content;
233+
return { type: "content" as const, content: processedContent, target, mode };
234+
})
235+
);
236+
204237
const client = await getContainerRuntimeClient();
205-
const tar = archiver("tar");
206-
contentsToCopy.forEach(({ content, target, mode }) => tar.append(content, { name: target, mode: mode }));
207-
tar.finalize();
208-
await client.container.putArchive(this.container, tar, "/");
238+
const { packTar } = await import("modern-tar/fs");
239+
const archive = packTar(sources);
240+
241+
await client.container.putArchive(this.container, archive, "/");
209242
log.debug(`Copied content to container`, { containerId: this.container.id });
210243
}
211244

tsconfig.base.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"types": [
77
"vitest/globals"
88
],
9-
"module": "commonjs",
9+
"module": "Node16",
1010
"declaration": true,
1111
"sourceMap": true,
1212
"strict": true,
13-
"moduleResolution": "node",
13+
"moduleResolution": "node16",
1414
"esModuleInterop": true,
1515
"skipLibCheck": true,
1616
"forceConsistentCasingInFileNames": true,

0 commit comments

Comments
 (0)