aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/client')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/client/store.py5
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/client/transfers.py9
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py33
3 files changed, 37 insertions, 10 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/client/store.py b/packages/meshbay-cli/src/meshbay_cli/client/store.py
index c48fe89..00ff9f3 100644
--- a/packages/meshbay-cli/src/meshbay_cli/client/store.py
+++ b/packages/meshbay-cli/src/meshbay_cli/client/store.py
@@ -63,7 +63,10 @@ def data_dir() -> Path:
def _write_private(path: Path, data: bytes) -> None:
"""Atomically, readable by this user only."""
tmp = path.with_name(path.name + ".tmp")
- fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+ # O_BINARY: without it Windows opens in text mode and writes every 0x0A of
+ # the ciphertext as CR LF, and the secrets no longer open.
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_BINARY", 0)
+ fd = os.open(tmp, flags, 0o600)
try:
os.write(fd, data)
os.fsync(fd)
diff --git a/packages/meshbay-cli/src/meshbay_cli/client/transfers.py b/packages/meshbay-cli/src/meshbay_cli/client/transfers.py
index 2cbf4cd..177c78b 100644
--- a/packages/meshbay-cli/src/meshbay_cli/client/transfers.py
+++ b/packages/meshbay-cli/src/meshbay_cli/client/transfers.py
@@ -128,7 +128,8 @@ class Resume:
sidecar = target.with_name(target.name + PART_SUFFIX + ".json")
done = 0
try:
- meta = json.loads(sidecar.read_text(encoding="utf-8"))
+ with open(long_path(sidecar), encoding="utf-8") as f:
+ meta = json.load(f)
if meta.get("id") == entry["id"] and meta.get("size") == entry["size"]:
# Every chunk but the last is whole, so the complete ones are
# what the part's length says.
@@ -198,8 +199,8 @@ def _prepare_part(resume: Resume, entry: dict, start: int) -> None:
pass
else:
os.truncate(part, start * DOWNLOAD_CHUNK)
- resume.sidecar.write_text(json.dumps({"id": entry["id"], "size": entry["size"]}),
- encoding="utf-8")
+ with open(long_path(resume.sidecar), "w", encoding="utf-8") as f:
+ json.dump({"id": entry["id"], "size": entry["size"]}, f)
def _publish(resume: Resume, target: Path, overwrite: bool) -> None:
@@ -220,7 +221,7 @@ def _publish(resume: Resume, target: Path, overwrite: bool) -> None:
raise TransferError(f"{target} already exists.") from None
os.replace(part, dest)
with contextlib.suppress(OSError):
- resume.sidecar.unlink()
+ os.unlink(long_path(resume.sidecar))
if sys.platform == "win32":
mark_of_the_web(target)
diff --git a/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py b/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py
index 070e686..b4ec781 100644
--- a/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py
+++ b/packages/meshbay-cli/src/meshbay_cli/client/upload_plan.py
@@ -12,7 +12,7 @@ import stat
from dataclasses import dataclass
from pathlib import Path
-from meshbay_common.paths import fold
+from meshbay_common.paths import fold, long_path
from ..errors import USAGE, CliError
from ..output import safe
@@ -36,7 +36,7 @@ def plan_upload(sources: list[str], recursive: bool):
for src in sources:
path = Path(src).expanduser()
try:
- st = path.stat()
+ st = os.stat(long_path(path))
except OSError as e:
problems.append(f"{src}: {e.strerror}")
continue
@@ -51,18 +51,18 @@ def plan_upload(sources: list[str], recursive: bool):
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):
+ for here, subdirs, names in _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():
+ if os.path.islink(long_path(Path(here) / d)):
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()
+ st_n = os.stat(long_path(p))
except OSError as e:
problems.append(f"{p}: {e.strerror}")
continue
@@ -74,6 +74,29 @@ def plan_upload(sources: list[str], recursive: bool):
return dirs, files, problems
+def _walk(top: Path):
+ """`os.walk` for a tree that may go past Windows' MAX_PATH: each folder is
+ listed under its long form (`long_path`, which only prefixes a long path, so
+ a short top alone would not do) and the paths given back stay plain."""
+ stack = [top]
+ while stack:
+ here = stack.pop()
+ try:
+ with os.scandir(long_path(here)) as it:
+ entries = list(it)
+ except OSError:
+ continue
+ subdirs, names = [], []
+ for e in entries:
+ try:
+ is_dir = e.is_dir()
+ except OSError:
+ is_dir = False
+ (subdirs if is_dir else names).append(e.name)
+ yield here, subdirs, names # the caller prunes and orders subdirs
+ stack.extend(here / d for d in reversed(subdirs))
+
+
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)]