Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
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
21 changes: 12 additions & 9 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,
relativizePath,
} from './utils'

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

for (const src of srcs) {
const args = [
src.toString(),
relativizePath(src, this.fileContextPath),
dest.toString(),
options?.user ?? '',
options?.mode ? padOctal(options.mode) : '',
Expand All @@ -369,6 +370,7 @@ export class TemplateBase
args,
force: options?.forceUpload || this.forceNextLayer,
forceUpload: options?.forceUpload,
filePath: src,
resolveSymlinks: options?.resolveSymlinks,
})
}
Expand Down Expand Up @@ -895,9 +897,11 @@ export class TemplateBase
return
}

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

Expand All @@ -922,7 +926,7 @@ export class TemplateBase
) {
await uploadFile(
{
fileName: src,
filePath: filePath.toString(),
fileContextPath: this.fileContextPath.toString(),
url,
ignorePatterns: [
Expand All @@ -934,14 +938,14 @@ export class TemplateBase
stackTrace
)
options.onBuildLogs?.(
new LogEntry(new Date(), 'info', `Uploaded '${src}'`)
new LogEntry(new Date(), 'info', `Uploaded '${filePath}'`)
)
} else {
options.onBuildLogs?.(
new LogEntry(
new Date(),
'info',
`Skipping upload of '${src}', already cached`
`Skipping upload of '${filePath}', already cached`
)
)
}
Expand Down Expand Up @@ -984,9 +988,8 @@ export class TemplateBase
return instruction
}

const src = instruction.args.length > 0 ? instruction.args[0] : null
const dest = instruction.args.length > 1 ? instruction.args[1] : null
if (src === null || dest === null) {
if (!instruction.filePath || !dest) {
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,
instruction.filePath.toString(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS sends unfiltered filePath to backend, Python doesn't

The JS serialize method passes the entire steps array directly to the API, including the new filePath field which contains the original absolute path and is of type PathLike (could be Buffer/URL). In contrast, Python's _serialize explicitly constructs new step objects with only type, args, force, filesHash, and forceUpload fields, filtering out filePath and resolveSymlinks. This inconsistency means the JS SDK sends potentially sensitive path information and non-string PathLike objects to the backend that the Python SDK properly excludes.

Fix in Cursor Fix in Web

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?: PathLike
}

/**
Expand Down
98 changes: 78 additions & 20 deletions packages/js-sdk/src/template/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from 'node:crypto'
import fs from 'node:fs'
import fs, { PathLike } from 'node:fs'
import path from 'node:path'
import { dynamicImport, dynamicRequire } from '../utils'
import { BASE_STEP_NAME, FINALIZE_STEP_NAME } from './consts'
Expand Down Expand Up @@ -54,7 +54,8 @@ export async function getAllFilesInPath(
ignore: ignorePatterns,
withFileTypes: true,
// this is required so that the ignore pattern is relative to the file path
cwd: contextPath,
// if the contextPath is absolute, we don't need to set the cwd
cwd: path.isAbsolute(contextPath) ? undefined : contextPath,
})

for (const file of globFiles) {
Expand All @@ -72,7 +73,7 @@ export async function getAllFilesInPath(
const dirFiles = await glob(dirPattern, {
ignore: ignorePatterns,
withFileTypes: true,
cwd: contextPath,
cwd: path.isAbsolute(contextPath) ? undefined : contextPath,
})
dirFiles.forEach((f) => files.set(f.fullpath(), f))
} else {
Expand Down Expand Up @@ -255,38 +256,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 filePath Glob pattern for files to include
* @param fileName Name of the file in the tar archive
* @param fileContextPath Base directory for resolving file 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 { packTar } =
await dynamicImport<typeof import('modern-tar/fs')>('modern-tar/fs')
const { createGzip } =
await dynamicImport<typeof import('node:zlib')>('node:zlib')

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

const filePaths = allFiles.map((file) => file.relativePosix())
const sources: Array<{
type: 'file' | 'directory'
source: string
target: string
}> = []

return create(
{
gzip: true,
cwd: fileContextPath,
follow: resolveSymlinks,
noDirRecurse: true,
},
filePaths
)
for (const file of allFiles) {
const sourcePath = file.fullpathPosix()
const targetPath = relativizePath(sourcePath, fileContextPath)

if (file.isDirectory()) {
sources.push({
type: 'directory',
source: sourcePath,
target: targetPath,
})
} else {
sources.push({
type: 'file',
source: sourcePath,
target: targetPath,
})
}
}

// Create tar stream with gzip compression
const tarStream = packTar(sources, {
dereference: resolveSymlinks, // Follow symlinks when resolveSymlinks is true
})

// Pipe through gzip compression
const gzipStream = createGzip()
tarStream.pipe(gzipStream)

return gzipStream
}

/**
Expand All @@ -298,14 +327,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 +347,7 @@ export async function tarFileStreamUpload(
return {
contentLength,
uploadStream: await tarFileStream(
fileName,
filePath,
fileContextPath,
ignorePatterns,
resolveSymlinks
Expand Down Expand Up @@ -369,3 +398,32 @@ export function readGCPServiceAccountJSON(
}
return JSON.stringify(pathOrContent)
}

/**
* Convert absolute paths to relativized paths.
* In addition to converting absolute paths to relative paths,
* it strips up directories (../ or ..\ on Windows).
*
* @param src Absolute path to convert
* @param fileContextPath Base directory for resolving relative paths
* @returns Relative path with forward slashes (for tar/cross-platform compatibility)
*/
export function relativizePath(
src: PathLike,
fileContextPath: PathLike
): string {
let rewrittenPath = src.toString()

// Convert absolute paths to relative paths
if (path.isAbsolute(rewrittenPath)) {
const contextPath = path.resolve(fileContextPath.toString())
const relativePath = path.relative(contextPath, rewrittenPath)
rewrittenPath = relativePath
}

// Strip up directories (../ or ..\ on Windows)
rewrittenPath = rewrittenPath.replace(/\.\.(\/|\\)/g, '')

// Normalize to forward slashes for cross-platform compatibility (tar archives require forward slashes)
return normalizePath(rewrittenPath)
}
18 changes: 18 additions & 0 deletions packages/js-sdk/tests/template/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
const template = Template()
// using base image to avoid re-building ubuntu:22.04 image
.fromBaseImage()
.copy('folder/*', 'folder', { forceUpload: true })

Check failure on line 34 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

Error: No files found in D:\a\E2B\E2B\packages\js-sdk\tests\template\folder\* ❯ tests/template/build.test.ts:34:6

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

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (ubuntu-22.04)

tests/template/build.test.ts > build template

Error: No files found in /home/runner/work/E2B/E2B/packages/js-sdk/tests/template/folder/* ❯ tests/template/build.test.ts:34:6
.runCmd('cat folder/test.txt')
.setWorkdir('/app')
.setStartCmd('echo "Hello, world!"', waitForTimeout(10_000))
Expand All @@ -51,7 +51,7 @@
const template = Template()
.fromImage('ubuntu:22.04')
.skipCache()
.copy('folder/*', 'folder', { forceUpload: true })

Check failure on line 54 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 symlinks

Error: No files found in D:\a\E2B\E2B\packages\js-sdk\tests\template\folder\* ❯ tests/template/build.test.ts:54:6

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

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (ubuntu-22.04)

tests/template/build.test.ts > build template with symlinks

Error: No files found in /home/runner/work/E2B/E2B/packages/js-sdk/tests/template/folder/* ❯ tests/template/build.test.ts:54:6
.runCmd('cat folder/symlink.txt')

await buildTemplate(template)
Expand All @@ -63,7 +63,7 @@
const template = Template()
.fromImage('ubuntu:22.04')
.skipCache()
.copy('folder/symlink.txt', 'folder/symlink.txt', {

Check failure on line 66 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 resolveSymlinks

Error: No files found in D:\a\E2B\E2B\packages\js-sdk\tests\template\folder\symlink.txt ❯ tests/template/build.test.ts:66:8

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

View workflow job for this annotation

GitHub Actions / JS SDK - Build and test (ubuntu-22.04)

tests/template/build.test.ts > build template with resolveSymlinks

Error: No files found in /home/runner/work/E2B/E2B/packages/js-sdk/tests/template/folder/symlink.txt ❯ tests/template/build.test.ts:66:8
forceUpload: true,
resolveSymlinks: true,
})
Expand All @@ -72,3 +72,21 @@
await buildTemplate(template)
}
)

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

const template = Template()
// using base image to avoid re-building ubuntu:22.04 image
.fromBaseImage()
.skipCache()
.copy(packageTxt, 'text.txt', { forceUpload: true })

Check failure on line 86 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 move files in sandbox: exit status 1 ❯ tests/template/build.test.ts:86:8
.copy(rootTxt, 'package.json', { forceUpload: true })
.runCmd(['ls -l .', 'cat text.txt', 'cat package.json'])

await buildTemplate(template, {}, defaultBuildLogger())
}
)
11 changes: 7 additions & 4 deletions packages/python-sdk/e2b/template/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
read_dockerignore,
read_gcp_service_account_json,
get_caller_frame,
relativize_path,
)
from types import TracebackType

Expand Down Expand Up @@ -66,7 +67,7 @@ def copy(

for src_item in srcs:
args = [
str(src_item),
relativize_path(str(src_item), self._template._file_context_path),
str(dest),
user or "",
pad_octal(mode) if mode else "",
Expand All @@ -78,6 +79,7 @@ def copy(
"force": force_upload or self._template._force_next_layer,
"forceUpload": force_upload,
"resolveSymlinks": resolve_symlinks,
"filePath": src_item,
}

self._template._instructions.append(instruction)
Expand Down Expand Up @@ -1283,14 +1285,15 @@ def _instructions_with_hashes(
stack_trace = self._stack_traces[index + 1]

args = instruction.get("args", [])
src = args[0] if len(args) > 0 else None
file_path = instruction.get("filePath")
dest = args[1] if len(args) > 1 else None
if src is None or dest is None:
if file_path is None or dest is None:
raise ValueError("Source path and destination path are required")

resolve_symlinks = instruction.get("resolveSymlinks")
step["filePath"] = file_path
step["filesHash"] = calculate_files_hash(
src,
str(file_path),
dest,
self._file_context_path,
[
Expand Down
1 change: 1 addition & 0 deletions packages/python-sdk/e2b/template/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Instruction(TypedDict):
forceUpload: NotRequired[Optional[Literal[True]]]
filesHash: NotRequired[Optional[str]]
resolveSymlinks: NotRequired[Optional[bool]]
filePath: NotRequired[Optional[Union[str, Path]]]


class GenericDockerRegistry(TypedDict):
Expand Down
Loading
Loading