文件内容
weiyun_skills/main.py
"""Weiyun Skills CLI - command line interface for Weiyun management."""
import sys
import json
import argparse
from weiyun_skills.client import WeiyunClient
from weiyun_skills.utils import format_size
try:
from tabulate import tabulate
except ImportError:
tabulate = None
def _confirm(prompt: str, assume_yes: bool = False) -> bool:
"""Ask the user to confirm a destructive/irreversible action.
Returns True only when the user explicitly types 'y' / 'yes' (case
insensitive). If stdin is not a TTY (e.g. running inside an automated
agent pipeline) the prompt is refused unless ``assume_yes`` is set via
an explicit ``--yes`` flag, so an agent cannot silently approve
destructive operations on the user's behalf.
"""
if assume_yes:
return True
# If we cannot interactively prompt the user, fail closed.
if not sys.stdin or not sys.stdin.isatty():
print(
"[!] Refusing to run a destructive action without an interactive "
"confirmation. Re-run in a terminal, or pass --yes if you have "
"manually verified the operation."
)
return False
try:
answer = input(f"{prompt} [y/N]: ").strip().lower()
except EOFError:
return False
return answer in ("y", "yes")
def _print_json(data: dict) -> None:
"""Pretty print JSON data."""
print(json.dumps(data, ensure_ascii=False, indent=2))
def _print_table(headers: list, rows: list) -> None:
"""Print data as a formatted table."""
if tabulate:
print(tabulate(rows, headers=headers, tablefmt="grid"))
else:
# Fallback: simple table output
print("\t".join(headers))
print("-" * (len(headers) * 20))
for row in rows:
print("\t".join(str(c) for c in row))
def cmd_list(client: WeiyunClient, args) -> None:
"""Handle 'list' command."""
result = client.list_files(
remote_path=args.path,
sort_by=getattr(args, "sort", "name"),
sort_order=getattr(args, "order", "asc"),
)
if not result["success"]:
print(f"[ERROR] {result['message']}")
return
files = result["data"]["files"]
if not files:
print(f"(empty directory: {args.path})")
return
headers = ["Type", "Name", "Size", "Updated"]
rows = []
for f in files:
icon = "📁" if f["type"] == "folder" else "📄"
rows.append([icon, f["name"], f["size_str"], f["updated_at"]])
_print_table(headers, rows)
print(f"\nTotal: {result['data']['total']} items")
def cmd_upload(client: WeiyunClient, args) -> None:
"""Handle 'upload' command."""
overwrite = getattr(args, "overwrite", False)
assume_yes = getattr(args, "yes", False)
# Uploading mutates the user's Weiyun account. When --overwrite is on,
# an existing remote file will be REPLACED, which is effectively a
# silent destructive action, so we surface that explicitly before
# asking for confirmation.
if overwrite:
print(
f"[!] --overwrite is enabled. If a file already exists at "
f"{args.remote}, its contents will be REPLACED."
)
if not _confirm(
f"Upload '{args.local}' to Weiyun at '{args.remote}'"
f"{' (overwrite allowed)' if overwrite else ''}?",
assume_yes=assume_yes,
):
print("[-] Upload cancelled.")
return
print(f"[*] Uploading {args.local} -> {args.remote}")
result = client.upload_file(
args.local, args.remote,
overwrite=overwrite,
)
if result["success"]:
d = result["data"]
print(f"[✓] Uploaded: {d['name']} ({format_size(d['size'])})")
print(f" Path: {d['remote_path']}")
print(f" MD5: {d['md5']}")
else:
print(f"[✗] Upload failed: {result['message']}")
def cmd_upload_folder(client: WeiyunClient, args) -> None:
"""Handle 'upload-folder' command."""
remote = getattr(args, "remote", "/")
overwrite = getattr(args, "overwrite", False)
assume_yes = getattr(args, "yes", False)
# Bulk folder uploads can create and/or overwrite many files at once,
# so we gate the whole operation behind the same confirmation prompt
# the rest of the mutating commands use.
if overwrite:
print(
f"[!] --overwrite is enabled. Existing files under "
f"'{remote}' may be REPLACED in bulk."
)
if not _confirm(
f"Upload folder '{args.local}' to Weiyun at '{remote}'"
f"{' (overwrite allowed)' if overwrite else ''}?",
assume_yes=assume_yes,
):
print("[-] Upload cancelled.")
return
print(f"[*] Uploading folder '{args.local}' -> Weiyun:{remote}")
result = client.upload_folder(
args.local, remote,
overwrite=overwrite,
)
if result["success"]:
d = result["data"]
print(f"[✓] Uploaded folder: {d['folder_name']}")
print(f" Files: {d['uploaded_count']} uploaded, "
f"{d['failed_count']} failed")
print(f" Size: {d['total_size_str']}")
print(f" Time: {d['elapsed']}s")
if d["uploaded_files"]:
print("\n Uploaded files:")
for f in d["uploaded_files"]:
instant = " ⚡" if f.get("instant_upload") else ""
print(f" 📄 {f['name']} ({f['size_str']}){instant}")
if d["failed_files"]:
print("\n Failed files:")
for f in d["failed_files"]:
print(f" ❌ {f['name']}: {f['error']}")
else:
print(f"[✗] Upload failed: {result['message']}")
def cmd_download(client: WeiyunClient, args) -> None:
"""Handle 'download' command."""
print(f"[*] Downloading {args.remote} -> {args.local}")
result = client.download_file(
args.remote, args.local,
overwrite=getattr(args, "overwrite", False)
)
if result["success"]:
d = result["data"]
print(f"[✓] Downloaded: {d['local_path']} ({format_size(d['size'])})")
print(f" MD5: {d['md5']}")
print(f" Time: {d['elapsed']}s")
else:
print(f"[✗] Download failed: {result['message']}")
def cmd_download_folder(client: WeiyunClient, args) -> None:
"""Handle 'download-folder' command."""
as_zip = getattr(args, "zip", False)
mode = "zip" if as_zip else "recursive"
print(f"[*] Downloading folder '{args.folder}' -> {args.local} (mode: {mode})")
result = client.download_folder(
args.folder, args.local,
overwrite=getattr(args, "overwrite", False),
as_zip=as_zip,
)
if result["success"]:
d = result["data"]
if as_zip:
print(f"[✓] Downloaded zip: {d['local_path']}")
print(f" Size: {d.get('size_str', format_size(d.get('size', 0)))}")
print(f" MD5: {d['md5']}")
print(f" Time: {d['elapsed']}s")
else:
print(f"[✓] Downloaded folder: {d['local_path']}")
print(f" Files: {d['downloaded_count']} downloaded, "
f"{d['failed_count']} failed")
print(f" Size: {d['total_size_str']}")
print(f" Time: {d['elapsed']}s")
if d["downloaded_files"]:
print("\n Downloaded files:")
for f in d["downloaded_files"]:
print(f" 📄 {f['name']} ({f['size_str']})")
if d["failed_files"]:
print("\n Failed files:")
for f in d["failed_files"]:
print(f" ❌ {f['name']}: {f['error']}")
else:
print(f"[✗] Download failed: {result['message']}")
def cmd_delete(client: WeiyunClient, args) -> None:
"""Handle 'delete' command."""
permanent = getattr(args, "permanent", False)
assume_yes = getattr(args, "yes", False)
action = "Permanently deleting" if permanent else "Deleting"
# Require explicit user confirmation before any remote deletion.
# Permanent delete skips the recycle bin and is effectively irreversible,
# so it uses a stricter prompt that spells out the consequences.
if permanent:
print(
f"[!] PERMANENT DELETE requested for: {args.path}\n"
f" This will bypass the recycle bin and CANNOT be undone."
)
if not _confirm(
f"Permanently delete '{args.path}'? This cannot be undone.",
assume_yes=assume_yes,
):
print("[-] Delete cancelled.")
return
else:
if not _confirm(
f"Move '{args.path}' to the Weiyun recycle bin?",
assume_yes=assume_yes,
):
print("[-] Delete cancelled.")
return
print(f"[*] {action} {args.path}")
result = client.delete_file(args.path, permanent=permanent)
if result["success"]:
d = result["data"]
msg = "permanently deleted" if d["is_permanent"] else "moved to recycle bin"
print(f"[✓] {d['deleted_path']} {msg}")
else:
print(f"[✗] Delete failed: {result['message']}")
def cmd_move(client: WeiyunClient, args) -> None:
"""Handle 'move' command."""
if not _confirm(
f"Move '{args.source}' to '{args.target}' on Weiyun?",
assume_yes=getattr(args, "yes", False),
):
print("[-] Move cancelled.")
return
print(f"[*] Moving {args.source} -> {args.target}")
result = client.move_file(args.source, args.target)
if result["success"]:
d = result["data"]
print(f"[✓] Moved to: {d['target_path']}")
else:
print(f"[✗] Move failed: {result['message']}")
def cmd_copy(client: WeiyunClient, args) -> None:
"""Handle 'copy' command."""
if not _confirm(
f"Copy '{args.source}' to '{args.target}' on Weiyun?",
assume_yes=getattr(args, "yes", False),
):
print("[-] Copy cancelled.")
return
print(f"[*] Copying {args.source} -> {args.target}")
result = client.copy_file(args.source, args.target)
if result["success"]:
d = result["data"]
print(f"[✓] Copied to: {d['target_path']}")
else:
print(f"[✗] Copy failed: {result['message']}")
def cmd_rename(client: WeiyunClient, args) -> None:
"""Handle 'rename' command."""
if not _confirm(
f"Rename '{args.path}' to '{args.name}' on Weiyun?",
assume_yes=getattr(args, "yes", False),
):
print("[-] Rename cancelled.")
return
print(f"[*] Renaming {args.path} -> {args.name}")
result = client.rename_file(args.path, args.name)
if result["success"]:
d = result["data"]
print(f"[✓] Renamed: {d['old_path']} -> {d['new_path']}")
else:
print(f"[✗] Rename failed: {result['message']}")
def cmd_mkdir(client: WeiyunClient, args) -> None:
"""Handle 'mkdir' command."""
# mkdir is a mutating operation (it changes the remote directory tree),
# so it goes through the same confirmation gate as upload/delete/etc.
if not _confirm(
f"Create folder '{args.path}' on Weiyun?",
assume_yes=getattr(args, "yes", False),
):
print("[-] Create cancelled.")
return
print(f"[*] Creating folder: {args.path}")
result = client.create_folder(args.path)
if result["success"]:
print(f"[✓] Created: {result['data']['path']}")
else:
print(f"[✗] Create failed: {result['message']}")
def cmd_search(client: WeiyunClient, args) -> None:
"""Handle 'search' command."""
result = client.search_files(
keyword=args.keyword,
file_type=getattr(args, "type", "all"),
)
if not result["success"]:
print(f"[ERROR] {result['message']}")
return
results = result["data"]["results"]
if not results:
print(f"No files found matching '{args.keyword}'")
return
headers = ["Name", "Size", "Path"]
rows = [[r["name"], r["size_str"], r["path"]] for r in results]
_print_table(headers, rows)
print(f"\nFound: {result['data']['total']} items")
def cmd_share(client: WeiyunClient, args) -> None:
"""Handle 'share' command."""
expire = getattr(args, "expire", 0)
password = getattr(args, "password", None)
assume_yes = getattr(args, "yes", False)
# Creating a share link can expose private files to anyone on the
# internet, so always surface the full parameters and require an
# explicit confirmation before calling the Weiyun API.
expire_desc = f"{expire} day(s)" if expire else "NEVER (permanent link)"
pw_desc = "set" if password else "NONE (link is the only secret)"
print("[!] About to create a public share link with these settings:")
print(f" File: {args.path}")
print(f" Expires in: {expire_desc}")
print(f" Password: {pw_desc}")
if not password or not expire:
print(
" WARNING: Sharing without both a password and an "
"expiration makes the file reachable indefinitely by anyone "
"holding the URL."
)
if not _confirm(
f"Create share link for '{args.path}'?",
assume_yes=assume_yes,
):
print("[-] Share cancelled.")
return
print(f"[*] Creating share for: {args.path}")
result = client.create_share(
args.path,
expire_days=expire,
password=password,
)
if result["success"]:
d = result["data"]
print(f"[✓] Share created!")
print(f" URL: {d['share_url']}")
if d.get("password"):
print(f" Password: {d['password']}")
if d.get("expire_at"):
print(f" Expires: {d['expire_at']}")
else:
print(f"[✗] Share failed: {result['message']}")
def cmd_unshare(client: WeiyunClient, args) -> None:
"""Handle 'unshare' command."""
if not _confirm(
f"Cancel share '{args.share_id}'?",
assume_yes=getattr(args, "yes", False),
):
print("[-] Unshare cancelled.")
return
result = client.cancel_share(args.share_id)
if result["success"]:
print(f"[✓] Share {args.share_id} cancelled")
else:
print(f"[✗] Cancel failed: {result['message']}")
def cmd_shares(client: WeiyunClient, args) -> None:
"""Handle 'shares' command."""
result = client.list_shares(
status=getattr(args, "status", "all")
)
if not result["success"]:
print(f"[ERROR] {result['message']}")
return
shares = result["data"]["shares"]
if not shares:
print("No shares found")
return
headers = ["ID", "File", "URL", "Status", "Views", "Downloads", "Expires"]
rows = []
for s in shares:
rows.append([
s["share_id"], s["file_name"], s["share_url"],
s["status"], s["view_count"], s["download_count"],
s["expire_at"],
])
_print_table(headers, rows)
print(f"\nTotal: {result['data']['total']} shares")
def cmd_space(client: WeiyunClient, args) -> None:
"""Handle 'space' command."""
result = client.get_space_info()
if not result["success"]:
print(f"[ERROR] {result['message']}")
return
d = result["data"]
print("=" * 40)
print(" Weiyun Space Usage")
print("=" * 40)
print(f" Total: {d['total_space_str']}")
print(f" Used: {d['used_space_str']} ({d['usage_percent']}%)")
print(f" Free: {d['free_space_str']}")
print(f" Files: {d['file_count']}")
print(f" Folders: {d['folder_count']}")
print("=" * 40)
# Simple progress bar
bar_width = 30
filled = int(bar_width * d["usage_percent"] / 100)
bar = "█" * filled + "░" * (bar_width - filled)
print(f" [{bar}] {d['usage_percent']}%")
def cmd_recycle(client: WeiyunClient, args) -> None:
"""Handle 'recycle' command."""
result = client.get_recycle_bin()
if not result["success"]:
print(f"[ERROR] {result['message']}")
return
files = result["data"]["files"]
if not files:
print("Recycle bin is empty")
return
headers = ["ID", "Name", "Size", "Original Path", "Deleted At"]
rows = []
for f in files:
rows.append([
f["file_id"], f["name"], f["size_str"],
f["original_path"], f["deleted_at"],
])
_print_table(headers, rows)
print(f"\nTotal: {result['data']['total']} items "
f"({result['data']['total_size_str']})")
def cmd_restore(client: WeiyunClient, args) -> None:
"""Handle 'restore' command."""
result = client.restore_file(args.file_id)
if result["success"]:
d = result["data"]
print(f"[✓] Restored to: {d['restored_path']}")
else:
print(f"[✗] Restore failed: {result['message']}")
def cmd_clear_recycle(client: WeiyunClient, args) -> None:
"""Handle 'clear-recycle' command."""
assume_yes = getattr(args, "yes", False)
if not getattr(args, "confirm", False) and not assume_yes:
print("[!] This will permanently delete all files in recycle bin!")
print(" Add --confirm flag to proceed.")
return
# Even with --confirm, prompt once more interactively (unless --yes).
if not _confirm(
"Permanently empty the Weiyun recycle bin? This cannot be undone.",
assume_yes=assume_yes,
):
print("[-] Clear cancelled.")
return
result = client.clear_recycle_bin(confirm=True)
if result["success"]:
d = result["data"]
print(f"[✓] Recycle bin cleared!")
print(f" Deleted: {d['deleted_count']} files")
print(f" Freed: {d['freed_space_str']}")
else:
print(f"[✗] Clear failed: {result['message']}")
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="Weiyun Skills - Tencent Weiyun Management CLI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python -m weiyun_skills.main list /
python -m weiyun_skills.main upload ./file.pdf /docs/
python -m weiyun_skills.main download /docs/file.pdf ./
python -m weiyun_skills.main share /docs/file.pdf --expire 7
python -m weiyun_skills.main space
"""
)
parser.add_argument(
"--cookies", type=str, default=None,
help="Cookies string (overrides cookies.json)"
)
parser.add_argument(
"-y", "--yes", action="store_true",
help=(
"Skip interactive confirmation prompts for destructive or "
"exposure-sensitive actions (delete, permanent-delete, move, "
"copy, rename, share, unshare, clear-recycle). Use with care."
)
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# list
p_list = subparsers.add_parser("list", help="List files in a directory")
p_list.add_argument("path", nargs="?", default="/", help="Directory path")
p_list.add_argument("--sort", choices=["name", "size", "time"], default="name")
p_list.add_argument("--order", choices=["asc", "desc"], default="asc")
# upload
p_upload = subparsers.add_parser("upload", help="Upload a file")
p_upload.add_argument("local", help="Local file path")
p_upload.add_argument("remote", help="Remote target path")
p_upload.add_argument("--overwrite", action="store_true")
# upload-folder
p_ul_folder = subparsers.add_parser("upload-folder",
help="Upload a folder")
p_ul_folder.add_argument("local", help="Local folder path")
p_ul_folder.add_argument("remote", nargs="?", default="/",
help="Remote target path (default: root)")
p_ul_folder.add_argument("--overwrite", action="store_true",
help="Overwrite existing files on Weiyun")
# download
p_download = subparsers.add_parser("download", help="Download a file")
p_download.add_argument("remote", help="Remote file path")
p_download.add_argument("local", help="Local save path")
p_download.add_argument("--overwrite", action="store_true")
# download-folder
p_dl_folder = subparsers.add_parser("download-folder",
help="Download a folder")
p_dl_folder.add_argument("folder", help="Folder name on Weiyun")
p_dl_folder.add_argument("local", help="Local save directory")
p_dl_folder.add_argument("--overwrite", action="store_true",
help="Overwrite existing local files")
p_dl_folder.add_argument("--zip", action="store_true",
help="Download as zip file instead of "
"individual files")
# delete
p_delete = subparsers.add_parser("delete", help="Delete a file")
p_delete.add_argument("path", help="File path to delete")
p_delete.add_argument("--permanent", action="store_true",
help="Permanently delete (skip recycle bin)")
# move
p_move = subparsers.add_parser("move", help="Move a file")
p_move.add_argument("source", help="Source path")
p_move.add_argument("target", help="Target directory path")
# copy
p_copy = subparsers.add_parser("copy", help="Copy a file")
p_copy.add_argument("source", help="Source path")
p_copy.add_argument("target", help="Target directory path")
# rename
p_rename = subparsers.add_parser("rename", help="Rename a file")
p_rename.add_argument("path", help="File path")
p_rename.add_argument("name", help="New name")
# mkdir
p_mkdir = subparsers.add_parser("mkdir", help="Create a folder")
p_mkdir.add_argument("path", help="Folder path")
# search
p_search = subparsers.add_parser("search", help="Search files")
p_search.add_argument("keyword", help="Search keyword")
p_search.add_argument("--type",
choices=["all", "document", "image", "video", "audio"],
default="all")
# share
p_share = subparsers.add_parser("share", help="Create a share link")
p_share.add_argument("path", help="File path to share")
p_share.add_argument("--expire", type=int, default=0,
help="Expire in days (0=permanent)")
p_share.add_argument("--password", type=str, default=None,
help="Share password (4 chars)")
# unshare
p_unshare = subparsers.add_parser("unshare", help="Cancel a share")
p_unshare.add_argument("share_id", help="Share ID to cancel")
# shares
p_shares = subparsers.add_parser("shares", help="List all shares")
p_shares.add_argument("--status", choices=["all", "active", "expired"],
default="all")
# space
subparsers.add_parser("space", help="Show space usage")
# recycle
subparsers.add_parser("recycle", help="Show recycle bin")
# restore
p_restore = subparsers.add_parser("restore", help="Restore from recycle bin")
p_restore.add_argument("file_id", help="File ID to restore")
# clear-recycle
p_clear = subparsers.add_parser("clear-recycle",
help="Clear recycle bin")
p_clear.add_argument("--confirm", action="store_true",
help="Confirm clear operation")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
# Initialize client
client = WeiyunClient(cookies_str=args.cookies)
# Dispatch command
commands = {
"list": cmd_list,
"upload": cmd_upload,
"upload-folder": cmd_upload_folder,
"download": cmd_download,
"download-folder": cmd_download_folder,
"delete": cmd_delete,
"move": cmd_move,
"copy": cmd_copy,
"rename": cmd_rename,
"mkdir": cmd_mkdir,
"search": cmd_search,
"share": cmd_share,
"unshare": cmd_unshare,
"shares": cmd_shares,
"space": cmd_space,
"recycle": cmd_recycle,
"restore": cmd_restore,
"clear-recycle": cmd_clear_recycle,
}
handler = commands.get(args.command)
if handler:
handler(client, args)
else:
parser.print_help()
if __name__ == "__main__":
main()