mycloudctl 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +2 -0
- cli/__main__.py +138 -0
- cli/api.py +323 -0
- cli/commands/__init__.py +1 -0
- cli/commands/auth.py +234 -0
- cli/commands/batch.py +175 -0
- cli/commands/favorites.py +35 -0
- cli/commands/files.py +356 -0
- cli/commands/folders.py +200 -0
- cli/commands/notifications.py +79 -0
- cli/commands/prefs.py +72 -0
- cli/commands/profile.py +157 -0
- cli/commands/search.py +78 -0
- cli/commands/servers.py +95 -0
- cli/commands/share.py +392 -0
- cli/commands/stash.py +92 -0
- cli/commands/storage.py +88 -0
- cli/config.py +87 -0
- cli/ui/__init__.py +1 -0
- cli/ui/table.py +112 -0
- cli/ui/tree.py +56 -0
- mycloudctl-1.0.0.dist-info/METADATA +377 -0
- mycloudctl-1.0.0.dist-info/RECORD +26 -0
- mycloudctl-1.0.0.dist-info/WHEEL +5 -0
- mycloudctl-1.0.0.dist-info/entry_points.txt +2 -0
- mycloudctl-1.0.0.dist-info/top_level.txt +1 -0
cli/commands/batch.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""commands/batch.py -- batch move, delete, download, share, stash, copy-shared, link."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from cli.api import CloudClient, APIError
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Batch operations on multiple files/folders at once.")
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client() -> CloudClient:
|
|
16
|
+
return CloudClient()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("delete")
|
|
20
|
+
def batch_delete(
|
|
21
|
+
ids: List[int] = typer.Argument(..., help="File IDs to delete"),
|
|
22
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
23
|
+
):
|
|
24
|
+
"""Delete multiple files in one operation."""
|
|
25
|
+
if not yes:
|
|
26
|
+
typer.confirm(f"Delete {len(ids)} file(s)? Cannot be undone.", abort=True)
|
|
27
|
+
client = _client()
|
|
28
|
+
try:
|
|
29
|
+
result = client.post("/api/batch_delete", json={"file_ids": ids})
|
|
30
|
+
deleted = result.get("deleted", len(ids))
|
|
31
|
+
console.print(f"[green][OK][/] {deleted} file(s) deleted.")
|
|
32
|
+
except APIError as e:
|
|
33
|
+
console.print(f"[red]-[/] {e}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("move")
|
|
37
|
+
def batch_move(
|
|
38
|
+
ids: List[int] = typer.Argument(..., help="File IDs to move"),
|
|
39
|
+
dest: int = typer.Option(..., "--dest", "-d", help="Destination folder ID"),
|
|
40
|
+
):
|
|
41
|
+
"""Move multiple files to a folder."""
|
|
42
|
+
client = _client()
|
|
43
|
+
try:
|
|
44
|
+
result = client.post("/api/batch_move", json={"file_ids": ids, "folder_id": dest})
|
|
45
|
+
if result.get("success"):
|
|
46
|
+
console.print(f"[green][OK][/] {len(ids)} file(s) moved to folder {dest}.")
|
|
47
|
+
else:
|
|
48
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
49
|
+
except APIError as e:
|
|
50
|
+
console.print(f"[red]-[/] {e}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("download")
|
|
54
|
+
def batch_download(
|
|
55
|
+
ids: List[int] = typer.Argument(..., help="File IDs to download"),
|
|
56
|
+
output: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
|
|
57
|
+
):
|
|
58
|
+
"""Download multiple files as a zip archive."""
|
|
59
|
+
client = _client()
|
|
60
|
+
console.print(f"[cyan]Preparing batch download of {len(ids)} file(s)[/]")
|
|
61
|
+
try:
|
|
62
|
+
result = client.post("/api/batch_download", json={"file_ids": ids})
|
|
63
|
+
zip_token = result.get("token") or result.get("batch_token")
|
|
64
|
+
if not zip_token:
|
|
65
|
+
console.print(f"[red]-[/] {result.get('message', 'No download token returned')}")
|
|
66
|
+
return
|
|
67
|
+
url = f"/api/batch_download?token={zip_token}"
|
|
68
|
+
path = client._stream_zip(client._url(url), output, "batch_download.zip")
|
|
69
|
+
console.print(f"[green][OK][/] Saved to [bold]{path}[/bold]")
|
|
70
|
+
except APIError as e:
|
|
71
|
+
console.print(f"[red]-[/] {e}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("share")
|
|
75
|
+
def batch_share(
|
|
76
|
+
ids: List[int] = typer.Argument(..., help="File IDs to share"),
|
|
77
|
+
user: str = typer.Option(..., "--user", "-u", help="Username or email of recipient"),
|
|
78
|
+
permission: str = typer.Option("read", "--permission", "-p", help="read | write"),
|
|
79
|
+
):
|
|
80
|
+
"""Share multiple files with a user."""
|
|
81
|
+
client = _client()
|
|
82
|
+
try:
|
|
83
|
+
result = client.post("/api/batch_share", json={
|
|
84
|
+
"file_ids": ids,
|
|
85
|
+
"email": user,
|
|
86
|
+
"permission": permission,
|
|
87
|
+
})
|
|
88
|
+
if result.get("success"):
|
|
89
|
+
console.print(f"[green][OK][/] {len(ids)} file(s) shared with [bold]{user}[/bold].")
|
|
90
|
+
else:
|
|
91
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
92
|
+
except APIError as e:
|
|
93
|
+
console.print(f"[red]-[/] {e}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("stash")
|
|
97
|
+
def batch_stash(
|
|
98
|
+
ids: List[int] = typer.Argument(..., help="File IDs to stash"),
|
|
99
|
+
days: int = typer.Option(7, "--days", "-d", help="Expiry in days"),
|
|
100
|
+
):
|
|
101
|
+
"""Stash multiple files at once with an expiry period."""
|
|
102
|
+
client = _client()
|
|
103
|
+
try:
|
|
104
|
+
result = client.post("/api/batch_stash", json={"file_ids": ids, "days": days})
|
|
105
|
+
if result.get("success"):
|
|
106
|
+
console.print(f"[green][OK][/] {len(ids)} file(s) stashed for {days} day(s).")
|
|
107
|
+
else:
|
|
108
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
109
|
+
except APIError as e:
|
|
110
|
+
console.print(f"[red]-[/] {e}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("unstash")
|
|
114
|
+
def batch_unstash(
|
|
115
|
+
ids: List[int] = typer.Argument(..., help="Stashed file IDs to restore"),
|
|
116
|
+
):
|
|
117
|
+
"""Restore multiple stashed files at once."""
|
|
118
|
+
client = _client()
|
|
119
|
+
try:
|
|
120
|
+
result = client.post("/api/batch_unstash", json={"file_ids": ids})
|
|
121
|
+
if result.get("success"):
|
|
122
|
+
console.print(f"[green][OK][/] {len(ids)} file(s) unstashed.")
|
|
123
|
+
else:
|
|
124
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
125
|
+
except APIError as e:
|
|
126
|
+
console.print(f"[red]-[/] {e}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("unshare")
|
|
130
|
+
def batch_unshare(
|
|
131
|
+
ids: List[int] = typer.Argument(..., help="File IDs to un-share"),
|
|
132
|
+
):
|
|
133
|
+
"""Remove all shares from multiple files."""
|
|
134
|
+
client = _client()
|
|
135
|
+
try:
|
|
136
|
+
result = client.post("/api/batch_remove_share", json={"file_ids": ids})
|
|
137
|
+
if result.get("success"):
|
|
138
|
+
console.print(f"[green][OK][/] Shares removed from {len(ids)} file(s).")
|
|
139
|
+
else:
|
|
140
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
141
|
+
except APIError as e:
|
|
142
|
+
console.print(f"[red]-[/] {e}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command("copy-shared")
|
|
146
|
+
def batch_copy_shared(
|
|
147
|
+
ids: List[int] = typer.Argument(..., help="Shared file IDs to copy to your cloud"),
|
|
148
|
+
):
|
|
149
|
+
"""Copy multiple shared files into your own storage at once."""
|
|
150
|
+
client = _client()
|
|
151
|
+
try:
|
|
152
|
+
result = client.post("/api/batch_add_to_mycloud", json={"file_ids": ids})
|
|
153
|
+
if result.get("success"):
|
|
154
|
+
console.print(f"[green][OK][/] {len(ids)} file(s) copied to your cloud.")
|
|
155
|
+
else:
|
|
156
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
157
|
+
except APIError as e:
|
|
158
|
+
console.print(f"[red]-[/] {e}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command("link")
|
|
162
|
+
def batch_link(
|
|
163
|
+
ids: List[int] = typer.Argument(..., help="File IDs to generate a shared link for"),
|
|
164
|
+
):
|
|
165
|
+
"""Generate a single public link for multiple files."""
|
|
166
|
+
client = _client()
|
|
167
|
+
try:
|
|
168
|
+
result = client.post("/api/generate_batch_link", json={"file_ids": ids})
|
|
169
|
+
link = result.get("link") or result.get("url") or result.get("share_link")
|
|
170
|
+
if link:
|
|
171
|
+
console.print(f"[green][OK][/] Batch share link:\n[bold cyan]{link}[/bold cyan]")
|
|
172
|
+
else:
|
|
173
|
+
console.print(f"[red]-[/] {result.get('message', 'Could not generate link')}")
|
|
174
|
+
except APIError as e:
|
|
175
|
+
console.print(f"[red]-[/] {e}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""commands/favorites.py toggle favorites on files and folders."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from cli.api import CloudClient, APIError
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Favorites toggle favorite status on files and folders.")
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _client() -> CloudClient:
|
|
14
|
+
return CloudClient()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("toggle")
|
|
18
|
+
def fav_toggle(
|
|
19
|
+
item_id: int = typer.Argument(..., help="File or folder ID"),
|
|
20
|
+
folder: bool = typer.Option(False, "--folder", "-f", help="ID is a folder"),
|
|
21
|
+
):
|
|
22
|
+
"""Toggle favorite on a file or folder."""
|
|
23
|
+
client = _client()
|
|
24
|
+
try:
|
|
25
|
+
if folder:
|
|
26
|
+
result = client.post(f"/api/folders/{item_id}/favorite")
|
|
27
|
+
else:
|
|
28
|
+
result = client.post(f"/api/files/{item_id}/favorite")
|
|
29
|
+
|
|
30
|
+
state = result.get("is_favorite")
|
|
31
|
+
icon = "[red][/]" if state else "[dim][/]"
|
|
32
|
+
label = "Favorited" if state else "Un-favorited"
|
|
33
|
+
console.print(f"{icon} {label} {'folder' if folder else 'file'} {item_id}.")
|
|
34
|
+
except APIError as e:
|
|
35
|
+
console.print(f"[red]-[/] {e}")
|
cli/commands/files.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""commands/files.py -- ls, upload, download, rm, rename, mv, info commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich import print as rprint
|
|
12
|
+
|
|
13
|
+
from cli.api import CloudClient, APIError
|
|
14
|
+
from cli.ui.table import files_table, folders_table, fmt_size
|
|
15
|
+
from cli.ui.tree import build_tree
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="File operations -- list, upload, download, delete, rename, move.")
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _client() -> CloudClient:
|
|
22
|
+
return CloudClient()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------ #
|
|
26
|
+
# ls
|
|
27
|
+
# ------------------------------------------------------------------ #
|
|
28
|
+
|
|
29
|
+
@app.command("ls")
|
|
30
|
+
def ls(
|
|
31
|
+
folder_id: Optional[int] = typer.Argument(None, help="Folder ID to list (default: root)"),
|
|
32
|
+
node: str = typer.Option("central", "--node", "-n", help="Node ID or 'central'"),
|
|
33
|
+
favorites: bool = typer.Option(False, "--favorites", "-f", help="Show favorites only"),
|
|
34
|
+
tree: bool = typer.Option(False, "--tree", "-t", help="Render as a tree"),
|
|
35
|
+
):
|
|
36
|
+
"""List files and folders."""
|
|
37
|
+
client = _client()
|
|
38
|
+
params: dict = {}
|
|
39
|
+
if folder_id is not None:
|
|
40
|
+
params["folder_id"] = folder_id
|
|
41
|
+
if node != "central":
|
|
42
|
+
params["node"] = node
|
|
43
|
+
if favorites:
|
|
44
|
+
params["favorites"] = "1"
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
data = client.get("/", **params)
|
|
48
|
+
except APIError as e:
|
|
49
|
+
console.print(f"[red]-[/] {e}")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
folders = data.get("folders", [])
|
|
53
|
+
files = data.get("files", [])
|
|
54
|
+
breadcrumb = data.get("breadcrumb", [])
|
|
55
|
+
|
|
56
|
+
if breadcrumb:
|
|
57
|
+
path_str = " / ".join(b.get("name", "?") for b in breadcrumb)
|
|
58
|
+
console.print(f"\n[dim] {path_str}[/dim]")
|
|
59
|
+
|
|
60
|
+
if tree:
|
|
61
|
+
t = build_tree(
|
|
62
|
+
folders, files,
|
|
63
|
+
root_label=f" [bold cyan]{breadcrumb[-1]['name'] if breadcrumb else 'myCloud'}[/]",
|
|
64
|
+
)
|
|
65
|
+
console.print(t)
|
|
66
|
+
else:
|
|
67
|
+
if folders:
|
|
68
|
+
console.print(folders_table(folders))
|
|
69
|
+
if files:
|
|
70
|
+
console.print(files_table(files))
|
|
71
|
+
if not folders and not files:
|
|
72
|
+
console.print("[dim]This folder is empty.[/dim]")
|
|
73
|
+
|
|
74
|
+
storage = data.get("storage_used_str") or data.get("storage_used", "")
|
|
75
|
+
if storage:
|
|
76
|
+
console.print(f"\n[dim]Storage used: {storage}[/dim]")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------ #
|
|
80
|
+
# upload
|
|
81
|
+
# ------------------------------------------------------------------ #
|
|
82
|
+
|
|
83
|
+
@app.command("upload")
|
|
84
|
+
def upload(
|
|
85
|
+
files: List[Path] = typer.Argument(..., help="Files to upload (supports multiple)"),
|
|
86
|
+
folder_id: Optional[int] = typer.Option(None, "--folder", "-d", help="Destination folder ID"),
|
|
87
|
+
node: str = typer.Option("central", "--node", "-n", help="Target node"),
|
|
88
|
+
stash: bool = typer.Option(False, "--stash", help="Upload directly to stash"),
|
|
89
|
+
):
|
|
90
|
+
"""Upload one or more files to myCloud."""
|
|
91
|
+
client = _client()
|
|
92
|
+
|
|
93
|
+
for f in files:
|
|
94
|
+
if not f.exists():
|
|
95
|
+
console.print(f"[red]- File not found:[/] {f}")
|
|
96
|
+
continue
|
|
97
|
+
if not f.is_file():
|
|
98
|
+
console.print(f"[yellow] Skipping (not a file):[/] {f}")
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
console.print(f"[cyan]^[/] Uploading [bold]{f.name}[/] ({fmt_size(f.stat().st_size)})")
|
|
102
|
+
try:
|
|
103
|
+
result = client.upload_file(f, folder_id=folder_id, node=node, stash=stash)
|
|
104
|
+
if result.get("success"):
|
|
105
|
+
console.print(f"[green][OK][/] {f.name} uploaded successfully.")
|
|
106
|
+
else:
|
|
107
|
+
msg = result.get("message") or result.get("error") or "Unknown error"
|
|
108
|
+
console.print(f"[red]-[/] {f.name}: {msg}")
|
|
109
|
+
except APIError as e:
|
|
110
|
+
console.print(f"[red]-[/] {f.name}: {e}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------ #
|
|
114
|
+
# download
|
|
115
|
+
# ------------------------------------------------------------------ #
|
|
116
|
+
|
|
117
|
+
@app.command("download")
|
|
118
|
+
def download(
|
|
119
|
+
file_ids: List[int] = typer.Argument(..., help="File ID(s) to download"),
|
|
120
|
+
output: Path = typer.Option(Path("."), "--output", "-o", help="Directory to save files"),
|
|
121
|
+
folder: Optional[int] = typer.Option(None, "--folder", help="Download an entire folder (by ID)"),
|
|
122
|
+
all_files: bool = typer.Option(False, "--all", "-a", help="Download all files as a zip"),
|
|
123
|
+
node: str = typer.Option("central", "--node", "-n", help="Node context for --all"),
|
|
124
|
+
):
|
|
125
|
+
"""Download file(s) from myCloud."""
|
|
126
|
+
client = _client()
|
|
127
|
+
|
|
128
|
+
if all_files:
|
|
129
|
+
console.print("[cyan][/] Downloading all files as zip")
|
|
130
|
+
try:
|
|
131
|
+
path = client.download_all(output, node=node)
|
|
132
|
+
console.print(f"[green][/] Saved to [bold]{path}[/bold]")
|
|
133
|
+
except APIError as e:
|
|
134
|
+
console.print(f"[red]-[/] {e}")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if folder is not None:
|
|
138
|
+
console.print(f"[cyan][/] Downloading folder {folder} as zip")
|
|
139
|
+
try:
|
|
140
|
+
path = client.download_folder(folder, output)
|
|
141
|
+
console.print(f"[green][/] Saved to [bold]{path}[/bold]")
|
|
142
|
+
except APIError as e:
|
|
143
|
+
console.print(f"[red]-[/] {e}")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
for fid in file_ids:
|
|
147
|
+
console.print(f"[cyan][/] Downloading file {fid}")
|
|
148
|
+
try:
|
|
149
|
+
path = client.download_file(fid, output)
|
|
150
|
+
console.print(f"[green][/] Saved to [bold]{path}[/bold]")
|
|
151
|
+
except APIError as e:
|
|
152
|
+
console.print(f"[red]-[/] File {fid}: {e}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------------ #
|
|
156
|
+
# rm
|
|
157
|
+
# ------------------------------------------------------------------ #
|
|
158
|
+
|
|
159
|
+
@app.command("rm")
|
|
160
|
+
def rm(
|
|
161
|
+
ids: List[int] = typer.Argument(..., help="File or folder ID(s) to delete"),
|
|
162
|
+
folder: bool = typer.Option(False, "--folder", "-f", help="IDs are folder IDs"),
|
|
163
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
164
|
+
):
|
|
165
|
+
"""Delete files or folders."""
|
|
166
|
+
client = _client()
|
|
167
|
+
kind = "folder" if folder else "file"
|
|
168
|
+
|
|
169
|
+
if not yes:
|
|
170
|
+
confirm = typer.confirm(
|
|
171
|
+
f"Delete {len(ids)} {kind}(s)? This cannot be undone.", default=False
|
|
172
|
+
)
|
|
173
|
+
if not confirm:
|
|
174
|
+
raise typer.Abort()
|
|
175
|
+
|
|
176
|
+
for item_id in ids:
|
|
177
|
+
try:
|
|
178
|
+
if folder:
|
|
179
|
+
result = client.post(f"/delete_folder/{item_id}")
|
|
180
|
+
else:
|
|
181
|
+
result = client.post(f"/delete/{item_id}")
|
|
182
|
+
if result.get("success"):
|
|
183
|
+
console.print(f"[green][/] {kind.capitalize()} {item_id} deleted.")
|
|
184
|
+
else:
|
|
185
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
186
|
+
except APIError as e:
|
|
187
|
+
console.print(f"[red]-[/] ID {item_id}: {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ------------------------------------------------------------------ #
|
|
191
|
+
# rename
|
|
192
|
+
# ------------------------------------------------------------------ #
|
|
193
|
+
|
|
194
|
+
@app.command("rename")
|
|
195
|
+
def rename(
|
|
196
|
+
item_id: int = typer.Argument(..., help="File or folder ID"),
|
|
197
|
+
new_name: str = typer.Argument(..., help="New name"),
|
|
198
|
+
folder: bool = typer.Option(False, "--folder", "-f", help="ID is a folder"),
|
|
199
|
+
):
|
|
200
|
+
"""Rename a file or folder."""
|
|
201
|
+
client = _client()
|
|
202
|
+
try:
|
|
203
|
+
if folder:
|
|
204
|
+
result = client.post(f"/rename_folder/{item_id}", json={"name": new_name})
|
|
205
|
+
else:
|
|
206
|
+
result = client.post(f"/rename/{item_id}", json={"filename": new_name})
|
|
207
|
+
if result.get("success"):
|
|
208
|
+
console.print(f"[green][/] Renamed to [bold]{new_name}[/bold].")
|
|
209
|
+
else:
|
|
210
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
211
|
+
except APIError as e:
|
|
212
|
+
console.print(f"[red]-[/] {e}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------ #
|
|
216
|
+
# mv
|
|
217
|
+
# ------------------------------------------------------------------ #
|
|
218
|
+
|
|
219
|
+
@app.command("mv")
|
|
220
|
+
def mv(
|
|
221
|
+
item_id: int = typer.Argument(..., help="File or folder ID to move"),
|
|
222
|
+
dest_folder_id: int = typer.Argument(..., help="Destination folder ID"),
|
|
223
|
+
folder: bool = typer.Option(False, "--folder", "-f", help="ID is a folder"),
|
|
224
|
+
):
|
|
225
|
+
"""Move a file or folder to a different folder."""
|
|
226
|
+
client = _client()
|
|
227
|
+
try:
|
|
228
|
+
|
|
229
|
if folder:
|
|
230
|
+
result = client.post("/api/move_folder", json={
|
|
231
|
+
"folder_id": item_id,
|
|
232
|
+
"new_parent_id": dest_folder_id,
|
|
233
|
+
})
|
|
234
|
+
else:
|
|
235
|
+
result = client.post("/api/move_file", json={
|
|
236
|
+
"file_id": item_id,
|
|
237
|
+
"destination_folder_id": dest_folder_id,
|
|
238
|
+
})
|
|
239
|
+
if result.get("success"):
|
|
240
|
+
console.print(f"[green][OK][/] Moved to folder {dest_folder_id}.")
|
|
241
|
+
else:
|
|
242
|
+
console.print(f"[red]-[/] {result.get('message', 'Error')}")
|
|
243
|
+
except APIError as e:
|
|
244
|
+
console.print(f"[red]-[/] {e}")
|
|
245
|
+
# ------------------------------------------------------------------ #
|
|
246
|
+
# info
|
|
247
|
+
# ------------------------------------------------------------------ #
|
|
248
|
+
|
|
249
|
+
@app.command("info")
|
|
250
|
+
def info(
|
|
251
|
+
file_id: int = typer.Argument(..., help="File ID"),
|
|
252
|
+
):
|
|
253
|
+
"""Show detailed metadata for a file."""
|
|
254
|
+
client = _client()
|
|
255
|
+
try:
|
|
256
|
+
data = client.get(f"/file_details/{file_id}")
|
|
257
|
+
except APIError as e:
|
|
258
|
+
console.print(f"[red]-[/] {e}")
|
|
259
|
+
raise typer.Exit(1)
|
|
260
|
+
|
|
261
|
+
f = data.get("file") or data
|
|
262
|
+
stashed = "[yellow]Yes[/]" if f.get("is_stashed") else "[dim]No[/]"
|
|
263
|
+
favorited = "[red]*[/]" if f.get("is_favorite") else "[dim]No[/]"
|
|
264
|
+
shared = "[cyan]Yes[/]" if f.get("is_shared") else "[dim]No[/]"
|
|
265
|
+
storage = f.get("storage_mode") or "central"
|
|
266
|
+
folder = f.get("folder_name") or (f"ID {f.get('folder_id')}" if f.get("folder_id") else "root")
|
|
267
|
+
|
|
268
|
+
console.print(Panel(
|
|
269
|
+
f"[bold]{f.get('original_filename', '?')}[/bold]\n\n"
|
|
270
|
+
f"[dim]ID:[/dim] {f.get('id', '?')}\n"
|
|
271
|
+
f"[dim]Size:[/dim] {fmt_size(f.get('file_size') or f.get('size'))}\n"
|
|
272
|
+
f"[dim]Uploaded:[/dim] {str(f.get('upload_date') or f.get('created_at', '?'))[:19]}\n"
|
|
273
|
+
|
|
274
|
+
f"[dim]Uploaded:[/dim] {str(f.get('upload_date') or f.get('created_at', '?'))[:19]}\n"
|
|
275
|
+
f"[dim]Folder:[/dim] {folder}\n"
|
|
276
|
+
f"[dim]Storage node:[/dim] {storage}\n"
|
|
277
|
+
f"[dim]Stashed:[/dim] {stashed}\n"
|
|
278
|
+
f"[dim]Favorite:[/dim] {favorited}\n"
|
|
279
|
+
f"[dim]Shared:[/dim] {shared}",
|
|
280
|
+
title="[bold]File Info[/bold]",
|
|
281
|
+
border_style="cyan",
|
|
282
|
+
))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.command("favorite")
|
|
286
|
+
def favorite(
|
|
287
|
+
file_id: int = typer.Argument(..., help="File ID"),
|
|
288
|
+
):
|
|
289
|
+
"""Toggle favorite status for a file."""
|
|
290
|
+
client = _client()
|
|
291
|
+
try:
|
|
292
|
+
result = client.post(f"/api/files/{file_id}/favorite")
|
|
293
|
+
state = result.get("is_favorite")
|
|
294
|
+
icon = "[red][/]" if state else "[dim][/]"
|
|
295
|
+
label = "Favorited" if state else "Un-favorited"
|
|
296
|
+
console.print(f"{icon} {label} file {file_id}.")
|
|
297
|
+
except APIError as e:
|
|
298
|
+
console.print(f"[red][FAIL][/] {e}")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@app.command("cat")
|
|
302
|
+
def cat(
|
|
303
|
+
file_id: int = typer.Argument(..., help="File ID to print"),
|
|
304
|
+
):
|
|
305
|
+
"""Print the raw contents of a file to the console."""
|
|
306
|
+
client = _client()
|
|
307
|
+
try:
|
|
308
|
+
raw_text = client.get_raw(f"/api/raw/{file_id}")
|
|
309
|
+
console.print(raw_text, end="")
|
|
310
|
+
except APIError as e:
|
|
311
|
+
console.print(f"[red]-[/] {e}")
|
|
312
|
+
raise typer.Exit(1)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@app.command("download-all")
|
|
316
|
+
def download_all(
|
|
317
|
+
dest: Path = typer.Option(Path.cwd(), "--dest", "-d", help="Destination folder"),
|
|
318
|
+
node: str = typer.Option("central", "--node", "-n", help="Storage node to filter by (or 'all')"),
|
|
319
|
+
):
|
|
320
|
+
"""Download all files as a ZIP archive."""
|
|
321
|
+
client = _client()
|
|
322
|
+
if not dest.exists():
|
|
323
|
+
dest.mkdir(parents=True)
|
|
324
|
+
if not dest.is_dir():
|
|
325
|
+
console.print(f"[red]Destination is not a directory:[/] {dest}")
|
|
326
|
+
raise typer.Exit(1)
|
|
327
|
+
|
|
328
|
+
console.print("[cyan]Preparing download for all files...[/cyan]")
|
|
329
|
+
try:
|
|
330
|
+
path = client.download_all(dest_dir=dest, node=node)
|
|
331
|
+
console.print(f"[green][OK][/] Saved to {path.name}")
|
|
332
|
+
except APIError as e:
|
|
333
|
+
console.print(f"[red]-[/] Download failed: {e}")
|
|
334
|
+
raise typer.Exit(1)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@app.command("rm-all")
|
|
338
|
+
def remove_all(
|
|
339
|
+
confirm: bool = typer.Option(False, "--yes", "-y", help="Confirm deletion without prompting"),
|
|
340
|
+
node: str = typer.Option("all", "--node", "-n", help="Storage node to wipe (or 'all')"),
|
|
341
|
+
):
|
|
342
|
+
"""Delete all files in your account."""
|
|
343
|
+
if not confirm:
|
|
344
|
+
sure = Prompt.ask(f"[bold red]Are you absolutely sure you want to delete ALL files on node '{node}'? This cannot be undone[/bold red] (y/N)")
|
|
345
|
+
if sure.lower() not in ('y', 'yes'):
|
|
346
|
+
console.print("Bulk deletion aborted.")
|
|
347
|
+
raise typer.Exit()
|
|
348
|
+
|
|
349
|
+
client = _client()
|
|
350
|
+
try:
|
|
351
|
+
result = client.post("/delete_all_files", json={"target_node": node})
|
|
352
|
+
if result.get("success"):
|
|
353
|
+
console.print(f"[green][OK][/] {result.get('message', 'All files deleted.')}")
|
|
354
|
+
else:
|
|
355
|
+
console.print(f"[red]-[/] {result.get('error', 'Error deleting files.')}")
|
|
356
|
+
except APIError as e:
|
|
357
|
+
console.print(f"[red]-[/] {e}")
|