Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
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()),
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
76 changes: 58 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,64 @@ 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)

const isOutsideContext =
path.isAbsolute(filePath) || filePath.startsWith('..')

return create(
{
gzip: true,
cwd: fileContextPath,
follow: resolveSymlinks,
noDirRecurse: true,
},
filePaths
)
const targetPath = isOutsideContext ? path.basename(fullPath) : relativePath

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()
tarStream.pipe(gzipStream)

return gzipStream
}

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

/**
* Rewrite the source path to the target path.
*
* @param src Source path
* @returns The rewritten source path
*/
export function rewriteSrc(src: string): string {
// return only the basename for up dirs
if (src.startsWith('..') || path.isAbsolute(src)) {
return path.basename(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())
}
)
32 changes: 32 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,32 @@
import { expect, test, describe } from 'vitest'
import { rewriteSrc } from '../../../src/template/utils'

describe('rewriteSrc', () => {
test('should return basename for parent directory paths', () => {
expect(rewriteSrc('../file.txt')).toBe('file.txt')
expect(rewriteSrc('../../config.json')).toBe('config.json')
expect(rewriteSrc('../dir/file.py')).toBe('file.py')
})

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

test('should preserve absolute paths', () => {
expect(rewriteSrc('/usr/local/file.txt')).toBe('/usr/local/file.txt')

Check failure on line 21 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 preserve absolute paths

AssertionError: expected 'file.txt' to be '/usr/local/file.txt' // Object.is equality Expected: "/usr/local/file.txt" Received: "file.txt" ❯ tests/template/utils/rewriteSrc.test.ts:21:47

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

View workflow job for this annotation

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

tests/template/utils/rewriteSrc.test.ts > rewriteSrc > should preserve absolute paths

AssertionError: expected 'file.txt' to be '/usr/local/file.txt' // Object.is equality Expected: "/usr/local/file.txt" Received: "file.txt" ❯ tests/template/utils/rewriteSrc.test.ts:21:47
expect(rewriteSrc('/home/user/project/file.py')).toBe(
'/home/user/project/file.py'
)
})

test('should handle glob patterns', () => {
expect(rewriteSrc('*.txt')).toBe('*.txt')
expect(rewriteSrc('**/*.py')).toBe('**/*.py')
expect(rewriteSrc('../*.txt')).toBe('*.txt')
})
})
33 changes: 33 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,37 @@ describe('tarFileStream', () => {
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 (outside context), only the basename should be used
expect(contents.has('file.txt')).toBe(true)
expect(contents.get('file.txt')?.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, only the basename should be used in the archive
expect(contents.has('config.txt')).toBe(true)
expect(contents.get('config.txt')?.toString()).toBe('config content')
})
})
12 changes: 8 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,
rewrite_src,
)
from types import TracebackType

Expand Down Expand Up @@ -65,8 +66,9 @@ def copy(
srcs = [src] if isinstance(src, (str, Path)) else src

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

self._template._instructions.append(instruction)
Expand Down Expand Up @@ -1275,22 +1278,23 @@ def _instructions_with_hashes(
"force": instruction["force"],
"forceUpload": instruction.get("forceUpload"),
"resolveSymlinks": instruction.get("resolveSymlinks"),
"filePath": instruction.get("filePath"),
}

if instruction["type"] == InstructionType.COPY:
stack_trace = None
if index + 1 < len(self._stack_traces):
stack_trace = self._stack_traces[index + 1]

file_path = instruction.get("filePath")
args = instruction.get("args", [])
src = args[0] if len(args) > 0 else None
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["filesHash"] = calculate_files_hash(
src,
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[str]]


class GenericDockerRegistry(TypedDict):
Expand Down
Loading
Loading