Première version non terminer du nouveau système de ticket § ajout du système de boutons persistant (non tester)
This commit is contained in:
parent
10173132a2
commit
93e2f9bb44
15 changed files with 3788 additions and 209 deletions
323
commandes/ticket/data/storage.py
Normal file
323
commandes/ticket/data/storage.py
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
"""
|
||||
Ticket Storage Utility
|
||||
======================
|
||||
Modular storage system for ticket data with caching support.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Any, List
|
||||
from threading import Lock
|
||||
|
||||
from .models import TicketData, GuildTicketConfig, TicketStatus
|
||||
|
||||
|
||||
class TicketStorage:
|
||||
"""
|
||||
Thread-safe storage manager for ticket data.
|
||||
|
||||
Features:
|
||||
- Automatic file creation
|
||||
- In-memory caching with write-through
|
||||
- Backup creation before writes
|
||||
- Data validation on load
|
||||
"""
|
||||
|
||||
def __init__(self, base_path: str = "data/tickets"):
|
||||
self.base_path = base_path
|
||||
self.config_path = os.path.join(base_path, "config.json")
|
||||
self.tickets_path = os.path.join(base_path, "tickets")
|
||||
self.backup_path = os.path.join(base_path, "backups")
|
||||
self.cache: Dict[str, Any] = {}
|
||||
self.cache_lock = Lock()
|
||||
self._ensure_directories()
|
||||
|
||||
def _ensure_directories(self) -> None:
|
||||
"""Create necessary directories"""
|
||||
for path in [self.base_path, self.tickets_path, self.backup_path]:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
def _create_backup(self, file_path: str) -> str:
|
||||
"""Create a timestamped backup of a file"""
|
||||
if not os.path.exists(file_path):
|
||||
return ""
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = os.path.basename(file_path)
|
||||
backup_file = os.path.join(self.backup_path, f"{timestamp}_{filename}")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as src:
|
||||
with open(backup_file, 'w', encoding='utf-8') as dst:
|
||||
dst.write(src.read())
|
||||
return backup_file
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _get_cache_key(self, guild_id: int, key_type: str) -> str:
|
||||
"""Generate a cache key"""
|
||||
return f"{guild_id}_{key_type}"
|
||||
|
||||
# --- Config Methods ---
|
||||
|
||||
def load_config(self, guild_id: int) -> GuildTicketConfig:
|
||||
"""Load guild ticket configuration"""
|
||||
cache_key = self._get_cache_key(guild_id, "config")
|
||||
|
||||
with self.cache_lock:
|
||||
if cache_key in self.cache:
|
||||
return self.cache[cache_key]
|
||||
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = json.load(f)
|
||||
|
||||
config_data = all_configs.get(str(guild_id), {})
|
||||
config = GuildTicketConfig.from_dict(config_data)
|
||||
config.guild_id = guild_id
|
||||
else:
|
||||
config = GuildTicketConfig(guild_id=guild_id)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
print(f"[TicketStorage] Error loading config for guild {guild_id}: {e}")
|
||||
config = GuildTicketConfig(guild_id=guild_id)
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = config
|
||||
|
||||
return config
|
||||
|
||||
def save_config(self, guild_id: int, config: GuildTicketConfig) -> bool:
|
||||
"""Save guild ticket configuration"""
|
||||
cache_key = self._get_cache_key(guild_id, "config")
|
||||
|
||||
try:
|
||||
# Create backup
|
||||
self._create_backup(self.config_path)
|
||||
|
||||
# Load all configs
|
||||
all_configs = {}
|
||||
if os.path.exists(self.config_path):
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
all_configs = json.load(f)
|
||||
|
||||
# Update config
|
||||
all_configs[str(guild_id)] = config.to_dict()
|
||||
|
||||
# Save
|
||||
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_configs, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# Update cache
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = config
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[TicketStorage] Error saving config for guild {guild_id}: {e}")
|
||||
return False
|
||||
|
||||
# --- Ticket Methods ---
|
||||
|
||||
def get_ticket_path(self, channel_id: int) -> str:
|
||||
"""Get the file path for a ticket"""
|
||||
return os.path.join(self.tickets_path, f"{channel_id}.json")
|
||||
|
||||
def load_ticket(self, channel_id: int) -> Optional[TicketData]:
|
||||
"""Load a ticket by channel ID"""
|
||||
cache_key = self._get_cache_key(channel_id, "ticket")
|
||||
|
||||
with self.cache_lock:
|
||||
if cache_key in self.cache:
|
||||
return self.cache[cache_key]
|
||||
|
||||
path = self.get_ticket_path(channel_id)
|
||||
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
ticket = TicketData.from_dict(data)
|
||||
else:
|
||||
return None
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
print(f"[TicketStorage] Error loading ticket {channel_id}: {e}")
|
||||
return None
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = ticket
|
||||
|
||||
return ticket
|
||||
|
||||
def save_ticket(self, ticket: TicketData) -> bool:
|
||||
"""Save a ticket"""
|
||||
cache_key = self._get_cache_key(ticket.channel_id, "ticket")
|
||||
|
||||
try:
|
||||
# Create backup
|
||||
self._create_backup(self.get_ticket_path(ticket.channel_id))
|
||||
|
||||
path = self.get_ticket_path(ticket.channel_id)
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(ticket.to_dict(), f, indent=4, ensure_ascii=False)
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache[cache_key] = ticket
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[TicketStorage] Error saving ticket {ticket.channel_id}: {e}")
|
||||
return False
|
||||
|
||||
def delete_ticket(self, channel_id: int) -> bool:
|
||||
"""Delete a ticket file"""
|
||||
cache_key = self._get_cache_key(channel_id, "ticket")
|
||||
path = self.get_ticket_path(channel_id)
|
||||
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
# Create backup before delete
|
||||
self._create_backup(path)
|
||||
os.remove(path)
|
||||
|
||||
with self.cache_lock:
|
||||
self.cache.pop(cache_key, None)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[TicketStorage] Error deleting ticket {channel_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_guild_tickets(self, guild_id: int, status: Optional[TicketStatus] = None) -> List[TicketData]:
|
||||
"""Get all tickets for a guild, optionally filtered by status"""
|
||||
tickets = []
|
||||
|
||||
if not os.path.exists(self.tickets_path):
|
||||
return tickets
|
||||
|
||||
for filename in os.listdir(self.tickets_path):
|
||||
if not filename.endswith('.json'):
|
||||
continue
|
||||
|
||||
try:
|
||||
channel_id = int(filename.replace('.json', ''))
|
||||
ticket = self.load_ticket(channel_id)
|
||||
|
||||
if ticket and ticket.guild_id == guild_id:
|
||||
if status is None or ticket.status == status:
|
||||
tickets.append(ticket)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return tickets
|
||||
|
||||
def get_user_tickets(self, guild_id: int, user_id: int, open_only: bool = True) -> List[TicketData]:
|
||||
"""Get all tickets for a user"""
|
||||
tickets = self.get_guild_tickets(guild_id)
|
||||
user_tickets = [t for t in tickets if t.user_id == user_id]
|
||||
|
||||
if open_only:
|
||||
user_tickets = [t for t in user_tickets if t.is_open]
|
||||
|
||||
return user_tickets
|
||||
|
||||
def count_user_tickets(self, guild_id: int, user_id: int, category_id: Optional[str] = None) -> int:
|
||||
"""Count tickets for a user, optionally in a specific category"""
|
||||
tickets = self.get_user_tickets(guild_id, user_id, open_only=True)
|
||||
|
||||
if category_id:
|
||||
tickets = [t for t in tickets if t.category_id == category_id]
|
||||
|
||||
return len(tickets)
|
||||
|
||||
# --- Bulk Operations ---
|
||||
|
||||
def get_all_guild_ids(self) -> List[int]:
|
||||
"""Get all guild IDs with ticket configurations"""
|
||||
if not os.path.exists(self.config_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
configs = json.load(f)
|
||||
return [int(gid) for gid in configs.keys()]
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return []
|
||||
|
||||
def cleanup_cache(self, max_size: int = 1000) -> int:
|
||||
"""Clear cache if it exceeds max size"""
|
||||
with self.cache_lock:
|
||||
if len(self.cache) <= max_size:
|
||||
return 0
|
||||
|
||||
# Clear all non-config keys (keep recent)
|
||||
removed = 0
|
||||
keys_to_remove = [k for k in self.cache.keys() if not k.endswith("_config")]
|
||||
|
||||
for key in keys_to_remove[:len(keys_to_remove) - max_size]:
|
||||
del self.cache[key]
|
||||
removed += 1
|
||||
|
||||
return removed
|
||||
|
||||
def invalidate_cache(self, guild_id: Optional[int] = None) -> None:
|
||||
"""Invalidate cache for a guild or all"""
|
||||
with self.cache_lock:
|
||||
if guild_id is None:
|
||||
self.cache.clear()
|
||||
else:
|
||||
keys_to_remove = [k for k in self.cache.keys() if k.startswith(str(guild_id))]
|
||||
for key in keys_to_remove:
|
||||
del self.cache[key]
|
||||
|
||||
# --- Statistics ---
|
||||
|
||||
def get_storage_stats(self) -> Dict[str, Any]:
|
||||
"""Get storage statistics"""
|
||||
stats = {
|
||||
"total_tickets": 0,
|
||||
"total_backups": 0,
|
||||
"config_size_bytes": 0,
|
||||
"cache_size": 0,
|
||||
"tickets_by_status": {}
|
||||
}
|
||||
|
||||
# Count tickets
|
||||
if os.path.exists(self.tickets_path):
|
||||
for filename in os.listdir(self.tickets_path):
|
||||
if filename.endswith('.json'):
|
||||
stats["total_tickets"] += 1
|
||||
|
||||
# Count backups
|
||||
if os.path.exists(self.backup_path):
|
||||
stats["total_backups"] = len(os.listdir(self.backup_path))
|
||||
|
||||
# Config size
|
||||
if os.path.exists(self.config_path):
|
||||
stats["config_size_bytes"] = os.path.getsize(self.config_path)
|
||||
|
||||
# Cache size
|
||||
with self.cache_lock:
|
||||
stats["cache_size"] = len(self.cache)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_storage_instance: Optional[TicketStorage] = None
|
||||
|
||||
|
||||
def get_storage() -> TicketStorage:
|
||||
"""Get the singleton storage instance"""
|
||||
global _storage_instance
|
||||
if _storage_instance is None:
|
||||
_storage_instance = TicketStorage()
|
||||
return _storage_instance
|
||||
|
||||
|
||||
def reset_storage() -> None:
|
||||
"""Reset the storage instance (for testing)"""
|
||||
global _storage_instance
|
||||
_storage_instance = None
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue