aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/commands/rm.py
blob: 6f5e86993df6ec3fe672dc771784f38bf3e4ef9c (plain) (blame)
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
109
110
111
"""
`rm`: delete files from groups. Asked first; `--yes` answers for a script.

Paths and patterns are resolved against each group's index, as `get` resolves
them, and the whole run is checked before anything is asked or sent: a file
this account may not delete — not uploaded by it, and this machine's key not
the node's operator — refuses the run (exit 8), as a folder does. The node
checks again on every file; one it refuses is reported and the others go on
(exit 4).
"""

import json

from ..client.groups import my_groups
from ..client.paths import handle, parse, resolve_group
from ..client.pool import Pool
from ..client.remove import delete_file, may_delete
from ..client.tree import Tree
from ..errors import NODE, NOT_FOUND, PARTIAL, REFUSED, USAGE, CliError
from ..output import confirm, err, safe, sentence
from .context import Context

HELP = "delete files you uploaded (or any, from the node's operator key)"

# How many names the confirmation shows before it counts the rest.
SHOWN = 10


def add_parser(sub, parent) -> None:
    p = sub.add_parser("rm", parents=[parent], help=HELP, description=sentence(HELP)
                       + ". Wildcards (* ? [...]) are matched against the group, so "
                       "quote them. Being the group's owner is not enough: the node "
                       "lets its operator's key, and the uploader's devices, delete.")
    p.add_argument("paths", nargs="+", metavar="PATH", help="group[@owner]/root/.../file")
    p.add_argument("-y", "--yes", action="store_true", help="do not ask for confirmation")


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)
    trees: dict[str, Tree] = {}
    targets = []           # (session, entry, shown)
    missing, folders, not_yours = [], [], []
    for text in a.paths:
        remote = parse(text)
        if remote is None or not remote.parts:
            raise CliError(USAGE, "Name the files to delete, inside a group.")
        group = resolve_group(groups, remote.group)
        session = await pool.session(group)
        if group["id"] not in trees:
            trees[group["id"]] = Tree.from_index(await session.index())
        tree = trees[group["id"]]
        found = tree.expand(remote.parts)
        if not found and tree.file(remote.inner):
            found = [remote.inner]              # a literal name with brackets in it
        if not found:
            missing.append(text)
        for path in found:
            shown = f"{handle(group)}/{path}"
            entry = tree.file(path)
            if entry is None:
                folders.append(shown)
            elif not may_delete(session, entry, account.user_id):
                not_yours.append(shown)
            elif all(e["id"] != entry["id"] for _, e, _ in targets):
                targets.append((session, entry, shown))
    if missing:
        raise CliError(NOT_FOUND, "Nothing in your groups matches: "
                                  + ", ".join(safe(m) for m in missing))
    if folders:
        raise CliError(USAGE, "rm deletes files; these are folders: "
                              + ", ".join(safe(f) for f in folders))
    if not_yours:
        raise CliError(REFUSED, "Not deleted — you did not upload these, and this machine's "
                                "key is not the node's operator:\n  "
                                + "\n  ".join(safe(n) for n in not_yours))

    names = [shown for _, _, shown in targets]
    listed = "\n  ".join(safe(n) for n in names[:SHOWN])
    more = f"\n  ... and {len(names) - SHOWN} more" if len(names) > SHOWN else ""
    confirm(f"Delete {len(names)} file{'s' if len(names) != 1 else ''} for every member "
            f"of the group?\n  {listed}{more}\n", a.yes)

    deleted, failed = [], []
    for session, entry, shown in targets:
        try:
            await delete_file(session, entry["id"])
            deleted.append(shown)
        except CliError as e:
            if e.code not in (NODE, REFUSED):
                raise
            failed.append(f"{shown}: {e.message}")
    if a.json:
        print(json.dumps({"deleted": deleted, "failed": failed}, ensure_ascii=False))
    elif not a.quiet:
        n = len(deleted)
        err(f"Deleted {n} file{'s' if n != 1 else ''}.")
    for f in failed:
        err(f"meshbay-cli: {safe(f)}")
    return PARTIAL if failed else 0