Skip to content

Commit

Permalink
Merge branch 'folder-http-endpoint' into folder-endpoint-access
Browse files Browse the repository at this point in the history
# Conflicts:
#	runhouse/servers/http/http_server.py
#	runhouse/servers/http/http_utils.py
  • Loading branch information
jlewitt1 committed Aug 6, 2024
2 parents 02473ff + e829646 commit fb2180a
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 41 deletions.
33 changes: 16 additions & 17 deletions runhouse/servers/http/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,18 @@
folder_get,
folder_ls,
folder_mkdir,
folder_mv,
folder_put,
folder_rm,
FolderMethod,
FolderOperation,
FolderParams,
get_token_from_request,
handle_exception_response,
OutputType,
PutObjectParams,
PutResourceParams,
RenameObjectParams,
resolve_folder_path,
Response,
serialize_data,
ServerSettings,
Expand Down Expand Up @@ -622,35 +624,32 @@ async def rename_object(request: Request, params: RenameObjectParams):
)

@staticmethod
@app.post("/folder")
@app.post("/folder/method/{operation}")
@validate_cluster_access
async def folder_operation(request: Request, folder_params: FolderParams):
async def folder_operation(
request: Request, operation: FolderOperation, folder_params: FolderParams
):
try:
operation = folder_params.operation
raw_path = folder_params.path
path = (
None
if raw_path is None
else Path(raw_path).expanduser()
if raw_path.startswith("~")
else Path(raw_path).resolve()
)
path = resolve_folder_path(folder_params.path)

if operation == FolderMethod.MKDIR:
if operation == FolderOperation.MKDIR:
return folder_mkdir(path)

elif operation == FolderMethod.GET:
elif operation == FolderOperation.GET:
return folder_get(path, folder_params)

elif operation == FolderMethod.PUT:
elif operation == FolderOperation.PUT:
return folder_put(path, folder_params)

elif operation == FolderMethod.LS:
elif operation == FolderOperation.LS:
return folder_ls(path, folder_params)

elif operation == FolderMethod.RM:
elif operation == FolderOperation.RM:
return folder_rm(path, folder_params)

elif operation == FolderOperation.MV:
return folder_mv(path, folder_params)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
Expand Down
105 changes: 81 additions & 24 deletions runhouse/servers/http/http_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import codecs
import json
import re
import shutil
import sys
from enum import Enum
from pathlib import Path
Expand Down Expand Up @@ -88,22 +89,23 @@ class OutputType:
CONFIG = "config"


class FolderMethod(str, Enum):
class FolderOperation(str, Enum):
GET = "get"
LS = "ls"
PUT = "put"
MKDIR = "mkdir"
RM = "rm"
MV = "mv"


class FolderParams(BaseModel):
operation: FolderMethod
path: Optional[str] = None
mode: Optional[str] = None
serialization: Optional[str] = None
overwrite: Optional[bool] = False
recursive: Optional[bool] = False
contents: Optional[Any] = None
dest_path: Optional[str] = None


def pickle_b64(picklable):
Expand Down Expand Up @@ -306,6 +308,18 @@ def handle_response(
###########################
#### Folder Operations ####
###########################


def resolve_folder_path(path: str):
return (
None
if path is None
else Path(path).expanduser()
if path.startswith("~")
else Path(path).resolve()
)


def folder_mkdir(path: Path):
if not path.parent.is_dir():
raise ValueError(
Expand All @@ -322,8 +336,24 @@ def folder_get(path: Path, folder_params: FolderParams):
serialization = folder_params.serialization
binary_mode = "b" in mode

with open(path, mode=mode) as f:
file_contents = f.read()
if not path.exists():
raise HTTPException(status_code=404, detail=f"Path {path} does not exist")

try:
with open(path, mode=mode) as f:
file_contents = f.read()
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"File {path} not found")

except PermissionError:
raise HTTPException(
status_code=403, detail=f"Permission denied for file in path {path}"
)

except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error reading file {path}: {str(e)}"
)

if binary_mode and isinstance(file_contents, bytes):
file_contents = file_contents.decode()
Expand All @@ -345,16 +375,17 @@ def folder_put(path: Path, folder_params: FolderParams):
contents = folder_params.contents

if not path.is_dir():
raise ValueError(f"Path {path} is not a directory")
raise HTTPException(status_code=404, detail=f"Path {path} is not a directory")

path.mkdir(exist_ok=True)

if overwrite is False:
existing_files = {str(item.name) for item in path.iterdir()}
intersection = existing_files.intersection(set(contents.keys()))
if intersection:
raise FileExistsError(
f"File(s) {intersection} already exist(s) at path: {path}"
raise HTTPException(
status_code=409,
detail=f"File(s) {intersection} already exist(s) at path: {path}",
)

for filename, file_obj in contents.items():
Expand All @@ -369,29 +400,31 @@ def folder_put(path: Path, folder_params: FolderParams):

file_path = path / filename
if not overwrite and file_path.exists():
raise FileExistsError(f"File {file_path} already exists.")
raise HTTPException(
status_code=409, detail=f"File {file_path} already exists"
)

try:
with open(file_path, mode) as f:
f.write(file_obj)
except Exception as e:
raise e
HTTPException(status_code=500, detail=f"Failed to write file: {str(e)}")

return Response(output_type=OutputType.SUCCESS)


def folder_ls(path: Path, folder_params: FolderParams):
recursive: bool = folder_params.recursive
if path is None:
raise ValueError("Path is required for ls operation")

if not path.exists():
raise FileNotFoundError(f"Path {path} does not exist")
raise HTTPException(status_code=404, detail=f"Path {path} does not exist")

if not path.is_dir():
raise ValueError(f"Path {path} is not a directory")
raise HTTPException(status_code=400, detail=f"Path {path} is not a directory")

files = list(path.rglob("*")) if recursive else [item for item in path.iterdir()]
files = (
list(path.rglob("*"))
if folder_params.recursive
else [item for item in path.iterdir()]
)
return Response(
data=files,
output_type=OutputType.RESULT_SERIALIZED,
Expand All @@ -400,8 +433,6 @@ def folder_ls(path: Path, folder_params: FolderParams):


def folder_rm(path: Path, folder_params: FolderParams):
import shutil

recursive: bool = folder_params.recursive
contents = folder_params.contents
if contents:
Expand All @@ -413,9 +444,11 @@ def folder_rm(path: Path, folder_params: FolderParams):
elif content_path.is_dir() and recursive:
shutil.rmtree(content_path)
else:
raise ValueError(
f"Path {content_path} is a directory and recursive is set to False"
raise HTTPException(
status_code=400,
detail=f"Path {content_path} is a directory and recursive is set to False",
)

return Response(output_type=OutputType.SUCCESS)

if not path.is_dir():
Expand All @@ -426,18 +459,42 @@ def folder_rm(path: Path, folder_params: FolderParams):
shutil.rmtree(path)
return Response(output_type=OutputType.SUCCESS)

items = path.iterdir()
items = list(path.iterdir())
if not items:
# Empty dir
# Remove the empty directory
path.rmdir()
return Response(output_type=OutputType.SUCCESS)

# Remove file contents, but not the directory itself (since recursive not set to `True`)
for item in items:
if item.is_file():
item.unlink()
else:
raise ValueError(
f"Folder {item} found in {path}, recursive is set to False"
raise HTTPException(
status_code=400,
detail=f"Folder {item} found in {path}, recursive is set to `False`",
)

return Response(output_type=OutputType.SUCCESS)


def folder_mv(path: Path, folder_params: FolderParams):
dest_path = resolve_folder_path(folder_params.dest_path)

if not path.exists():
raise HTTPException(
status_code=404, detail=f"The source path {path} does not exist"
)

if dest_path.exists():
raise HTTPException(
status_code=409, detail=f"The destination path {dest_path} already exists"
)

# Create the destination directory if it doesn't exist
dest_path.parent.mkdir(parents=True, exist_ok=True)

# Move the directory
shutil.move(str(path), str(dest_path))

return Response(output_type=OutputType.SUCCESS)

0 comments on commit fb2180a

Please sign in to comment.