1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
"""
`mkdir`: make folders in a group, as the Files tab's "New folder" does.
Any member may, where the root accepts uploads: organising where files go is
not privileged on the node (removing a folder is — see `rm -r`). The parent
must exist, unless --parents makes the missing ones too; with it a folder
already there is not an error. Not -p, which is the passphrase everywhere in
this program. Names follow the node's rule for uploads, checked here
first so nothing is sent that it would refuse.
"""
import json
from ..client.groups import my_groups
from ..client.names import same_name, upload_name_problem
from ..client.paths import handle, parse, resolve_group
from ..client.pool import Pool
from ..client.transfers import TransferError, create_dir
from ..client.tree import Tree
from ..errors import NOT_FOUND, PARTIAL, REFUSED, USAGE, CliError
from ..output import err, safe, sentence
from .context import Context
HELP = "make folders in a group"
def add_parser(sub, parent) -> None:
p = sub.add_parser("mkdir", parents=[parent], help=HELP, description=sentence(HELP)
+ ", where its root accepts uploads.")
p.add_argument("paths", nargs="+", metavar="PATH",
help="group[@owner]/root/.../new-folder")
p.add_argument("--parents", action="store_true",
help="make missing parent folders too; a folder already there is "
"not an error (not -p: that is --password)")
async def run(ctx: Context) -> int:
await ctx.hub.check_version()
account = ctx.account()
pool = Pool(account)
try:
return await _run(ctx, account, pool)
finally:
await pool.close()
await account.close()
async def _run(ctx: Context, account, pool: Pool) -> int:
a = ctx.args
groups = await my_groups(account)
plans = [] # (session, group, [(parent, name), ...])
for text in a.paths:
remote = parse(text)
if remote is None or len(remote.parts) < 2:
raise CliError(USAGE, "Name the folder to make inside a root: "
"group[@owner]/root/folder")
group = resolve_group(groups, remote.group)
session = await pool.session(group)
tree = Tree.from_index(await session.index())
shown = f"{handle(group)}/{remote.inner}"
root = tree.roots.get(remote.parts[0])
if root is None:
raise CliError(NOT_FOUND, f"No root {safe(remote.parts[0])} in "
f"{safe(handle(group))}.")
if not root.get("available", True):
raise CliError(REFUSED, f"{safe(shown)}: its drive is disconnected from the node.")
if not root.get("writable"):
raise CliError(REFUSED, f"{safe(shown)}: the root is read-only — its operator "
"does not accept uploads there.")
steps = []
for depth in range(2, len(remote.parts) + 1):
parent, name = "/".join(remote.parts[:depth - 1]), remote.parts[depth - 1]
if (why := upload_name_problem(name, is_dir=True)):
raise CliError(REFUSED, f"{safe(name)}: {why}")
path = f"{parent}/{name}"
if tree.is_dir(path):
continue
# Folders are told apart without case, as the node does.
clash = next((i for i in tree.children(parent) if same_name(i.name, name)),
None) if tree.is_dir(parent) else None
if clash is not None:
raise CliError(REFUSED, f"{safe(handle(group))}/{safe(clash.path)} is already "
"there" + ("" if clash.is_dir else " (a file)") + ".")
if depth < len(remote.parts) and not a.parents:
raise CliError(NOT_FOUND, f"No folder {safe(handle(group))}/{safe(path)} "
"(--parents makes it too).")
steps.append((parent, name))
if not steps and not a.parents:
raise CliError(REFUSED, f"{safe(shown)} is already there.")
plans.append((session, group, steps))
made, failed = [], []
for session, group, steps in plans:
for parent, name in steps:
try:
path = await create_dir(session, parent, name)
except TransferError as e:
failed.append(f"{handle(group)}/{parent}/{name}: {e.message}")
break # the folders below it cannot follow
made.append(f"{handle(group)}/{path}")
if a.json:
print(json.dumps({"made": made, "failed": failed}, ensure_ascii=False))
elif not a.quiet:
for m in made:
err(f"Made {safe(m)}/")
for f in failed:
err(f"meshbay-cli: {safe(f)}")
return PARTIAL if failed else 0
|