zakat.file_server
xxx
1import socketserver 2import threading 3import os 4import uuid 5import shutil 6import json 7from enum import Enum, auto 8from wsgiref.simple_server import make_server 9import io 10 11 12class FileType(Enum): 13 Database = 'db' 14 CSV = 'csv' 15 16 17# SAFE Circular Imports (Duplicated class again) 18class Action(Enum): 19 CREATE = auto() 20 TRACK = auto() 21 LOG = auto() 22 SUB = auto() 23 ADD_FILE = auto() 24 REMOVE_FILE = auto() 25 BOX_TRANSFER = auto() 26 EXCHANGE = auto() 27 REPORT = auto() 28 ZAKAT = auto() 29 30 31def find_available_port() -> int: 32 """ 33 Finds and returns an available TCP port on the local machine. 34 35 This function utilizes a TCP server socket to bind to port 0, which 36 instructs the operating system to automatically assign an available 37 port. The assigned port is then extracted and returned. 38 39 Returns: 40 int: The available TCP port number. 41 42 Raises: 43 OSError: If an error occurs during the port binding process, such 44 as all ports being in use. 45 46 Example: 47 port = find_available_port() 48 print(f"Available port: {port}") 49 """ 50 with socketserver.TCPServer(("localhost", 0), None) as s: 51 return s.server_address[1] 52 53 54def start_file_server(database_path: str, database_callback: callable = None, csv_callback: callable = None, 55 debug: bool = False) -> tuple: 56 """ 57 Starts a multi-purpose WSGI server to manage file interactions for a Zakat application. 58 59 This server facilitates the following functionalities: 60 61 1. GET /{file_uuid}/get: Download the database file specified by `database_path`. 62 2. GET /{file_uuid}/upload: Display an HTML form for uploading files. 63 3. POST /{file_uuid}/upload: Handle file uploads, distinguishing between: 64 - Database File (.db): Replaces the existing database with the uploaded one. 65 - CSV File (.csv): Imports data from the CSV into the existing database. 66 67 Args: 68 database_path (str): The path to the camel database file. 69 database_callback (callable, optional): A function to call after a successful database upload. 70 It receives the uploaded database path as its argument. 71 csv_callback (callable, optional): A function to call after a successful CSV upload. It receives the uploaded CSV path, 72 the database path, and the debug flag as its arguments. 73 debug (bool, optional): If True, print debugging information. Defaults to False. 74 75 Returns: 76 Tuple[str, str, str, threading.Thread, Callable[[], None]]: A tuple containing: 77 - file_name (str): The name of the database file. 78 - download_url (str): The URL to download the database file. 79 - upload_url (str): The URL to access the file upload form. 80 - server_thread (threading.Thread): The thread running the server. 81 - shutdown_server (Callable[[], None]): A function to gracefully shut down the server. 82 83 Example: 84 _, download_url, upload_url, server_thread, shutdown_server = start_file_server("zakat.db") 85 print(f"Download database: {download_url}") 86 print(f"Upload files: {upload_url}") 87 server_thread.start() 88 # ... later ... 89 shutdown_server() 90 """ 91 file_uuid = uuid.uuid4() 92 file_name = os.path.basename(database_path) 93 94 port = find_available_port() 95 download_url = f"http://localhost:{port}/{file_uuid}/get" 96 upload_url = f"http://localhost:{port}/{file_uuid}/upload" 97 98 # Upload directory 99 upload_directory = "./uploads" 100 os.makedirs(upload_directory, exist_ok=True) 101 102 # HTML templates 103 upload_form = f""" 104 <html lang="en"> 105 <head> 106 <title>Zakat File Server</title> 107 </head> 108 <body> 109 <h1>Zakat File Server</h1> 110 <h3>You can download the <a target="__blank" href="{download_url}">database file</a>...</h3> 111 <h3>Or upload a new file to restore a database or import `CSV` file:</h3> 112 <form action="/{file_uuid}/upload" method="post" enctype="multipart/form-data"> 113 <input type="file" name="file" required><br/> 114 <input type="radio" id="{FileType.Database.value}" name="upload_type" value="{FileType.Database.value}" required> 115 <label for="database">Database File</label><br/> 116 <input type="radio"id="{FileType.CSV.value}" name="upload_type" value="{FileType.CSV.value}"> 117 <label for="csv">CSV File</label><br/> 118 <input type="submit" value="Upload"><br/> 119 </form> 120 </body></html> 121 """ 122 123 # WSGI application 124 def wsgi_app(environ, start_response): 125 path = environ.get('PATH_INFO', '') 126 method = environ.get('REQUEST_METHOD', 'GET') 127 128 if path == f"/{file_uuid}/get" and method == 'GET': 129 # GET: Serve the existing file 130 try: 131 with open(database_path, "rb") as f: 132 file_content = f.read() 133 134 start_response('200 OK', [ 135 ('Content-type', 'application/octet-stream'), 136 ('Content-Disposition', f'attachment; filename="{file_name}"'), 137 ('Content-Length', str(len(file_content))) 138 ]) 139 return [file_content] 140 except FileNotFoundError: 141 start_response('404 Not Found', [('Content-type', 'text/plain')]) 142 return [b'File not found'] 143 144 elif path == f"/{file_uuid}/upload" and method == 'GET': 145 # GET: Serve the upload form 146 start_response('200 OK', [('Content-type', 'text/html')]) 147 return [upload_form.encode()] 148 149 elif path == f"/{file_uuid}/upload" and method == 'POST': 150 # POST: Handle file uploads 151 try: 152 # Get content length 153 content_length = int(environ.get('CONTENT_LENGTH', 0)) 154 155 # Get content type and boundary 156 content_type = environ.get('CONTENT_TYPE', '') 157 158 # Read the request body 159 request_body = environ['wsgi.input'].read(content_length) 160 161 # Create a file-like object from the request body 162 # request_body_file = io.BytesIO(request_body) 163 164 # Parse the multipart form data using WSGI approach 165 # First, detect the boundary from content_type 166 boundary = None 167 for part in content_type.split(';'): 168 part = part.strip() 169 if part.startswith('boundary='): 170 boundary = part[9:] 171 if boundary.startswith('"') and boundary.endswith('"'): 172 boundary = boundary[1:-1] 173 break 174 175 if not boundary: 176 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 177 return [b"Missing boundary in multipart form data"] 178 179 # Process multipart data 180 parts = request_body.split(f'--{boundary}'.encode()) 181 182 # Initialize variables to store form data 183 upload_type = None 184 # file_item = None 185 file_data = None 186 filename = None 187 188 # Process each part 189 for part in parts: 190 if not part.strip(): 191 continue 192 193 # Split header and body 194 try: 195 headers_raw, body = part.split(b'\r\n\r\n', 1) 196 headers_text = headers_raw.decode('utf-8') 197 except ValueError: 198 continue 199 200 # Parse headers 201 headers = {} 202 for header_line in headers_text.split('\r\n'): 203 if ':' in header_line: 204 name, value = header_line.split(':', 1) 205 headers[name.strip().lower()] = value.strip() 206 207 # Get content disposition 208 content_disposition = headers.get('content-disposition', '') 209 if not content_disposition.startswith('form-data'): 210 continue 211 212 # Extract field name 213 field_name = None 214 for item in content_disposition.split(';'): 215 item = item.strip() 216 if item.startswith('name='): 217 field_name = item[5:].strip('"\'') 218 break 219 220 if not field_name: 221 continue 222 223 # Handle upload_type field 224 if field_name == 'upload_type': 225 # Remove trailing data including the boundary 226 body_end = body.find(b'\r\n--') 227 if body_end >= 0: 228 body = body[:body_end] 229 upload_type = body.decode('utf-8').strip() 230 231 # Handle file field 232 elif field_name == 'file': 233 # Extract filename 234 for item in content_disposition.split(';'): 235 item = item.strip() 236 if item.startswith('filename='): 237 filename = item[9:].strip('"\'') 238 break 239 240 if filename: 241 # Remove trailing data including the boundary 242 body_end = body.find(b'\r\n--') 243 if body_end >= 0: 244 body = body[:body_end] 245 file_data = body 246 247 if debug: 248 print('upload_type', upload_type) 249 250 if debug: 251 print('upload_type:', upload_type) 252 print('filename:', filename) 253 254 if not upload_type or upload_type not in [FileType.Database.value, FileType.CSV.value]: 255 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 256 return [b"Invalid upload type"] 257 258 if not filename or not file_data: 259 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 260 return [b"Missing file data"] 261 262 if debug: 263 print(f'Uploaded filename: {filename}') 264 265 # Save the file 266 file_path = os.path.join(upload_directory, upload_type) 267 with open(file_path, 'wb') as f: 268 f.write(file_data) 269 270 # Process based on file type 271 if upload_type == FileType.Database.value: 272 try: 273 # Verify database file 274 if database_callback is not None: 275 database_callback(file_path) 276 277 # Copy database into the original path 278 shutil.copy2(file_path, database_path) 279 280 start_response('200 OK', [('Content-type', 'text/plain')]) 281 return [b"Database file uploaded successfully."] 282 except Exception as e: 283 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 284 return [str(e).encode()] 285 286 elif upload_type == FileType.CSV.value: 287 try: 288 if csv_callback is not None: 289 result = csv_callback(file_path, database_path, debug) 290 if debug: 291 print(f'CSV imported: {result}') 292 if len(result[2]) != 0: 293 start_response('200 OK', [('Content-type', 'application/json')]) 294 return [json.dumps(result).encode()] 295 296 start_response('200 OK', [('Content-type', 'text/plain')]) 297 return [b"CSV file uploaded successfully."] 298 except Exception as e: 299 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 300 return [str(e).encode()] 301 302 except Exception as e: 303 start_response('500 Internal Server Error', [('Content-type', 'text/plain')]) 304 return [f"Error processing upload: {str(e)}".encode()] 305 306 else: 307 # 404 for anything else 308 start_response('404 Not Found', [('Content-type', 'text/plain')]) 309 return [b'Not Found'] 310 311 # Create and start the server 312 httpd = make_server('localhost', port, wsgi_app) 313 server_thread = threading.Thread(target=httpd.serve_forever) 314 315 def shutdown_server(): 316 nonlocal httpd, server_thread 317 httpd.shutdown() 318 server_thread.join() # Wait for the thread to finish 319 320 return file_name, download_url, upload_url, server_thread, shutdown_server 321 322 323def main(): 324 from zakat_tracker import ZakatTracker, Action # SAFE Circular Imports 325 # Example usage (replace with your file path) 326 file_to_share = f"{uuid.uuid4()}.{ZakatTracker.ext()}" # Or any other file type 327 328 def database_callback(file_path): 329 ZakatTracker(db_path=file_path) 330 331 def csv_callback(file_path, database_path, debug): 332 x = ZakatTracker(db_path=database_path) 333 return x.import_csv(file_path, debug=debug) 334 335 file_name, download_url, upload_url, server_thread, shutdown_server = start_file_server( 336 file_to_share, 337 database_callback=database_callback, 338 csv_callback=csv_callback, 339 debug=True, 340 ) 341 342 print(f"\nTo download '{file_name}', use this URL:") 343 print(download_url) 344 345 print(f"\nTo upload a new '{file_name}', use this URL:") 346 print(upload_url) 347 print("(The uploaded file will replace the existing one.)") 348 349 print("\nStarting the server...") 350 server_thread.start() 351 print("The server started.") 352 353 input("\nPress Enter to stop the server...") 354 shutdown_server() 355 356 357if __name__ == "__main__": 358 main()
19class Action(Enum): 20 CREATE = auto() 21 TRACK = auto() 22 LOG = auto() 23 SUB = auto() 24 ADD_FILE = auto() 25 REMOVE_FILE = auto() 26 BOX_TRANSFER = auto() 27 EXCHANGE = auto() 28 REPORT = auto() 29 ZAKAT = auto()
32def find_available_port() -> int: 33 """ 34 Finds and returns an available TCP port on the local machine. 35 36 This function utilizes a TCP server socket to bind to port 0, which 37 instructs the operating system to automatically assign an available 38 port. The assigned port is then extracted and returned. 39 40 Returns: 41 int: The available TCP port number. 42 43 Raises: 44 OSError: If an error occurs during the port binding process, such 45 as all ports being in use. 46 47 Example: 48 port = find_available_port() 49 print(f"Available port: {port}") 50 """ 51 with socketserver.TCPServer(("localhost", 0), None) as s: 52 return s.server_address[1]
Finds and returns an available TCP port on the local machine.
This function utilizes a TCP server socket to bind to port 0, which instructs the operating system to automatically assign an available port. The assigned port is then extracted and returned.
Returns: int: The available TCP port number.
Raises: OSError: If an error occurs during the port binding process, such as all ports being in use.
Example: port = find_available_port() print(f"Available port: {port}")
55def start_file_server(database_path: str, database_callback: callable = None, csv_callback: callable = None, 56 debug: bool = False) -> tuple: 57 """ 58 Starts a multi-purpose WSGI server to manage file interactions for a Zakat application. 59 60 This server facilitates the following functionalities: 61 62 1. GET /{file_uuid}/get: Download the database file specified by `database_path`. 63 2. GET /{file_uuid}/upload: Display an HTML form for uploading files. 64 3. POST /{file_uuid}/upload: Handle file uploads, distinguishing between: 65 - Database File (.db): Replaces the existing database with the uploaded one. 66 - CSV File (.csv): Imports data from the CSV into the existing database. 67 68 Args: 69 database_path (str): The path to the camel database file. 70 database_callback (callable, optional): A function to call after a successful database upload. 71 It receives the uploaded database path as its argument. 72 csv_callback (callable, optional): A function to call after a successful CSV upload. It receives the uploaded CSV path, 73 the database path, and the debug flag as its arguments. 74 debug (bool, optional): If True, print debugging information. Defaults to False. 75 76 Returns: 77 Tuple[str, str, str, threading.Thread, Callable[[], None]]: A tuple containing: 78 - file_name (str): The name of the database file. 79 - download_url (str): The URL to download the database file. 80 - upload_url (str): The URL to access the file upload form. 81 - server_thread (threading.Thread): The thread running the server. 82 - shutdown_server (Callable[[], None]): A function to gracefully shut down the server. 83 84 Example: 85 _, download_url, upload_url, server_thread, shutdown_server = start_file_server("zakat.db") 86 print(f"Download database: {download_url}") 87 print(f"Upload files: {upload_url}") 88 server_thread.start() 89 # ... later ... 90 shutdown_server() 91 """ 92 file_uuid = uuid.uuid4() 93 file_name = os.path.basename(database_path) 94 95 port = find_available_port() 96 download_url = f"http://localhost:{port}/{file_uuid}/get" 97 upload_url = f"http://localhost:{port}/{file_uuid}/upload" 98 99 # Upload directory 100 upload_directory = "./uploads" 101 os.makedirs(upload_directory, exist_ok=True) 102 103 # HTML templates 104 upload_form = f""" 105 <html lang="en"> 106 <head> 107 <title>Zakat File Server</title> 108 </head> 109 <body> 110 <h1>Zakat File Server</h1> 111 <h3>You can download the <a target="__blank" href="{download_url}">database file</a>...</h3> 112 <h3>Or upload a new file to restore a database or import `CSV` file:</h3> 113 <form action="/{file_uuid}/upload" method="post" enctype="multipart/form-data"> 114 <input type="file" name="file" required><br/> 115 <input type="radio" id="{FileType.Database.value}" name="upload_type" value="{FileType.Database.value}" required> 116 <label for="database">Database File</label><br/> 117 <input type="radio"id="{FileType.CSV.value}" name="upload_type" value="{FileType.CSV.value}"> 118 <label for="csv">CSV File</label><br/> 119 <input type="submit" value="Upload"><br/> 120 </form> 121 </body></html> 122 """ 123 124 # WSGI application 125 def wsgi_app(environ, start_response): 126 path = environ.get('PATH_INFO', '') 127 method = environ.get('REQUEST_METHOD', 'GET') 128 129 if path == f"/{file_uuid}/get" and method == 'GET': 130 # GET: Serve the existing file 131 try: 132 with open(database_path, "rb") as f: 133 file_content = f.read() 134 135 start_response('200 OK', [ 136 ('Content-type', 'application/octet-stream'), 137 ('Content-Disposition', f'attachment; filename="{file_name}"'), 138 ('Content-Length', str(len(file_content))) 139 ]) 140 return [file_content] 141 except FileNotFoundError: 142 start_response('404 Not Found', [('Content-type', 'text/plain')]) 143 return [b'File not found'] 144 145 elif path == f"/{file_uuid}/upload" and method == 'GET': 146 # GET: Serve the upload form 147 start_response('200 OK', [('Content-type', 'text/html')]) 148 return [upload_form.encode()] 149 150 elif path == f"/{file_uuid}/upload" and method == 'POST': 151 # POST: Handle file uploads 152 try: 153 # Get content length 154 content_length = int(environ.get('CONTENT_LENGTH', 0)) 155 156 # Get content type and boundary 157 content_type = environ.get('CONTENT_TYPE', '') 158 159 # Read the request body 160 request_body = environ['wsgi.input'].read(content_length) 161 162 # Create a file-like object from the request body 163 # request_body_file = io.BytesIO(request_body) 164 165 # Parse the multipart form data using WSGI approach 166 # First, detect the boundary from content_type 167 boundary = None 168 for part in content_type.split(';'): 169 part = part.strip() 170 if part.startswith('boundary='): 171 boundary = part[9:] 172 if boundary.startswith('"') and boundary.endswith('"'): 173 boundary = boundary[1:-1] 174 break 175 176 if not boundary: 177 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 178 return [b"Missing boundary in multipart form data"] 179 180 # Process multipart data 181 parts = request_body.split(f'--{boundary}'.encode()) 182 183 # Initialize variables to store form data 184 upload_type = None 185 # file_item = None 186 file_data = None 187 filename = None 188 189 # Process each part 190 for part in parts: 191 if not part.strip(): 192 continue 193 194 # Split header and body 195 try: 196 headers_raw, body = part.split(b'\r\n\r\n', 1) 197 headers_text = headers_raw.decode('utf-8') 198 except ValueError: 199 continue 200 201 # Parse headers 202 headers = {} 203 for header_line in headers_text.split('\r\n'): 204 if ':' in header_line: 205 name, value = header_line.split(':', 1) 206 headers[name.strip().lower()] = value.strip() 207 208 # Get content disposition 209 content_disposition = headers.get('content-disposition', '') 210 if not content_disposition.startswith('form-data'): 211 continue 212 213 # Extract field name 214 field_name = None 215 for item in content_disposition.split(';'): 216 item = item.strip() 217 if item.startswith('name='): 218 field_name = item[5:].strip('"\'') 219 break 220 221 if not field_name: 222 continue 223 224 # Handle upload_type field 225 if field_name == 'upload_type': 226 # Remove trailing data including the boundary 227 body_end = body.find(b'\r\n--') 228 if body_end >= 0: 229 body = body[:body_end] 230 upload_type = body.decode('utf-8').strip() 231 232 # Handle file field 233 elif field_name == 'file': 234 # Extract filename 235 for item in content_disposition.split(';'): 236 item = item.strip() 237 if item.startswith('filename='): 238 filename = item[9:].strip('"\'') 239 break 240 241 if filename: 242 # Remove trailing data including the boundary 243 body_end = body.find(b'\r\n--') 244 if body_end >= 0: 245 body = body[:body_end] 246 file_data = body 247 248 if debug: 249 print('upload_type', upload_type) 250 251 if debug: 252 print('upload_type:', upload_type) 253 print('filename:', filename) 254 255 if not upload_type or upload_type not in [FileType.Database.value, FileType.CSV.value]: 256 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 257 return [b"Invalid upload type"] 258 259 if not filename or not file_data: 260 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 261 return [b"Missing file data"] 262 263 if debug: 264 print(f'Uploaded filename: {filename}') 265 266 # Save the file 267 file_path = os.path.join(upload_directory, upload_type) 268 with open(file_path, 'wb') as f: 269 f.write(file_data) 270 271 # Process based on file type 272 if upload_type == FileType.Database.value: 273 try: 274 # Verify database file 275 if database_callback is not None: 276 database_callback(file_path) 277 278 # Copy database into the original path 279 shutil.copy2(file_path, database_path) 280 281 start_response('200 OK', [('Content-type', 'text/plain')]) 282 return [b"Database file uploaded successfully."] 283 except Exception as e: 284 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 285 return [str(e).encode()] 286 287 elif upload_type == FileType.CSV.value: 288 try: 289 if csv_callback is not None: 290 result = csv_callback(file_path, database_path, debug) 291 if debug: 292 print(f'CSV imported: {result}') 293 if len(result[2]) != 0: 294 start_response('200 OK', [('Content-type', 'application/json')]) 295 return [json.dumps(result).encode()] 296 297 start_response('200 OK', [('Content-type', 'text/plain')]) 298 return [b"CSV file uploaded successfully."] 299 except Exception as e: 300 start_response('400 Bad Request', [('Content-type', 'text/plain')]) 301 return [str(e).encode()] 302 303 except Exception as e: 304 start_response('500 Internal Server Error', [('Content-type', 'text/plain')]) 305 return [f"Error processing upload: {str(e)}".encode()] 306 307 else: 308 # 404 for anything else 309 start_response('404 Not Found', [('Content-type', 'text/plain')]) 310 return [b'Not Found'] 311 312 # Create and start the server 313 httpd = make_server('localhost', port, wsgi_app) 314 server_thread = threading.Thread(target=httpd.serve_forever) 315 316 def shutdown_server(): 317 nonlocal httpd, server_thread 318 httpd.shutdown() 319 server_thread.join() # Wait for the thread to finish 320 321 return file_name, download_url, upload_url, server_thread, shutdown_server
Starts a multi-purpose WSGI server to manage file interactions for a Zakat application.
This server facilitates the following functionalities:
- GET /{file_uuid}/get: Download the database file specified by
database_path
. - GET /{file_uuid}/upload: Display an HTML form for uploading files.
- POST /{file_uuid}/upload: Handle file uploads, distinguishing between:
- Database File (.db): Replaces the existing database with the uploaded one.
- CSV File (.csv): Imports data from the CSV into the existing database.
Args: database_path (str): The path to the camel database file. database_callback (callable, optional): A function to call after a successful database upload. It receives the uploaded database path as its argument. csv_callback (callable, optional): A function to call after a successful CSV upload. It receives the uploaded CSV path, the database path, and the debug flag as its arguments. debug (bool, optional): If True, print debugging information. Defaults to False.
Returns: Tuple[str, str, str, threading.Thread, Callable[[], None]]: A tuple containing: - file_name (str): The name of the database file. - download_url (str): The URL to download the database file. - upload_url (str): The URL to access the file upload form. - server_thread (threading.Thread): The thread running the server. - shutdown_server (Callable[[], None]): A function to gracefully shut down the server.
Example: _, download_url, upload_url, server_thread, shutdown_server = start_file_server("zakat.db") print(f"Download database: {download_url}") print(f"Upload files: {upload_url}") server_thread.start() # ... later ... shutdown_server()
324def main(): 325 from zakat_tracker import ZakatTracker, Action # SAFE Circular Imports 326 # Example usage (replace with your file path) 327 file_to_share = f"{uuid.uuid4()}.{ZakatTracker.ext()}" # Or any other file type 328 329 def database_callback(file_path): 330 ZakatTracker(db_path=file_path) 331 332 def csv_callback(file_path, database_path, debug): 333 x = ZakatTracker(db_path=database_path) 334 return x.import_csv(file_path, debug=debug) 335 336 file_name, download_url, upload_url, server_thread, shutdown_server = start_file_server( 337 file_to_share, 338 database_callback=database_callback, 339 csv_callback=csv_callback, 340 debug=True, 341 ) 342 343 print(f"\nTo download '{file_name}', use this URL:") 344 print(download_url) 345 346 print(f"\nTo upload a new '{file_name}', use this URL:") 347 print(upload_url) 348 print("(The uploaded file will replace the existing one.)") 349 350 print("\nStarting the server...") 351 server_thread.start() 352 print("The server started.") 353 354 input("\nPress Enter to stop the server...") 355 shutdown_server()