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

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

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