aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-12-24 06:50:59 -0500
committerBobby <[email protected]>2024-12-24 06:50:59 -0500
commit6df9f0dc40501e8f55bcc883dfe5be65e60d3c3d (patch)
tree273148d564c11ae46e9f96c0231ce57427f5591f /internal
parent4feba2452a151ed999d52d4a0d53b0b0584bf70e (diff)
downloadthatcomputerscientist-6df9f0dc40501e8f55bcc883dfe5be65e60d3c3d.tar.xz
thatcomputerscientist-6df9f0dc40501e8f55bcc883dfe5be65e60d3c3d.zip
bucket load of things
Diffstat (limited to 'internal')
-rw-r--r--internal/admin_utilities.py145
-rw-r--r--internal/cache_utils.py118
2 files changed, 263 insertions, 0 deletions
diff --git a/internal/admin_utilities.py b/internal/admin_utilities.py
new file mode 100644
index 00000000..c8014fe1
--- /dev/null
+++ b/internal/admin_utilities.py
@@ -0,0 +1,145 @@
+from typing import List, Dict, Union
+from io import BytesIO
+from uuid import uuid4
+from django.conf import settings
+from minio import Minio
+from minio.error import S3Error
+
+
+class MinioFileManager:
+ def __init__(self):
+ """Initialize MinIO client with settings credentials."""
+ self.client = Minio(
+ endpoint=settings.MINIO_ENDPOINT,
+ access_key=settings.MINIO_ACCESS_KEY,
+ secret_key=settings.MINIO_SECRET_KEY,
+ secure=getattr(settings, "MINIO_SECURE", True),
+ )
+
+ def list_buckets(self) -> List[Dict[str, str]]:
+ """List all buckets with creation dates."""
+ try:
+ buckets = self.client.list_buckets()
+ return [
+ {
+ "name": bucket.name,
+ "created": (
+ bucket.creation_date.isoformat()
+ if bucket.creation_date
+ else None
+ ),
+ }
+ for bucket in buckets
+ ]
+ except S3Error:
+ return []
+
+ def list_objects(
+ self, bucket: str, prefix: str = ""
+ ) -> List[Dict[str, Union[str, bool]]]:
+ """List objects in a directory within bucket."""
+ try:
+ if not self.client.bucket_exists(bucket):
+ return []
+
+ # Ensure prefix ends with slash for directory listing
+ prefix = prefix.rstrip("/") + "/" if prefix else ""
+ objects = self.client.list_objects(bucket, prefix=prefix, recursive=False)
+
+ result = []
+ seen_prefixes = set()
+
+ for obj in objects:
+ # Skip the directory object itself
+ if obj.object_name == prefix:
+ continue
+
+ # Get relative path from prefix
+ name = obj.object_name[len(prefix) :]
+ if not name:
+ continue
+
+ # Handle nested directories
+ if "/" in name:
+ dir_name = name.split("/")[0] + "/"
+ if dir_name not in seen_prefixes:
+ seen_prefixes.add(dir_name)
+ result.append(
+ {
+ "name": dir_name.rstrip("/"),
+ "path": (prefix + dir_name).rstrip("/"),
+ "type": "folder",
+ "size": 0,
+ "last_modified": None,
+ }
+ )
+ else:
+ result.append(
+ {
+ "name": name,
+ "path": obj.object_name,
+ "type": "file",
+ "size": obj.size,
+ "last_modified": (
+ obj.last_modified.isoformat()
+ if obj.last_modified
+ else None
+ ),
+ }
+ )
+
+ return result
+ except S3Error as e:
+ print(f"MinIO Error: {e}")
+ return []
+
+ def create_folder(self, bucket: str, folder_path: str) -> bool:
+ """Create a folder in the specified bucket."""
+ try:
+ if not self.client.bucket_exists(bucket):
+ self.client.make_bucket(bucket)
+
+ folder_path = folder_path.rstrip("/") + "/"
+ self.client.put_object(bucket, folder_path, BytesIO(b""), 0)
+ return True
+ except S3Error:
+ return False
+
+ def upload_file(self, bucket: str, file_path: str, object_name: str = None) -> bool:
+ """Upload a file to MinIO."""
+ try:
+ if not self.client.bucket_exists(bucket):
+ self.client.make_bucket(bucket)
+
+ object_name = object_name or str(uuid4())
+ self.client.fput_object(bucket, object_name, file_path)
+ return True
+ except S3Error:
+ return False
+
+ def delete_object(
+ self, bucket: str, object_path: str, recursive: bool = False
+ ) -> bool:
+ """Delete a file or folder from bucket."""
+ try:
+ if recursive and not object_path.endswith("/"):
+ object_path += "/"
+ objects = self.client.list_objects(
+ bucket, prefix=object_path, recursive=True
+ )
+ for obj in objects:
+ self.client.remove_object(bucket, obj.object_name)
+
+ self.client.remove_object(bucket, object_path)
+ return True
+ except S3Error:
+ return False
+
+ def generate_presigned_url(
+ self, bucket: str, object_name: str, expiry: int = 3600
+ ) -> str:
+ """Generate a presigned URL for file access."""
+ try:
+ return self.client.presigned_get_object(bucket, object_name, expiry)
+ except S3Error:
+ return ""
diff --git a/internal/cache_utils.py b/internal/cache_utils.py
new file mode 100644
index 00000000..7be70e57
--- /dev/null
+++ b/internal/cache_utils.py
@@ -0,0 +1,118 @@
+from django.core.cache import cache
+from django_redis import get_redis_connection
+from functools import wraps
+import hashlib
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def generate_cache_key(*args, prefix="", **kwargs):
+ """Generate a unique cache key based on args and kwargs"""
+ # Sort kwargs to ensure consistent key generation
+ sorted_kwargs = sorted(kwargs.items())
+
+ # Convert all arguments to strings and join them
+ key_parts = [str(arg) for arg in args] + [f"{k}:{v}" for k, v in sorted_kwargs]
+ key_string = ":".join(key_parts)
+
+ # Create an MD5 hash of the key string to ensure safe key length
+ hash_object = hashlib.md5(key_string.encode())
+ hashed_key = hash_object.hexdigest()
+
+ return f"{prefix}:{hashed_key}" if prefix else hashed_key
+
+
+def safe_cache_get(key):
+ """Safely get data from cache with error handling"""
+ try:
+ return cache.get(key)
+ except Exception as e:
+ logger.error(f"Cache get error for key {key}: {str(e)}")
+ return None
+
+
+def safe_cache_set(key, value, timeout=None):
+ """Safely set data in cache with error handling"""
+ try:
+ cache.set(key, value, timeout)
+ return True
+ except Exception as e:
+ logger.error(f"Cache set error for key {key}: {str(e)}")
+ return False
+
+
+def cache_data(prefix, timeout=60 * 15):
+ """
+ Generic cache decorator that can be used for any function.
+
+ Args:
+ prefix (str): Prefix for the cache key (e.g., 'anime_data', 'streaming_data')
+ timeout (int): Cache timeout in seconds
+ """
+
+ def decorator(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ # Generate cache key using all arguments
+ cache_key = generate_cache_key(*args, prefix=prefix, **kwargs)
+
+ # Try to get from cache
+ cached_data = safe_cache_get(cache_key)
+ if cached_data is not None:
+ return cached_data
+
+ try:
+ # Get fresh data
+ result = func(*args, **kwargs)
+ # Cache the result
+ safe_cache_set(cache_key, result, timeout)
+ return result
+ except Exception as e:
+ logger.error(
+ f"Error in {prefix} for args {args}, kwargs {kwargs}: {str(e)}"
+ )
+ # On error, return fresh data without caching
+ return func(*args, **kwargs)
+
+ return wrapper
+
+ return decorator
+
+
+def clear_cache(pattern=None, prefix=None):
+ """
+ Clear cache entries based on a pattern or prefix.
+
+ Args:
+ pattern (str): Direct Redis key pattern to match (e.g., '*anime_id*')
+ prefix (str): Cache prefix to clear (e.g., 'anime_data')
+ """
+ try:
+ redis_conn = get_redis_connection("default")
+
+ if not pattern and not prefix:
+ # Clear all known prefixes if neither pattern nor prefix specified
+ prefixes = ["anime_data:*", "streaming_data:*", "search_results:*"]
+ total_cleared = 0
+ for p in prefixes:
+ keys = redis_conn.keys(p)
+ if keys:
+ redis_conn.delete(*keys)
+ total_cleared += len(keys)
+ logger.info(f"Cleared {len(keys)} entries for pattern {p}")
+ return total_cleared
+
+ if prefix:
+ pattern = f"{prefix}:*"
+
+ keys = redis_conn.keys(pattern)
+ if keys:
+ redis_conn.delete(*keys)
+ logger.info(f"Cleared {len(keys)} cache entries matching {pattern}")
+ return len(keys)
+ return 0
+
+ except Exception as e:
+ logger.error(f"Error clearing cache with pattern {pattern}: {str(e)}")
+ return 0