summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py92
1 files changed, 92 insertions, 0 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py b/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py
new file mode 100644
index 0000000..070e686
--- /dev/null
+++ b/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py
@@ -0,0 +1,92 @@
+"""
+What an upload will do, decided before anything is sent — shared by `put` and
+the interactive mode, so both refuse the same names and the same collisions.
+
+A name already in the destination folder, compared without case as the node and
+the Files tab compare it, or a name the node does not accept (`names.py`), is
+found here; what to do about it (refuse the run, skip it, ask) is the caller's.
+"""
+
+import os
+import stat
+from dataclasses import dataclass
+from pathlib import Path
+
+from meshbay_common.paths import fold
+
+from ..errors import USAGE, CliError
+from ..output import safe
+from .names import same_name, upload_name_problem
+from .tree import Tree
+
+
+@dataclass
+class UploadFile:
+ local: Path
+ rel: list[str] # remote parts below the destination folder
+ size: int
+
+
+def plan_upload(sources: list[str], recursive: bool):
+ """Folders to create (parents first) and files to send, as remote parts."""
+ dirs: list[list[str]] = []
+ files: list[UploadFile] = []
+ problems: list[str] = []
+ seen: dict[str, str] = {}
+ for src in sources:
+ path = Path(src).expanduser()
+ try:
+ st = path.stat()
+ except OSError as e:
+ problems.append(f"{src}: {e.strerror}")
+ continue
+ top = path.resolve().name if src.rstrip("/\\") in (".", "..") else path.name
+ key = fold(top)
+ if key in seen:
+ problems.append(f"{src} and {seen[key]} would have the same name there")
+ continue
+ seen[key] = src
+ if stat.S_ISREG(st.st_mode):
+ files.append(UploadFile(path, [top], st.st_size))
+ elif stat.S_ISDIR(st.st_mode):
+ if not recursive:
+ raise CliError(USAGE, f"{safe(src)} is a folder; use -R.")
+ for here, subdirs, names in os.walk(path):
+ rel = [top] + list(Path(here).relative_to(path).parts)
+ dirs.append(rel)
+ for d in list(subdirs):
+ if (Path(here) / d).is_symlink():
+ subdirs.remove(d)
+ problems.append(f"{Path(here) / d} is a link to a folder; not "
+ "followed")
+ for n in names:
+ p = Path(here) / n
+ try:
+ st_n = p.stat()
+ except OSError as e:
+ problems.append(f"{p}: {e.strerror}")
+ continue
+ if stat.S_ISREG(st_n.st_mode):
+ files.append(UploadFile(p, rel + [n], st_n.st_size))
+ subdirs.sort()
+ else:
+ problems.append(f"{src} is neither a file nor a folder")
+ return dirs, files, problems
+
+
+def taken_names(tree: Tree, folder: str, names: list[str]) -> set[str]:
+ """The names among `names` already in `folder`, compared without case."""
+ there = [i.name for i in tree.children(folder, include_unavailable=True)]
+ return {n for n in names if any(same_name(n, t) for t in there)}
+
+
+def refused_names(file_names: list[str], dirs: list[list[str]]) -> list[str]:
+ """Names the node would refuse — the client's copy of its rule."""
+ out = []
+ for name in file_names:
+ if (why := upload_name_problem(name)):
+ out.append(f"{name}: {why}")
+ for d in dirs:
+ if (why := upload_name_problem(d[-1], is_dir=True)):
+ out.append(f"{'/'.join(d)}/: {why}")
+ return out