diff --git a/opendevin/sandbox/e2b/sandbox.py b/opendevin/sandbox/e2b/sandbox.py index 484daa847a..e5455531e7 100644 --- a/opendevin/sandbox/e2b/sandbox.py +++ b/opendevin/sandbox/e2b/sandbox.py @@ -1,3 +1,6 @@ +import os +import tarfile +from glob import glob from typing import Dict, Tuple from e2b import Sandbox as E2BSandbox from e2b.sandbox.exception import ( @@ -15,6 +18,7 @@ class E2BBox(Sandbox): closed = False cur_background_id = 0 background_commands: Dict[int, Process] = {} + _cwd: str = '/home/user' def __init__( self, @@ -27,7 +31,7 @@ class E2BBox(Sandbox): # It's possible to stream stdout and stderr from sandbox and from each process on_stderr=lambda x: logger.info(f'E2B sandbox stderr: {x}'), on_stdout=lambda x: logger.info(f'E2B sandbox stdout: {x}'), - cwd='/home/user', # Default workdir inside sandbox + cwd=self._cwd, # Default workdir inside sandbox ) self.timeout = timeout logger.info(f'Started E2B sandbox with ID "{self.sandbox.id}"') @@ -36,6 +40,23 @@ class E2BBox(Sandbox): def filesystem(self): return self.sandbox.filesystem + def _archive(self, host_src: str, recursive: bool = False): + if recursive: + assert os.path.isdir(host_src), 'Source must be a directory when recursive is True' + files = glob(host_src + '/**/*', recursive=True) + srcname = os.path.basename(host_src) + tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar') + with tarfile.open(tar_filename, mode='w') as tar: + for file in files: + tar.add(file, arcname=os.path.relpath(file, os.path.dirname(host_src))) + else: + assert os.path.isfile(host_src), 'Source must be a file when recursive is False' + srcname = os.path.basename(host_src) + tar_filename = os.path.join(os.path.dirname(host_src), srcname + '.tar') + with tarfile.open(tar_filename, mode='w') as tar: + tar.add(host_src, arcname=srcname) + return tar_filename + # TODO: This won't work if we didn't wait for the background process to finish def read_logs(self, process_id: int) -> str: proc = self.background_commands.get(process_id) @@ -62,8 +83,28 @@ class E2BBox(Sandbox): return process_output.exit_code, logs_str def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False): - # FIXME - raise NotImplementedError('Copying files to E2B sandbox is not implemented yet') + """Copies a local file or directory to the sandbox.""" + tar_filename = self._archive(host_src, recursive) + + # Prepend the sandbox destination with our sandbox cwd + sandbox_dest = os.path.join(self._cwd, sandbox_dest.lstrip('/')) + + with open(tar_filename, 'rb') as tar_file: + # Upload the archive to /home/user (default destination that always exists) + uploaded_path = self.sandbox.upload_file(tar_file) + + # Check if sandbox_dest exists. If not, create it. + process = self.sandbox.process.start_and_wait(f'test -d {sandbox_dest}') + if process.exit_code != 0: + self.sandbox.filesystem.make_dir(sandbox_dest) + + # Extract the archive into the destination and delete the archive + process = self.sandbox.process.start_and_wait(f'sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}') + if process.exit_code != 0: + raise Exception(f'Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}') + + # Delete the local archive + os.remove(tar_filename) def execute_in_background(self, cmd: str) -> Process: process = self.sandbox.process.start(cmd)