zakat.file_server
xxx
1import http.server 2import socketserver 3import threading 4import os 5import uuid 6import cgi 7from enum import Enum, auto 8import shutil 9import json 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 HTTP 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 pickle 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 class Handler(http.server.SimpleHTTPRequestHandler): 99 def do_GET(self): 100 if self.path == f"/{file_uuid}/get": 101 # GET: Serve the existing file 102 try: 103 with open(database_path, "rb") as f: 104 self.send_response(200) 105 self.send_header("Content-type", "application/octet-stream") 106 self.send_header("Content-Disposition", f'attachment; filename="{file_name}"') 107 self.end_headers() 108 self.wfile.write(f.read()) 109 except FileNotFoundError: 110 self.send_error(404, "File not found") 111 elif self.path == f"/{file_uuid}/upload": 112 # GET: Serve the upload form 113 self.send_response(200) 114 self.send_header("Content-type", "text/html") 115 self.end_headers() 116 self.wfile.write(f""" 117 <html lang="en"> 118 <head> 119 <title>Zakat File Server</title> 120 </head> 121 <body> 122 <h1>Zakat File Server</h1> 123 <h3>You can download the <a target="__blank" href="{download_url}">database file</a>...</h3> 124 <h3>Or upload a new file to restore a database or import `CSV` file:</h3> 125 <form action="/{file_uuid}/upload" method="post" enctype="multipart/form-data"> 126 <input type="file" name="file" required><br/> 127 <input type="radio" id="{FileType.Database.value}" name="upload_type" value="{FileType.Database.value}" required> 128 <label for="database">Database File</label><br/> 129 <input type="radio"id="{FileType.CSV.value}" name="upload_type" value="{FileType.CSV.value}"> 130 <label for="csv">CSV File</label><br/> 131 <input type="submit" value="Upload"><br/> 132 </form> 133 </body></html> 134 """.encode()) 135 else: 136 self.send_error(404) 137 138 def do_POST(self): 139 if self.path == f"/{file_uuid}/upload": 140 # POST: Handle request 141 # 1. Get the Form Data 142 form_data = cgi.FieldStorage( 143 fp=self.rfile, 144 headers=self.headers, 145 environ={'REQUEST_METHOD': 'POST'} 146 ) 147 upload_type = form_data.getvalue("upload_type") 148 149 if debug: 150 print('upload_type', upload_type) 151 152 if upload_type not in [FileType.Database.value, FileType.CSV.value]: 153 self.send_error(400, "Invalid upload type") 154 return 155 156 # 2. Extract File Data 157 file_item = form_data['file'] # Assuming 'file' is your file input name 158 159 # 3. Get File Details 160 filename = file_item.filename 161 file_data = file_item.file.read() # Read the file's content 162 163 if debug: 164 print(f'Uploaded filename: {filename}') 165 166 # 4. Define Storage Path for CSV 167 upload_directory = "./uploads" # Create this directory if it doesn't exist 168 os.makedirs(upload_directory, exist_ok=True) 169 file_path = os.path.join(upload_directory, upload_type) 170 171 # 5. Write to Disk 172 with open(file_path, 'wb') as f: 173 f.write(file_data) 174 175 match upload_type: 176 case FileType.Database.value: 177 178 try: 179 # 6. Verify database file 180 # ZakatTracker(db_path=file_path) # FATAL, Circular Imports Error 181 if database_callback is not None: 182 database_callback(file_path) 183 184 # 7. Copy database into the original path 185 shutil.copy2(file_path, database_path) 186 except Exception as e: 187 self.send_error(400, str(e)) 188 return 189 190 case FileType.CSV.value: 191 # 6. Verify CSV file 192 try: 193 # x = ZakatTracker(db_path=database_path) # FATAL, Circular Imports Error 194 # result = x.import_csv(file_path, debug=debug) 195 if csv_callback is not None: 196 result = csv_callback(file_path, database_path, debug) 197 if debug: 198 print(f'CSV imported: {result}') 199 if len(result[2]) != 0: 200 self.send_response(200) 201 self.end_headers() 202 self.wfile.write(json.dumps(result).encode()) 203 return 204 except Exception as e: 205 self.send_error(400, str(e)) 206 return 207 208 self.send_response(200) 209 self.end_headers() 210 self.wfile.write(b"File uploaded successfully.") 211 212 httpd = socketserver.TCPServer(("localhost", port), Handler) 213 server_thread = threading.Thread(target=httpd.serve_forever) 214 215 def shutdown_server(): 216 nonlocal httpd, server_thread 217 httpd.shutdown() 218 httpd.server_close() # Close the socket 219 server_thread.join() # Wait for the thread to finish 220 221 return file_name, download_url, upload_url, server_thread, shutdown_server 222 223 224def main(): 225 from zakat_tracker import ZakatTracker, Action # SAFE Circular Imports 226 # Example usage (replace with your file path) 227 file_to_share = f"{uuid.uuid4()}.pickle" # Or any other file type 228 229 def database_callback(file_path): 230 ZakatTracker(db_path=file_path) 231 232 def csv_callback(file_path, database_path, debug): 233 x = ZakatTracker(db_path=database_path) 234 return x.import_csv(file_path, debug=debug) 235 236 file_name, download_url, upload_url, server_thread, shutdown_server = start_file_server( 237 file_to_share, 238 database_callback=database_callback, 239 csv_callback=csv_callback, 240 debug=True, 241 ) 242 243 print(f"\nTo download '{file_name}', use this URL:") 244 print(download_url) 245 246 print(f"\nTo upload a new '{file_name}', use this URL:") 247 print(upload_url) 248 print("(The uploaded file will replace the existing one.)") 249 250 print("\nString the server...") 251 server_thread.start() 252 print("The server started.") 253 254 input("\nPress Enter to stop the server...") 255 shutdown_server() 256 257 258if __name__ == "__main__": 259 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 HTTP 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 pickle 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 class Handler(http.server.SimpleHTTPRequestHandler): 100 def do_GET(self): 101 if self.path == f"/{file_uuid}/get": 102 # GET: Serve the existing file 103 try: 104 with open(database_path, "rb") as f: 105 self.send_response(200) 106 self.send_header("Content-type", "application/octet-stream") 107 self.send_header("Content-Disposition", f'attachment; filename="{file_name}"') 108 self.end_headers() 109 self.wfile.write(f.read()) 110 except FileNotFoundError: 111 self.send_error(404, "File not found") 112 elif self.path == f"/{file_uuid}/upload": 113 # GET: Serve the upload form 114 self.send_response(200) 115 self.send_header("Content-type", "text/html") 116 self.end_headers() 117 self.wfile.write(f""" 118 <html lang="en"> 119 <head> 120 <title>Zakat File Server</title> 121 </head> 122 <body> 123 <h1>Zakat File Server</h1> 124 <h3>You can download the <a target="__blank" href="{download_url}">database file</a>...</h3> 125 <h3>Or upload a new file to restore a database or import `CSV` file:</h3> 126 <form action="/{file_uuid}/upload" method="post" enctype="multipart/form-data"> 127 <input type="file" name="file" required><br/> 128 <input type="radio" id="{FileType.Database.value}" name="upload_type" value="{FileType.Database.value}" required> 129 <label for="database">Database File</label><br/> 130 <input type="radio"id="{FileType.CSV.value}" name="upload_type" value="{FileType.CSV.value}"> 131 <label for="csv">CSV File</label><br/> 132 <input type="submit" value="Upload"><br/> 133 </form> 134 </body></html> 135 """.encode()) 136 else: 137 self.send_error(404) 138 139 def do_POST(self): 140 if self.path == f"/{file_uuid}/upload": 141 # POST: Handle request 142 # 1. Get the Form Data 143 form_data = cgi.FieldStorage( 144 fp=self.rfile, 145 headers=self.headers, 146 environ={'REQUEST_METHOD': 'POST'} 147 ) 148 upload_type = form_data.getvalue("upload_type") 149 150 if debug: 151 print('upload_type', upload_type) 152 153 if upload_type not in [FileType.Database.value, FileType.CSV.value]: 154 self.send_error(400, "Invalid upload type") 155 return 156 157 # 2. Extract File Data 158 file_item = form_data['file'] # Assuming 'file' is your file input name 159 160 # 3. Get File Details 161 filename = file_item.filename 162 file_data = file_item.file.read() # Read the file's content 163 164 if debug: 165 print(f'Uploaded filename: {filename}') 166 167 # 4. Define Storage Path for CSV 168 upload_directory = "./uploads" # Create this directory if it doesn't exist 169 os.makedirs(upload_directory, exist_ok=True) 170 file_path = os.path.join(upload_directory, upload_type) 171 172 # 5. Write to Disk 173 with open(file_path, 'wb') as f: 174 f.write(file_data) 175 176 match upload_type: 177 case FileType.Database.value: 178 179 try: 180 # 6. Verify database file 181 # ZakatTracker(db_path=file_path) # FATAL, Circular Imports Error 182 if database_callback is not None: 183 database_callback(file_path) 184 185 # 7. Copy database into the original path 186 shutil.copy2(file_path, database_path) 187 except Exception as e: 188 self.send_error(400, str(e)) 189 return 190 191 case FileType.CSV.value: 192 # 6. Verify CSV file 193 try: 194 # x = ZakatTracker(db_path=database_path) # FATAL, Circular Imports Error 195 # result = x.import_csv(file_path, debug=debug) 196 if csv_callback is not None: 197 result = csv_callback(file_path, database_path, debug) 198 if debug: 199 print(f'CSV imported: {result}') 200 if len(result[2]) != 0: 201 self.send_response(200) 202 self.end_headers() 203 self.wfile.write(json.dumps(result).encode()) 204 return 205 except Exception as e: 206 self.send_error(400, str(e)) 207 return 208 209 self.send_response(200) 210 self.end_headers() 211 self.wfile.write(b"File uploaded successfully.") 212 213 httpd = socketserver.TCPServer(("localhost", port), Handler) 214 server_thread = threading.Thread(target=httpd.serve_forever) 215 216 def shutdown_server(): 217 nonlocal httpd, server_thread 218 httpd.shutdown() 219 httpd.server_close() # Close the socket 220 server_thread.join() # Wait for the thread to finish 221 222 return file_name, download_url, upload_url, server_thread, shutdown_server
Starts a multi-purpose HTTP 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 pickle 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()
225def main(): 226 from zakat_tracker import ZakatTracker, Action # SAFE Circular Imports 227 # Example usage (replace with your file path) 228 file_to_share = f"{uuid.uuid4()}.pickle" # Or any other file type 229 230 def database_callback(file_path): 231 ZakatTracker(db_path=file_path) 232 233 def csv_callback(file_path, database_path, debug): 234 x = ZakatTracker(db_path=database_path) 235 return x.import_csv(file_path, debug=debug) 236 237 file_name, download_url, upload_url, server_thread, shutdown_server = start_file_server( 238 file_to_share, 239 database_callback=database_callback, 240 csv_callback=csv_callback, 241 debug=True, 242 ) 243 244 print(f"\nTo download '{file_name}', use this URL:") 245 print(download_url) 246 247 print(f"\nTo upload a new '{file_name}', use this URL:") 248 print(upload_url) 249 print("(The uploaded file will replace the existing one.)") 250 251 print("\nString the server...") 252 server_thread.start() 253 print("The server started.") 254 255 input("\nPress Enter to stop the server...") 256 shutdown_server()