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()
class FileType(enum.Enum):
13class FileType(Enum):
14    Database = 'db'
15    CSV = 'csv'
Database = <FileType.Database: 'db'>
CSV = <FileType.CSV: 'csv'>
class Action(enum.Enum):
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()
CREATE = <Action.CREATE: 1>
TRACK = <Action.TRACK: 2>
LOG = <Action.LOG: 3>
SUB = <Action.SUB: 4>
ADD_FILE = <Action.ADD_FILE: 5>
REMOVE_FILE = <Action.REMOVE_FILE: 6>
BOX_TRANSFER = <Action.BOX_TRANSFER: 7>
EXCHANGE = <Action.EXCHANGE: 8>
REPORT = <Action.REPORT: 9>
ZAKAT = <Action.ZAKAT: 10>
def find_available_port() -> int:
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}")

def start_file_server( database_path: str, database_callback: <built-in function callable> = None, csv_callback: <built-in function callable> = None, debug: bool = False) -> tuple:
 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:

  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.

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()

def main():
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()