|
| 1 | +""" |
| 2 | +CLI commands for file operations. |
| 3 | +""" |
| 4 | + |
| 5 | +import argparse |
| 6 | +import json |
| 7 | +import sys |
| 8 | +from pathlib import Path |
| 9 | +from typing import cast |
| 10 | + |
| 11 | +from slack_clacks.auth.client import create_client |
| 12 | +from slack_clacks.auth.validation import get_scopes_for_mode, validate |
| 13 | +from slack_clacks.configuration.database import ( |
| 14 | + ensure_db_updated, |
| 15 | + get_current_context, |
| 16 | + get_session, |
| 17 | +) |
| 18 | +from slack_clacks.files.operations import ( |
| 19 | + download_file_to_path, |
| 20 | + download_file_to_stdout, |
| 21 | + extract_file_id_from_permalink, |
| 22 | + get_file_info, |
| 23 | + list_files, |
| 24 | +) |
| 25 | +from slack_clacks.messaging.operations import resolve_channel_id, resolve_user_id |
| 26 | +from slack_clacks.upload.cli import generate_upload_parser |
| 27 | + |
| 28 | + |
| 29 | +def handle_download(args: argparse.Namespace) -> None: |
| 30 | + ensure_db_updated(config_dir=args.config_dir) |
| 31 | + with get_session(args.config_dir) as session: |
| 32 | + context = get_current_context(session) |
| 33 | + if context is None: |
| 34 | + raise ValueError( |
| 35 | + "No active authentication context. Authenticate with: clacks auth login" |
| 36 | + ) |
| 37 | + |
| 38 | + scopes = get_scopes_for_mode(context.app_type) |
| 39 | + validate("files:read", scopes, raise_on_error=True) |
| 40 | + |
| 41 | + client = create_client(context.access_token, context.app_type) |
| 42 | + |
| 43 | + # Resolve file ID |
| 44 | + if args.file_id: |
| 45 | + file_id = args.file_id |
| 46 | + else: |
| 47 | + file_id = extract_file_id_from_permalink(args.permalink) |
| 48 | + |
| 49 | + # Get file metadata |
| 50 | + info = cast(dict, get_file_info(client, file_id)) |
| 51 | + file_data = info.get("file", {}) |
| 52 | + filename = file_data.get("name", file_id) |
| 53 | + download_url = file_data.get("url_private_download") or file_data.get( |
| 54 | + "url_private" |
| 55 | + ) |
| 56 | + |
| 57 | + if not download_url: |
| 58 | + raise ValueError(f"No download URL available for file {file_id}") |
| 59 | + |
| 60 | + # Download to stdout |
| 61 | + if args.write == "-": |
| 62 | + nbytes = download_file_to_stdout( |
| 63 | + download_url, context.access_token, context.app_type |
| 64 | + ) |
| 65 | + print(f"{filename}: {nbytes} bytes", file=sys.stderr) |
| 66 | + return |
| 67 | + |
| 68 | + # Determine output path |
| 69 | + if args.write: |
| 70 | + output_path = Path(args.write) |
| 71 | + else: |
| 72 | + output_path = Path(filename) |
| 73 | + |
| 74 | + # Check for existing file |
| 75 | + if output_path.exists() and not args.force: |
| 76 | + raise FileExistsError( |
| 77 | + f"File already exists: {output_path}. Use --force to overwrite." |
| 78 | + ) |
| 79 | + |
| 80 | + nbytes = download_file_to_path( |
| 81 | + download_url, context.access_token, context.app_type, output_path |
| 82 | + ) |
| 83 | + print(f"{output_path}: {nbytes} bytes", file=sys.stderr) |
| 84 | + |
| 85 | + |
| 86 | +def handle_list(args: argparse.Namespace) -> None: |
| 87 | + ensure_db_updated(config_dir=args.config_dir) |
| 88 | + with get_session(args.config_dir) as session: |
| 89 | + context = get_current_context(session) |
| 90 | + if context is None: |
| 91 | + raise ValueError( |
| 92 | + "No active authentication context. Authenticate with: clacks auth login" |
| 93 | + ) |
| 94 | + |
| 95 | + scopes = get_scopes_for_mode(context.app_type) |
| 96 | + validate("files:read", scopes, raise_on_error=True) |
| 97 | + |
| 98 | + client = create_client(context.access_token, context.app_type) |
| 99 | + |
| 100 | + # Resolve channel/user identifiers to IDs |
| 101 | + channel_id = None |
| 102 | + if args.channel: |
| 103 | + channel_id = resolve_channel_id(client, args.channel, session, context.name) |
| 104 | + |
| 105 | + user_id = None |
| 106 | + if args.user: |
| 107 | + user_id = resolve_user_id(client, args.user, session, context.name) |
| 108 | + |
| 109 | + result = list_files( |
| 110 | + client, channel=channel_id, user=user_id, limit=args.limit, page=args.page |
| 111 | + ) |
| 112 | + json.dump(result, sys.stdout) |
| 113 | + |
| 114 | + |
| 115 | +def handle_info(args: argparse.Namespace) -> None: |
| 116 | + ensure_db_updated(config_dir=args.config_dir) |
| 117 | + with get_session(args.config_dir) as session: |
| 118 | + context = get_current_context(session) |
| 119 | + if context is None: |
| 120 | + raise ValueError( |
| 121 | + "No active authentication context. Authenticate with: clacks auth login" |
| 122 | + ) |
| 123 | + |
| 124 | + scopes = get_scopes_for_mode(context.app_type) |
| 125 | + validate("files:read", scopes, raise_on_error=True) |
| 126 | + |
| 127 | + client = create_client(context.access_token, context.app_type) |
| 128 | + |
| 129 | + result = get_file_info(client, args.file_id) |
| 130 | + json.dump(result, sys.stdout) |
| 131 | + |
| 132 | + |
| 133 | +def generate_files_cli() -> argparse.ArgumentParser: |
| 134 | + parser = argparse.ArgumentParser( |
| 135 | + description="Upload, download, list, and inspect files" |
| 136 | + ) |
| 137 | + parser.set_defaults(func=lambda _: parser.print_help()) |
| 138 | + |
| 139 | + subparsers = parser.add_subparsers(dest="files_command") |
| 140 | + |
| 141 | + # --- download --- |
| 142 | + dl_parser = subparsers.add_parser("download", help="Download a file from Slack") |
| 143 | + dl_parser.add_argument( |
| 144 | + "-D", |
| 145 | + "--config-dir", |
| 146 | + type=str, |
| 147 | + default=None, |
| 148 | + help="Configuration directory", |
| 149 | + ) |
| 150 | + id_group = dl_parser.add_mutually_exclusive_group(required=True) |
| 151 | + id_group.add_argument( |
| 152 | + "-i", |
| 153 | + "--file-id", |
| 154 | + type=str, |
| 155 | + help="Slack file ID (e.g., F2147483862)", |
| 156 | + ) |
| 157 | + id_group.add_argument( |
| 158 | + "--permalink", |
| 159 | + type=str, |
| 160 | + help="Slack file permalink URL", |
| 161 | + ) |
| 162 | + dl_parser.add_argument( |
| 163 | + "-w", |
| 164 | + "--write", |
| 165 | + type=str, |
| 166 | + default=None, |
| 167 | + help="Output path (default: original filename in CWD, use - for stdout)", |
| 168 | + ) |
| 169 | + dl_parser.add_argument( |
| 170 | + "--force", |
| 171 | + action="store_true", |
| 172 | + help="Overwrite existing file without prompting", |
| 173 | + ) |
| 174 | + dl_parser.set_defaults(func=handle_download) |
| 175 | + |
| 176 | + # --- list --- |
| 177 | + list_parser = subparsers.add_parser("list", help="List files") |
| 178 | + list_parser.add_argument( |
| 179 | + "-D", |
| 180 | + "--config-dir", |
| 181 | + type=str, |
| 182 | + default=None, |
| 183 | + help="Configuration directory", |
| 184 | + ) |
| 185 | + list_parser.add_argument( |
| 186 | + "-c", |
| 187 | + "--channel", |
| 188 | + type=str, |
| 189 | + help="Filter by channel", |
| 190 | + ) |
| 191 | + list_parser.add_argument( |
| 192 | + "-u", |
| 193 | + "--user", |
| 194 | + type=str, |
| 195 | + help="Filter by user", |
| 196 | + ) |
| 197 | + list_parser.add_argument( |
| 198 | + "-l", |
| 199 | + "--limit", |
| 200 | + type=int, |
| 201 | + default=20, |
| 202 | + help="Max files to list (default: 20)", |
| 203 | + ) |
| 204 | + list_parser.add_argument( |
| 205 | + "-p", |
| 206 | + "--page", |
| 207 | + type=int, |
| 208 | + default=1, |
| 209 | + help="Page number (default: 1)", |
| 210 | + ) |
| 211 | + list_parser.set_defaults(func=handle_list) |
| 212 | + |
| 213 | + # --- info --- |
| 214 | + info_parser = subparsers.add_parser("info", help="Show file metadata") |
| 215 | + info_parser.add_argument( |
| 216 | + "-D", |
| 217 | + "--config-dir", |
| 218 | + type=str, |
| 219 | + default=None, |
| 220 | + help="Configuration directory", |
| 221 | + ) |
| 222 | + info_parser.add_argument( |
| 223 | + "-i", |
| 224 | + "--file-id", |
| 225 | + type=str, |
| 226 | + required=True, |
| 227 | + help="Slack file ID (e.g., F2147483862)", |
| 228 | + ) |
| 229 | + info_parser.set_defaults(func=handle_info) |
| 230 | + |
| 231 | + # --- upload --- |
| 232 | + upload_parser = generate_upload_parser() |
| 233 | + subparsers.add_parser( |
| 234 | + "upload", |
| 235 | + parents=[upload_parser], |
| 236 | + add_help=False, |
| 237 | + help=upload_parser.description, |
| 238 | + ) |
| 239 | + |
| 240 | + return parser |
0 commit comments