Skip to content

Commit

Permalink
Merge pull request #332 from latchbio/ayush/mv-glob
Browse files Browse the repository at this point in the history
  • Loading branch information
ayushkamat authored Oct 24, 2023
2 parents 7e9d0f2 + d76a4d8 commit 90b5d7a
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 85 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Types of changes

# Latch SDK Changelog

## 2.36.1 - 2023-10-24

### Changed

* `latch mv` now supports glob patterns (with the same restrictions as `latch cp`)

## 2.36.0 - 2023-10-23

### Added
Expand Down
14 changes: 11 additions & 3 deletions latch_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,17 +347,25 @@ def cp(


@main.command("mv")
@click.argument("src", shell_complete=remote_complete, nargs=-1)
@click.argument("src", shell_complete=remote_complete, nargs=1)
@click.argument("dest", shell_complete=remote_complete, nargs=1)
def mv(src: str, dest: str):
@click.option(
"--no-glob",
"-G",
help="Don't expand globs in remote paths",
is_flag=True,
default=False,
show_default=True,
)
def mv(src: str, dest: str, no_glob: bool):
"""Move remote files in LatchData."""

crash_handler.message = f"Unable to move {src} to {dest}"
crash_handler.pkg_root = str(Path.cwd())

from latch_cli.services.move import move

move(src, dest)
move(src, dest, no_glob=no_glob)


@main.command("ls")
Expand Down
3 changes: 3 additions & 0 deletions latch_cli/services/cp/ldata_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ def _get_immediate_children_of_node(path: str) -> List[str]:
{"argPath": path},
)["ldataResolvePathData"]

if lrpd is None:
return []

res: List[str] = []
for node in lrpd["childLdataTreeEdges"]["nodes"]:
res.append(node["child"]["name"])
Expand Down
208 changes: 126 additions & 82 deletions latch_cli/services/move.py
Original file line number Diff line number Diff line change
@@ -1,105 +1,149 @@
from textwrap import dedent

import click
import gql
from gql.transport.exceptions import TransportQueryError
from latch_sdk_gql.execute import execute

from latch_cli.services.cp.glob import expand_pattern
from latch_cli.services.cp.ldata_utils import LDataNodeType, get_node_data
from latch_cli.utils.path import get_name_from_path, get_path_error, is_remote_path


def move(
src: str,
dest: str,
*,
no_glob: bool = False,
):
if not is_remote_path(src) or not is_remote_path(dest):
raise ValueError(
click.secho(
f"`latch mv` cannot be used for local file operations. Please make sure"
f" both of your input paths are remote (beginning with `latch://`)"
f" all of your input paths are remote (beginning with `latch://`)",
fg="red",
)
raise click.exceptions.Exit(1)

if no_glob:
srcs = [src]
else:
srcs = expand_pattern(src)

if len(srcs) == 0:
click.secho(f"Could not find any files that match pattern {src}. Exiting.")
raise click.exceptions.Exit(0)

node_data = get_node_data(src, dest, allow_resolve_to_parent=True)
node_data = get_node_data(*srcs, dest, allow_resolve_to_parent=True)

src_data = node_data.data[src]
dest_data = node_data.data[dest]
acc_id = node_data.acc_id

path_by_id = {v.id: k for k, v in node_data.data.items()}

if src_data.is_parent:
raise get_path_error(src, "not found", acc_id)

new_name = None
if dest_data.is_parent:
new_name = get_name_from_path(dest)
elif dest_data.type in {LDataNodeType.obj, LDataNodeType.link}:
raise get_path_error(dest, "object already exists at path.", acc_id)

try:
execute(
gql.gql("""
mutation Move(
$argNode: BigInt!
$argDestParent: BigInt!
$argNewName: String
) {
ldataMove(
input: {
argNode: $argNode
argDestParent: $argDestParent
argNewName: $argNewName
}
) {
clientMutationId
}
}"""),
{
"argNode": src_data.id,
"argDestParent": dest_data.id,
"argNewName": new_name,
},
multi_src = len(srcs) > 1

if multi_src and dest_data.is_parent:
click.secho(
f"Remote destination {dest} does not exist. Cannot move multiple source"
" files to a destination that does not exist.",
fg="red",
)
except TransportQueryError as e:
if e.errors is None or len(e.errors) == 0:
raise e

msg: str = e.errors[0]["message"]

if msg.startswith("Permission denied on node"):
node_id = msg.rsplit(" ", 1)[1]
path = path_by_id[node_id]

raise get_path_error(path, "permission denied.", acc_id) from e
elif msg == "Refusing to make node its own parent":
raise get_path_error(dest, f"is a parent of {src}.", acc_id) from e
elif msg == "Refusing to parent node to an object node":
raise get_path_error(dest, f"object exists at path.", acc_id) from e
elif msg == "Refusing to move a share link (or into a share link)":
if src_data.type is LDataNodeType.link:
path = src
raise click.exceptions.Exit(1)
elif multi_src and dest_data.type in {LDataNodeType.obj, LDataNodeType.link}:
click.secho(
f"Remote destination {dest} is not a directory. Cannot move multiple source"
" files to a destination that is not a directory.",
fg="red",
)
raise click.exceptions.Exit(1)

for s in srcs:
src_data = node_data.data[s]

path_by_id = {v.id: k for k, v in node_data.data.items()}

if src_data.is_parent:
raise get_path_error(s, "not found", acc_id)

new_name = None
if dest_data.is_parent:
new_name = get_name_from_path(dest)
elif dest_data.type in {LDataNodeType.obj, LDataNodeType.link}:
raise get_path_error(dest, "object already exists at path.", acc_id)

try:
execute(
gql.gql("""
mutation Move(
$argNode: BigInt!
$argDestParent: BigInt!
$argNewName: String
) {
ldataMove(
input: {
argNode: $argNode
argDestParent: $argDestParent
argNewName: $argNewName
}
) {
clientMutationId
}
}
"""),
{
"argNode": src_data.id,
"argDestParent": dest_data.id,
"argNewName": new_name,
},
)
except TransportQueryError as e:
if e.errors is None or len(e.errors) == 0:
raise e

msg: str = e.errors[0]["message"]

if msg.startswith("Permission denied on node"):
node_id = msg.rsplit(" ", 1)[1]
path = path_by_id[node_id]

raise get_path_error(path, "permission denied.", acc_id) from e
elif msg == "Refusing to make node its own parent":
raise get_path_error(dest, f"is a parent of {s}.", acc_id) from e
elif msg == "Refusing to parent node to an object node":
raise get_path_error(dest, f"object exists at path.", acc_id) from e
elif msg == "Refusing to move a share link (or into a share link)":
if src_data.type is LDataNodeType.link:
path = s
else:
path = dest

raise get_path_error(path, f"is a share link.", acc_id) from e
elif msg.startswith("Refusing to move account root"):
raise get_path_error(s, "is an account root.", acc_id) from e
elif msg.startswith("Refusing to move removed node"):
raise get_path_error(s, "not found.", acc_id) from e
elif msg.startswith("Refusing to move already moved node"):
raise get_path_error(
s,
"copy in progress. Please wait until the node has finished copying"
" before moving.",
acc_id,
) from e
elif msg == "Conflicting object in destination":
raise get_path_error(dest, "object exists at path.", acc_id) from e
elif msg.startswith("Refusing to do noop move"):
raise get_path_error(dest, "cannot move node to itself.", acc_id) from e
else:
path = dest

raise get_path_error(path, f"is a share link.", acc_id) from e
elif msg.startswith("Refusing to move account root"):
raise get_path_error(src, "is an account root.", acc_id) from e
elif msg.startswith("Refusing to move removed node"):
raise get_path_error(src, "not found.", acc_id) from e
elif msg.startswith("Refusing to move already moved node"):
raise get_path_error(
src,
"copy in progress. Please wait until the node has finished copying"
" before moving.",
acc_id,
) from e
elif msg == "Conflicting object in destination":
raise get_path_error(dest, "object exists at path.", acc_id) from e
elif msg.startswith("Refusing to do noop move"):
raise get_path_error(dest, "cannot move node to itself.", acc_id) from e
else:
raise e

click.echo(f"""
{click.style("Move Succeeded.", fg="green")}
{click.style("Source: ", fg="blue")}{(src)}
{click.style("Destination: ", fg="blue")}{(dest)}""")
raise e

if len(srcs) == 1:
src_str = f'{click.style("Source: ", fg="blue")}{srcs[0]}'
else:
src_str = "\n".join(
[click.style("Sources: ", fg="blue"), *[f" {s}" for s in srcs]]
)

click.echo(dedent(f"""
{click.style("Move Succeeded.", fg="green")}
__srcs__
{click.style("Destination: ", fg="blue")}{(dest)}
""").replace("__srcs__", src_str).strip())

0 comments on commit 90b5d7a

Please sign in to comment.