Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1a256cb
added tests
mishushakov Jan 6, 2026
2c756cf
added tests
mishushakov Jan 6, 2026
eb41e1a
fix abs and ../ file upload in JS
mishushakov Jan 6, 2026
b64e8c2
simplify implementation
mishushakov Jan 6, 2026
044fc37
added tests, updated implementation
mishushakov Jan 6, 2026
be81cb7
added changeset
mishushakov Jan 6, 2026
c7dfb53
fixes logic
mishushakov Jan 6, 2026
aaf2d1c
perserve absolute file paths
mishushakov Jan 6, 2026
5d3aed1
updated impl
mishushakov Jan 6, 2026
2a98000
forward js gzip stream errors
mishushakov Jan 6, 2026
bc2cc16
update rewrite_src signature (python)
mishushakov Jan 6, 2026
ba5213f
updated path handling
mishushakov Jan 6, 2026
7945dcd
updated js tests
mishushakov Jan 6, 2026
e917560
updated tests, normalize path python
mishushakov Jan 6, 2026
3542b2e
fix windows tests
mishushakov Jan 6, 2026
91b18be
fixes js tests on window, normalize paths on rewrite
mishushakov Jan 6, 2026
3a3cf70
normalize glob pattern
mishushakov Jan 6, 2026
ee21f6c
sync python version
mishushakov Jan 6, 2026
435ec35
format & lint
mishushakov Jan 6, 2026
357a65b
normalize glob path in python (windows)
mishushakov Jan 6, 2026
3ea8d7c
normalize path on windows for wcmatch
mishushakov Jan 6, 2026
fe1a9f5
simplified js implementation, removed unused stat
mishushakov Jan 7, 2026
abd00bd
updated python impl
mishushakov Jan 7, 2026
739edf7
undo file change
mishushakov Jan 7, 2026
751dbcc
add trailing newline
mishushakov Jan 7, 2026
c707e7d
fmt
mishushakov Jan 7, 2026
451105a
sync path normalization code
mishushakov Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/gentle-spiders-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': patch
'e2b': patch
---

added handling for absolute file paths (/) and up paths (../)
5 changes: 3 additions & 2 deletions packages/js-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"openapi-typescript": "^7.9.1",
"playwright": "^1.55.1",
"react": "^18.3.1",
"tar": "^7.5.2",
"tsup": "^8.4.0",
"typedoc": "0.26.8",
"typedoc-plugin-markdown": "4.2.7",
Expand Down Expand Up @@ -97,9 +98,9 @@
"compare-versions": "^6.1.0",
"dockerfile-ast": "^0.7.1",
"glob": "^11.1.0",
"modern-tar": "^0.7.3",
"openapi-fetch": "^0.14.1",
"platform": "^1.3.6",
"tar": "^7.5.2"
"platform": "^1.3.6"
},
"engines": {
"node": ">=20"
Expand Down
6 changes: 3 additions & 3 deletions packages/js-sdk/src/template/buildApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,19 @@ export async function getFileUploadLink(

export async function uploadFile(
options: {
fileName: string
filePath: string
fileContextPath: string
url: string
ignorePatterns: string[]
resolveSymlinks: boolean
},
stackTrace: string | undefined
) {
const { fileName, url, fileContextPath, ignorePatterns, resolveSymlinks } =
const { filePath, url, fileContextPath, ignorePatterns, resolveSymlinks } =
options
try {
const { contentLength, uploadStream } = await tarFileStreamUpload(
fileName,
filePath,
fileContextPath,
ignorePatterns,
resolveSymlinks
Expand Down
15 changes: 9 additions & 6 deletions packages/js-sdk/src/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
padOctal,
readDockerignore,
readGCPServiceAccountJSON,
rewriteSrc,
} from './utils'

/**
Expand Down Expand Up @@ -358,7 +359,7 @@ export class TemplateBase

for (const src of srcs) {
const args = [
src.toString(),
rewriteSrc(src.toString(), this.fileContextPath.toString()),
dest.toString(),
options?.user ?? '',
options?.mode ? padOctal(options.mode) : '',
Expand All @@ -370,6 +371,7 @@ export class TemplateBase
force: options?.forceUpload || this.forceNextLayer,
forceUpload: options?.forceUpload,
resolveSymlinks: options?.resolveSymlinks,
filePath: src.toString(),
})
}

Expand Down Expand Up @@ -896,8 +898,9 @@ export class TemplateBase
}

const src = instruction.args.length > 0 ? instruction.args[0] : null
const filePath = instruction.filePath ?? null
const filesHash = instruction.filesHash ?? null
if (src === null || filesHash === null) {
if (src === null || filePath === null || filesHash === null) {
throw new Error('Source path and files hash are required')
}

Expand All @@ -922,7 +925,7 @@ export class TemplateBase
) {
await uploadFile(
{
fileName: src,
filePath,
fileContextPath: this.fileContextPath.toString(),
url,
ignorePatterns: [
Expand Down Expand Up @@ -984,9 +987,9 @@ export class TemplateBase
return instruction
}

const src = instruction.args.length > 0 ? instruction.args[0] : null
const filePath = instruction.filePath ?? null
const dest = instruction.args.length > 1 ? instruction.args[1] : null
if (src === null || dest === null) {
if (filePath === null || dest === null) {
throw new Error('Source path and destination path are required')
}

Expand All @@ -998,7 +1001,7 @@ export class TemplateBase
return {
...instruction,
filesHash: await calculateFilesHash(
src,
filePath,
dest,
this.fileContextPath.toString(),
[
Expand Down
1 change: 1 addition & 0 deletions packages/js-sdk/src/template/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type Instruction = {
forceUpload?: true
filesHash?: string
resolveSymlinks?: boolean
filePath?: string
}

/**
Expand Down
89 changes: 71 additions & 18 deletions packages/js-sdk/src/template/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,38 +255,76 @@ export function padOctal(mode: number): string {
/**
* Create a compressed tar stream of files matching a pattern.
*
* @param fileName Glob pattern for files to include
* @param fileContextPath Base directory for resolving file paths
* @param filePath Original file path pattern (may include .. for outside-context files)
* @param fileContextPath Base directory for resolving relative paths
* @param ignorePatterns Ignore patterns to exclude from the archive
* @param resolveSymlinks Whether to follow symbolic links
* @returns A readable stream of the gzipped tar archive
*/
export async function tarFileStream(
fileName: string,
filePath: string,
fileContextPath: string,
ignorePatterns: string[],
resolveSymlinks: boolean
) {
const { create } = await dynamicImport<typeof import('tar')>('tar')
const modernTar =
dynamicRequire<typeof import('modern-tar/fs')>('modern-tar/fs')
const zlib = dynamicRequire<typeof import('node:zlib')>('node:zlib')

const allFiles = await getAllFilesInPath(
fileName,
filePath,
fileContextPath,
ignorePatterns,
true
)

const filePaths = allFiles.map((file) => file.relativePosix())
const sources = allFiles.map((file) => {
const fullPath = file.fullpath()
const relativePath = file.relativePosix()
const stats = fs.lstatSync(fullPath)

let targetPath: string
// Must match what rewriteSrc produces for COPY instruction consistency
if (path.isAbsolute(filePath)) {
// For absolute paths, use the full path (matching rewriteSrc behavior)
targetPath = fullPath
} else if (filePath.startsWith('..')) {
// For paths outside of the context directory, use the full resolved path
targetPath = fullPath
} else {
// For relative paths within context, use the relative path
targetPath = relativePath
}

return create(
{
gzip: true,
cwd: fileContextPath,
follow: resolveSymlinks,
noDirRecurse: true,
},
filePaths
)
if (stats.isDirectory()) {
return {
type: 'directory' as const,
source: fullPath,
target: targetPath,
}
}

return {
type: 'file' as const,
source: fullPath,
target: targetPath,
}
})

// packTar returns a Node.js Readable stream
const tarStream = modernTar.packTar(sources, {
dereference: resolveSymlinks,
})

// Compress with gzip
const gzipStream = zlib.createGzip()

// Forward errors from tarStream to gzipStream to prevent hanging on errors
// (e.g., file read failure, permission issues)
tarStream.on('error', (err) => gzipStream.destroy(err))
tarStream.pipe(gzipStream)

return gzipStream
}

/**
Expand All @@ -298,14 +336,14 @@ export async function tarFileStream(
* @returns Object containing the content length and upload stream
*/
export async function tarFileStreamUpload(
fileName: string,
filePath: string,
fileContextPath: string,
ignorePatterns: string[],
resolveSymlinks: boolean
) {
// First pass: calculate the compressed size
const sizeCalculationStream = await tarFileStream(
fileName,
filePath,
fileContextPath,
ignorePatterns,
resolveSymlinks
Expand All @@ -318,7 +356,7 @@ export async function tarFileStreamUpload(
return {
contentLength,
uploadStream: await tarFileStream(
fileName,
filePath,
fileContextPath,
ignorePatterns,
resolveSymlinks
Expand Down Expand Up @@ -369,3 +407,18 @@ export function readGCPServiceAccountJSON(
}
return JSON.stringify(pathOrContent)
}

/**
* Rewrite the source path to the target path.
*
* @param src Source path
* @param fileContextPath Base directory for resolving relative paths
* @returns The rewritten source path
*/
export function rewriteSrc(src: string, fileContextPath: string): string {
// return the full path for paths outside of the context directory
if (src.startsWith('..')) {
return path.resolve(fileContextPath, src)
}
return src
}
17 changes: 17 additions & 0 deletions packages/js-sdk/tests/template/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,20 @@
await buildTemplate(template)
}
)

buildTemplateTest(
'build template with absolute paths',
async ({ buildTemplate }) => {
const packageTxt = path.resolve(process.cwd(), folderPath, 'test.txt')

const template = Template()
// using base image to avoid re-building ubuntu:22.04 image
.fromBaseImage()
.skipCache()
.copy(packageTxt, 'text.txt', { forceUpload: true })
.copy('../../../../package.json', 'package.json', { forceUpload: true })
.runCmd(['ls -l .', 'cat text.txt', 'cat package.json'])

Check failure on line 87 in packages/js-sdk/tests/template/build.test.ts

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (windows-latest)

tests/template/build.test.ts > build template with absolute paths

BuildError: failed to run command 'ls -l . && cat text.txt && cat package.json': exit status 1 ❯ tests/template/build.test.ts:87:8

await buildTemplate(template, {}, defaultBuildLogger())
}
)
40 changes: 40 additions & 0 deletions packages/js-sdk/tests/template/utils/rewriteSrc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, test, describe } from 'vitest'
import { rewriteSrc } from '../../../src/template/utils'

describe('rewriteSrc', () => {
const contextPath = '/home/user/project'

test('should return resolved path for parent directory paths', () => {
expect(rewriteSrc('../file.txt', contextPath)).toBe('/home/user/file.txt')

Check failure on line 8 in packages/js-sdk/tests/template/utils/rewriteSrc.test.ts

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (windows-latest)

tests/template/utils/rewriteSrc.test.ts > rewriteSrc > should return resolved path for parent directory paths

AssertionError: expected 'D:\home\user\file.txt' to be '/home/user/file.txt' // Object.is equality Expected: "/home/user/file.txt" Received: "D:\home\user\file.txt" ❯ tests/template/utils/rewriteSrc.test.ts:8:52
expect(rewriteSrc('../../config.json', contextPath)).toBe(
'/home/config.json'
)
expect(rewriteSrc('../dir/file.py', contextPath)).toBe(
'/home/user/dir/file.py'
)
})

test('should preserve relative paths', () => {
expect(rewriteSrc('file.txt', contextPath)).toBe('file.txt')
expect(rewriteSrc('dir/file.txt', contextPath)).toBe('dir/file.txt')
expect(rewriteSrc('./file.txt', contextPath)).toBe('./file.txt')
expect(rewriteSrc('src/components/Button.tsx', contextPath)).toBe(
'src/components/Button.tsx'
)
})

test('should preserve absolute paths', () => {
expect(rewriteSrc('/usr/local/file.txt', contextPath)).toBe(
'/usr/local/file.txt'
)
expect(rewriteSrc('/home/user/project/file.py', contextPath)).toBe(
'/home/user/project/file.py'
)
})

test('should handle glob patterns', () => {
expect(rewriteSrc('*.txt', contextPath)).toBe('*.txt')
expect(rewriteSrc('**/*.py', contextPath)).toBe('**/*.py')
expect(rewriteSrc('../*.txt', contextPath)).toBe('/home/user/*.txt')

Check failure on line 38 in packages/js-sdk/tests/template/utils/rewriteSrc.test.ts

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (windows-latest)

tests/template/utils/rewriteSrc.test.ts > rewriteSrc > should handle glob patterns

AssertionError: expected 'D:\home\user\*.txt' to be '/home/user/*.txt' // Object.is equality Expected: "/home/user/*.txt" Received: "D:\home\user\*.txt" ❯ tests/template/utils/rewriteSrc.test.ts:38:49
})
})
38 changes: 38 additions & 0 deletions packages/js-sdk/tests/template/utils/tarFileStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,42 @@
expect(link.type).toBe('SymbolicLink')
expect(link.linkpath).toBe('original.txt')
})

test('should handle absolute paths', async () => {
// Create test file
const filePath = join(testDir, 'file.txt')
await writeFile(filePath, 'content')

// Use absolute path pattern
const absPattern = join(testDir, '*.txt')
const stream = await tarFileStream(absPattern, testDir, [], false)
const contents = await extractTarContents(stream)

// For absolute paths, the full path is preserved in the archive
// (tar strips the leading slash, so /var/... becomes var/...)
const expectedPath = filePath.replace(/^\//, '')
expect(contents.has(expectedPath)).toBe(true)
expect(contents.get(expectedPath)?.toString()).toBe('content')
})

test('should handle parent directory paths', async () => {
// Create a subdirectory structure
const subdir = join(testDir, 'project', 'src')
await mkdir(subdir, { recursive: true })

// Create a file in the parent directory
const parentFile = join(testDir, 'project', 'config.txt')
await writeFile(parentFile, 'config content')

// Use .. pattern from the subdirectory context
const stream = await tarFileStream('../config.txt', subdir, [], false)
const contents = await extractTarContents(stream)

// For .. paths, the full resolved path should be used in the archive
// (tar strips the leading slash, so /var/... becomes var/...)
const resolvedPath = join(testDir, 'project', 'config.txt')
const expectedPath = resolvedPath.replace(/^\//, '')
expect(contents.has(expectedPath)).toBe(true)

Check failure on line 264 in packages/js-sdk/tests/template/utils/tarFileStream.test.ts

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (windows-latest)

tests/template/utils/tarFileStream.test.ts > tarFileStream > should handle parent directory paths

AssertionError: expected false to be true // Object.is equality - Expected + Received - true + false ❯ tests/template/utils/tarFileStream.test.ts:264:40
expect(contents.get(expectedPath)?.toString()).toBe('config content')
})
})
Loading
Loading