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