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()
@enum.unique
class FileType(enum.Enum):
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').
Database = <FileType.Database: 'db'>
CSV = <FileType.CSV: 'csv'>
@enum.unique
class Action(enum.Enum):
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').
CREATE = <Action.CREATE: 'CREATE'>
TRACK = <Action.TRACK: 'TRACK'>
LOG = <Action.LOG: 'LOG'>
SUBTRACT = <Action.SUBTRACT: 'SUBTRACT'>
ADD_FILE = <Action.ADD_FILE: 'ADD_FILE'>
REMOVE_FILE = <Action.REMOVE_FILE: 'REMOVE_FILE'>
BOX_TRANSFER = <Action.BOX_TRANSFER: 'BOX_TRANSFER'>
EXCHANGE = <Action.EXCHANGE: 'EXCHANGE'>
REPORT = <Action.REPORT: 'REPORT'>
ZAKAT = <Action.ZAKAT: 'ZAKAT'>
def find_available_port() -> int:
 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}")
def start_file_server( database_path: str, database_callback: Optional[<built-in function callable>] = None, csv_callback: Optional[<built-in function callable>] = None, debug: bool = False) -> tuple:
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:

  1. GET /{file_uuid}/get: Download the database file specified by database_path.
  2. GET /{file_uuid}/upload: Display an HTML form for uploading files.
  3. 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()
def main():
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()