Skip to content
Open
Show file tree
Hide file tree
Changes from 22 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
135 changes: 107 additions & 28 deletions packages/js-sdk/src/template/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'node:path'
import { dynamicImport, dynamicRequire } from '../utils'
import { BASE_STEP_NAME, FINALIZE_STEP_NAME } from './consts'
import type { Path } from 'glob'
import type { TarSource } from 'modern-tar/fs'

/**
* Read and parse a .dockerignore file.
Expand Down Expand Up @@ -33,6 +34,32 @@ function normalizePath(path: string): string {
return path.replace(/\\/g, '/')
}

/**
* Convert a filesystem path to POSIX format for use in tar archives and Dockerfiles.
*
* Tar archives and Docker expect POSIX-style paths (forward slashes).
* On Windows, the drive letter (e.g., C:) is stripped and backslashes are converted.
*
* @param fsPath - The filesystem path to convert
* @returns The POSIX-formatted path suitable for tar/Docker
*
* @example
* ```ts
* toPosixPath('D:\\a\\E2B\\file.txt') // Returns 'a/E2B/file.txt'
* toPosixPath('/home/user/file.txt') // Returns 'home/user/file.txt'
* ```
*/
export function toPosixPath(fsPath: string): string {
// Normalize to forward slashes (POSIX format used by tar)
let posixPath = fsPath.replace(/\\/g, '/')
// Strip Windows drive letter (e.g., C:)
if (posixPath.length >= 2 && posixPath[1] === ':') {
posixPath = posixPath.slice(2)
}
// Strip leading slash
return posixPath.replace(/^\//, '')
}

/**
* Get all files for a given path and ignore patterns.
*
Expand All @@ -50,11 +77,18 @@ export async function getAllFilesInPath(
const { glob } = await dynamicImport<typeof import('glob')>('glob')
const files = new Map<string, Path>()

const globFiles = await glob(src, {
// For absolute paths, don't use cwd as glob will handle them directly
// For relative paths, use cwd to resolve relative to contextPath
const isAbsoluteSrc = path.isAbsolute(src)

// Normalize path separators for glob (glob expects forward slashes even on Windows)
const globPattern = normalizePath(src)
const cwd = isAbsoluteSrc ? process.cwd() : contextPath

const globFiles = await glob(globPattern, {
ignore: ignorePatterns,
withFileTypes: true,
// this is required so that the ignore pattern is relative to the file path
cwd: contextPath,
cwd,
})

for (const file of globFiles) {
Expand All @@ -63,16 +97,11 @@ export async function getAllFilesInPath(
if (includeDirectories) {
files.set(file.fullpath(), file)
}
const dirPattern = normalizePath(
// When the matched directory is '.', `file.relative()` can be an empty string.
// In that case, we want to match all files under the current directory instead of
// creating an absolute glob like '/**/*' which would traverse the entire filesystem.
path.join(file.relative() || '.', '**/*')
)
const dirPattern = normalizePath(path.join(file.fullpath(), '**/*'))
const dirFiles = await glob(dirPattern, {
ignore: ignorePatterns,
withFileTypes: true,
cwd: contextPath,
cwd,
})
dirFiles.forEach((f) => files.set(f.fullpath(), f))
} else {
Expand Down Expand Up @@ -255,38 +284,66 @@ 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: TarSource[] = allFiles.map((file) => {
const fullPath = file.fullpath()
const relativePath = file.relativePosix()

const asAbsolutePath =
path.isAbsolute(filePath) || filePath.startsWith('..')
const targetPath = asAbsolutePath ? toPosixPath(fullPath) : relativePath

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

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

// 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 +355,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 +375,7 @@ export async function tarFileStreamUpload(
return {
contentLength,
uploadStream: await tarFileStream(
fileName,
filePath,
fileContextPath,
ignorePatterns,
resolveSymlinks
Expand Down Expand Up @@ -369,3 +426,25 @@ export function readGCPServiceAccountJSON(
}
return JSON.stringify(pathOrContent)
}

/**
* Rewrite the source path to the target path.
*
* For paths outside the context directory (starting with ..) or absolute paths,
* returns the full resolved path in POSIX format for Docker/tar compatibility.
*
* @param src Source path
* @param fileContextPath Base directory for resolving relative paths
* @returns The rewritten source path in POSIX format
*/
export function rewriteSrc(src: string, fileContextPath: string): string {
// For absolute paths, convert to POSIX format for Docker/tar compatibility
if (path.isAbsolute(src)) {
return toPosixPath(src)
}
// For paths outside of the context directory, return the full resolved path in POSIX format
if (src.startsWith('..')) {
return toPosixPath(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 @@ buildTemplateTest(
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'])

await buildTemplate(template, {}, defaultBuildLogger())
}
)
Loading
Loading