1+ import tarfile
2+ import zipfile
3+ from io import BytesIO
14from typing import Optional
25
36import click
7+ from tabulate import tabulate
48
59from ...utils .launchable_client import LaunchableClient
610from ..helper import require_session
711
812
13+ class AttachmentStatus :
14+ SUCCESS = "✓ Recorded successfully"
15+ FAILED = "⚠ Failed to record"
16+ SKIPPED_NON_TEXT = "⚠ Skipped: not a valid text file"
17+
18+
919@click .command ()
1020@click .option (
1121 '--session' ,
@@ -21,15 +31,95 @@ def attachment(
2131 session : Optional [str ] = None
2232):
2333 client = LaunchableClient (app = context .obj )
34+ summary_rows = []
2435 try :
2536 session = require_session (session )
37+ assert session is not None
2638
2739 for a in attachments :
28- click .echo ("Sending {}" .format (a ))
29- with open (a , mode = 'rb' ) as f :
30- res = client .request (
31- "post" , "{}/attachment" .format (session ), compress = True , payload = f ,
32- additional_headers = {"Content-Disposition" : "attachment;filename=\" {}\" " .format (a )})
33- res .raise_for_status ()
40+ # If zip file
41+ if zipfile .is_zipfile (a ):
42+ with zipfile .ZipFile (a , 'r' ) as zip_file :
43+ for zip_info in zip_file .infolist ():
44+ if zip_info .is_dir ():
45+ continue
46+
47+ file_content = zip_file .read (zip_info .filename )
48+
49+ if not valid_utf8_file (file_content ):
50+ summary_rows .append (
51+ [zip_info .filename , AttachmentStatus .SKIPPED_NON_TEXT ])
52+ continue
53+
54+ status = post_attachment (
55+ client , session , file_content , zip_info .filename )
56+ summary_rows .append ([zip_info .filename , status ])
57+
58+ # If tar file (tar, tar.gz, tar.bz2, tgz, etc.)
59+ elif tarfile .is_tarfile (a ):
60+ with tarfile .open (a , 'r:*' ) as tar_file :
61+ for tar_info in tar_file :
62+ if tar_info .isdir ():
63+ continue
64+
65+ file_obj = tar_file .extractfile (tar_info )
66+ if file_obj is None :
67+ continue
68+
69+ file_content = file_obj .read ()
70+
71+ if not valid_utf8_file (file_content ):
72+ summary_rows .append (
73+ [tar_info .name , AttachmentStatus .SKIPPED_NON_TEXT ])
74+ continue
75+
76+ status = post_attachment (
77+ client , session , file_content , tar_info .name )
78+ summary_rows .append ([tar_info .name , status ])
79+
80+ else :
81+ with open (a , mode = 'rb' ) as f :
82+ file_content = f .read ()
83+
84+ if not valid_utf8_file (file_content ):
85+ summary_rows .append (
86+ [a , AttachmentStatus .SKIPPED_NON_TEXT ])
87+ continue
88+
89+ status = post_attachment (client , session , file_content , a )
90+ summary_rows .append ([a , status ])
91+
3492 except Exception as e :
3593 client .print_exception_and_recover (e )
94+
95+ display_summary_as_table (summary_rows )
96+
97+
98+ def valid_utf8_file (file_content : bytes ) -> bool :
99+ # Check for null bytes (binary files)
100+ if b'\x00 ' in file_content :
101+ return False
102+
103+ try :
104+ file_content .decode ('utf-8' )
105+ return True
106+ except UnicodeDecodeError :
107+ return False
108+
109+
110+ def post_attachment (client : LaunchableClient , session : str , file_content : bytes , filename : str ) -> str :
111+ try :
112+ res = client .request (
113+ "post" , "{}/attachment" .format (session ), compress = True , payload = BytesIO (file_content ),
114+ additional_headers = {"Content-Disposition" : "attachment;filename=\" {}\" " .format (filename )})
115+ res .raise_for_status ()
116+ return AttachmentStatus .SUCCESS
117+ except Exception as e :
118+ click .echo ("Failed to upload {}: {}" .format (
119+ filename , str (e )), err = True )
120+ return AttachmentStatus .FAILED
121+
122+
123+ def display_summary_as_table (rows ):
124+ headers = ["File" , "Status" ]
125+ click .echo (tabulate (rows , headers , tablefmt = "github" ))
0 commit comments