2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
14# CREATION DATE: 11-10-2025
15# LAST Modified: 17:56:8 09-01-2026
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: File in charge of providing a boiled down interface for interracting with an s3 bucket.
22# +==== END CatFeeder =================+
25from __future__
import annotations
27from typing
import List, Union, Dict, Any, Optional, cast
29from botocore.client
import Config
30from botocore.exceptions
import BotoCoreError, ClientError
31from display_tty
import Disp, initialise_logger
32from .bucket_class_aliases
import S3ObjectLike, S3BucketLike, S3ServiceResourceLike
33from .
import bucket_constants
as CONST
34from ..core
import FinalClass
39 Class to manage interaction with an S3-compatible bucket like MinIO.
42 disp (Disp): Logger instance for debugging and error reporting.
43 connection (Optional[S3ServiceResourceLike]): Active S3 connection resource.
44 debug (bool): Debug mode flag.
45 success (int): Numeric success code.
46 error (int): Numeric error code.
49 disp: Disp = initialise_logger(__qualname__,
False)
52 _initialization_attempted: bool =
False
53 _initialization_failed: bool =
False
55 def __init__(self, error: int = 84, success: int = 0, auto_connect: bool =
True, debug: bool =
False) ->
None:
56 """Initialize the Bucket instance.
59 error (int, optional): Numeric error code. Defaults to 84.
60 success (int, optional): Numeric success code. Defaults to 0.
61 auto_connect (bool, optional): If True, automatically connect to S3. Defaults to True.
62 debug (bool, optional): Enable debug logging. Defaults to False.
65 self.
disp.update_disp_debug(debug)
66 self.
disp.log_debug(
"Initialising...")
80 self.
disp.log_debug(
"Initialised")
83 """Automatically connect to S3 service.
85 Called during __init__ when auto_connect=True.
86 Tracks connection attempts to provide better error messages.
88 Bucket._initialization_attempted =
True
91 Bucket._initialization_failed =
True
92 msg =
"Failed to connect to S3/MinIO service."
93 self.
disp.log_error(msg)
97 except (BotoCoreError, ClientError, ConnectionError, RuntimeError)
as e:
98 Bucket._initialization_failed =
True
99 self.
disp.log_error(f
"Connection failed: {e}")
103 """Best-effort cleanup invoked when the instance is garbage-collected.
105 This releases the S3 connection resource.
110 except (BotoCoreError, ClientError, ConnectionError, RuntimeError, AttributeError):
115 """Ensure the Bucket instance is connected to S3.
117 If connection was never attempted, try to auto-connect.
118 If connection was attempted but failed, try to reconnect once.
121 RuntimeError: If connection has failed and cannot be re-established.
126 if not Bucket._initialization_attempted:
128 "Attempting lazy connection",
"_ensure_connected")
132 self.
disp.log_debug(
"Attempting to reconnect",
"_ensure_connected")
135 Bucket._initialization_failed =
False
138 "Bucket connection failed. Cannot perform operations without active S3 connection."
143 Connect to the S3-compatible service (MinIO, Cellar, AWS S3, etc.).
146 int: success or error code.
150 endpoint_url = CONST.BUCKET_HOST
153 if not endpoint_url.startswith((
'http://',
'https://')):
154 endpoint_url = f
"http://{endpoint_url}"
157 if CONST.BUCKET_PORT:
158 endpoint_url = f
"{endpoint_url}:{CONST.BUCKET_PORT}"
161 resource = boto3.resource(
162 CONST.BUCKET_RESSOURCE_TYPE,
163 endpoint_url=endpoint_url,
164 aws_access_key_id=CONST.BUCKET_USER,
165 aws_secret_access_key=CONST.BUCKET_PASSWORD,
166 config=Config(signature_version=CONST.BUCKET_SIGNATURE_VERSION)
168 self.
connection = cast(S3ServiceResourceLike, resource)
171 assert conn
is not None
172 conn.meta.client.list_buckets()
174 "Connection to S3-compatible service successful.")
176 except (BotoCoreError, ClientError)
as e:
178 f
"Failed to connect to S3-compatible service: {str(e)}"
184 Check if the connection to the S3-compatible service is active.
187 bool: True if connected, False otherwise.
190 self.
disp.log_error(
"No connection object found.")
196 self.
disp.log_info(
"Connection is active.")
198 except (BotoCoreError, ClientError, ConnectionError)
as e:
200 f
"Connection check failed: {str(e)}"
206 Disconnect from the S3-compatible service by setting the connection to None.
209 int: success or error code.
212 self.
disp.log_warning(
213 "No active connection to disconnect."
220 "Disconnected from the S3-compatible service."
226 Retrieve a list of all bucket names.
229 Union[List[str], int]: A list of bucket names or error code.
234 raise ConnectionError(
"No connection established.")
235 buckets = [bucket.name
for bucket
in self.
connection.buckets.all()]
237 except (BotoCoreError, ClientError, ConnectionError)
as e:
239 f
"Error fetching bucket names: {str(e)}"
248 bucket_name (str): Name of the bucket to create.
251 int: success or error code.
256 raise ConnectionError(
"No connection established.")
259 f
"Bucket '{bucket_name}' created successfully."
262 except (BotoCoreError, ClientError, ConnectionError)
as e:
264 f
"Failed to create bucket '{bucket_name}': {str(e)}"
268 def upload_file(self, bucket_name: str, file_path: str, key_name: Optional[str] =
None) -> int:
270 Upload a file to the specified bucket.
273 bucket_name (str): Name of the target bucket.
274 file_path (str): Path of the file to upload.
275 key_name (Optional[str]): Name to save the file as in the bucket. Defaults to the file path name.
278 int: success or error code.
280 key_name = key_name
or file_path
284 raise ConnectionError(
"No connection established.")
287 bucket.upload_file(file_path, key_name)
288 msg = f
"File '{file_path}' uploaded to bucket "
289 msg += f
"'{bucket_name}' as '{key_name}'."
290 self.
disp.log_info(msg)
292 except (BotoCoreError, ClientError, ConnectionError)
as e:
293 msg = f
"Failed to upload file '{file_path}' to bucket "
294 msg += f
"'{bucket_name}': {str(e)}"
295 self.
disp.log_error(msg)
298 def upload_stream(self, bucket_name: str, data: bytes, key_name: Optional[str] =
None) -> int:
299 """Upload a file to the specified bucket from a byte stream.
302 bucket_name (str): Name of the target bucket.
303 data (bytes): The file content as bytes.
304 key_name (Optional[str]): Name to save the file as in the bucket. Defaults to a generated name if not provided.
307 int: success or error code.
311 "key_name must be provided for stream uploads")
316 raise ConnectionError(
"No connection established.")
319 bucket.put_object(Key=key_name, Body=data)
320 msg = f
"File stream uploaded to bucket '{bucket_name}' as '{key_name}' ({len(data)} bytes)."
321 self.
disp.log_info(msg)
323 except (BotoCoreError, ClientError, ConnectionError, RuntimeError)
as e:
324 msg = f
"Failed to upload file stream to bucket '{bucket_name}' as '{key_name}': {str(e)}"
325 self.
disp.log_error(msg)
328 def download_file(self, bucket_name: str, key_name: str, destination_path: Optional[str] =
None) -> int:
330 Download a file from the specified bucket.
333 bucket_name (str): Name of the target bucket.
334 key_name (str): Name of the file to download.
335 destination_path (Optional[str]): Local path where the file will be saved. Defaults to the key_name if not provided.
338 int: success or error code.
340 destination_path = destination_path
or key_name
344 raise ConnectionError(
"No connection established.")
347 bucket.download_file(key_name, destination_path)
348 msg = f
"File '{key_name}' downloaded from bucket "
349 msg += f
"'{bucket_name}' to '{destination_path}'."
350 self.
disp.log_info(msg)
352 except (BotoCoreError, ClientError, ConnectionError)
as e:
353 msg = f
"Failed to download file '{key_name}'"
354 msg += f
" from bucket '{bucket_name}': {str(e)}"
355 self.
disp.log_error(msg)
359 """Download a file from the specified bucket and return it as a byte stream.
362 bucket_name (str): Name of the target bucket.
363 key_name (str): Name of the file to download.
366 Union[bytes, int]: File content as bytes or error code.
371 raise ConnectionError(
"No connection established.")
374 obj: S3ObjectLike = bucket.Object(
376 file_data = obj.get()[
'Body'].read()
377 msg = f
"File '{key_name}' downloaded from bucket "
378 msg += f
"'{bucket_name}' as stream ({len(file_data)} bytes)."
379 self.
disp.log_info(msg)
381 except (BotoCoreError, ClientError, ConnectionError, RuntimeError)
as e:
382 msg = f
"Failed to download file '{key_name}' from bucket "
383 msg += f
"'{bucket_name}' as stream: {str(e)}"
384 self.
disp.log_error(msg)
389 Delete a file from the specified bucket.
392 bucket_name (str): Name of the bucket.
393 key_name (str): Name of the file to delete.
396 int: success or error code.
401 raise ConnectionError(
"No connection established.")
404 bucket.Object(key_name).delete()
406 f
"File '{key_name}' deleted from bucket '{bucket_name}'."
409 except (BotoCoreError, ClientError, ConnectionError)
as e:
410 msg = f
"Failed to delete file '{key_name}' from bucket "
411 msg += f
"'{bucket_name}': {str(e)}"
412 self.
disp.log_error(msg)
420 bucket_name (str): Name of the bucket to delete.
423 int: success or error code.
428 raise ConnectionError(
"No connection established.")
433 f
"Bucket '{bucket_name}' deleted successfully."
436 except (BotoCoreError, ClientError, ConnectionError)
as e:
438 f
"Failed to delete bucket '{bucket_name}': {str(e)}"
444 List all files in the specified bucket.
447 bucket_name (str): Name of the bucket.
450 Union[List[str], int]: List of file names or error code.
455 raise ConnectionError(
"No connection established.")
456 files: List[str] = []
459 for obj
in bucket.objects.all():
460 files.append(obj.key)
462 except (BotoCoreError, ClientError, ConnectionError)
as e:
463 msg = f
"Failed to retrieve files from bucket '{bucket_name}'"
465 self.
disp.log_error(msg)
468 def get_bucket_file(self, bucket_name: str, key_name: str) -> Union[Dict[str, Any], int]:
470 Get information about a specific file in the bucket.
473 bucket_name (str): Name of the bucket.
474 key_name (str): Name of the file.
477 Union[Dict[str, Any], int]: File metadata (path and size) or error code.
482 raise ConnectionError(
"No connection established.")
485 obj: S3ObjectLike = bucket.Object(
487 return {
'file_path': key_name,
'file_size': obj.content_length}
488 except (BotoCoreError, ClientError, ConnectionError, RuntimeError)
as e:
489 msg = f
"Failed to get file '{key_name}' "
490 msg += f
"from bucket '{bucket_name}': {str(e)}"
491 self.
disp.log_error(msg)
Union[List[str], int] get_bucket_files(self, str bucket_name)
int download_file(self, str bucket_name, str key_name, Optional[str] destination_path=None)
int delete_bucket(self, str bucket_name)
int delete_file(self, str bucket_name, str key_name)
int upload_file(self, str bucket_name, str file_path, Optional[str] key_name=None)
None _ensure_connected(self)
Union[bytes, int] download_stream(self, str bucket_name, str key_name)
Union[List[str], int] get_bucket_names(self)
int upload_stream(self, str bucket_name, bytes data, Optional[str] key_name=None)
Union[Dict[str, Any], int] get_bucket_file(self, str bucket_name, str key_name)
None __init__(self, int error=84, int success=0, bool auto_connect=True, bool debug=False)
int create_bucket(self, str bucket_name)
Optional[S3ServiceResourceLike] connection