henaiv2 / user_storage.py
joashsam's picture
Create user_storage.py
5eb6aee verified
Raw
History Blame Contribute Delete
17.5 kB
"""
user_storage.py - User-Specific Storage Management
Handles all file operations with user isolation
"""
import os
import json
import shutil
from pathlib import Path
from datetime import datetime
import uuid
import logging
logger = logging.getLogger(__name__)
# ===========================================
# CONFIGURATION
# ===========================================
# Base directory for all user data
# On HF Spaces, use /tmp for ephemeral storage (or persistent storage if mounted)
DATA_ROOT = os.environ.get('USER_DATA_ROOT', '/data/users')
# Ensure data root exists
os.makedirs(DATA_ROOT, exist_ok=True)
# ===========================================
# PATH HELPERS
# ===========================================
def get_user_dir(user_id):
"""Get the base directory for a user"""
user_dir = os.path.join(DATA_ROOT, str(user_id))
os.makedirs(user_dir, exist_ok=True)
return user_dir
def get_conversations_path(user_id):
"""Get path to user's conversations file"""
return os.path.join(get_user_dir(user_id), 'conversations.json')
def get_workspace_path(user_id):
"""Get path to user's workspace file"""
return os.path.join(get_user_dir(user_id), 'workspace.json')
def get_user_profile_path(user_id):
"""Get path to user's profile file"""
return os.path.join(get_user_dir(user_id), 'profile.json')
def get_user_uploads_dir(user_id):
"""Get path to user's uploads directory"""
uploads_dir = os.path.join(get_user_dir(user_id), 'uploads')
os.makedirs(uploads_dir, exist_ok=True)
return uploads_dir
def get_user_generated_images_dir(user_id):
"""Get path to user's generated images directory"""
images_dir = os.path.join(get_user_dir(user_id), 'generated_images')
os.makedirs(images_dir, exist_ok=True)
return images_dir
def get_user_documents_dir(user_id):
"""Get path to user's documents directory"""
docs_dir = os.path.join(get_user_dir(user_id), 'documents')
os.makedirs(docs_dir, exist_ok=True)
return docs_dir
# ===========================================
# CONVERSATIONS
# ===========================================
def load_conversations(user_id):
"""
Load user's conversations from JSON file.
Returns empty dict if file doesn't exist.
"""
conv_path = get_conversations_path(user_id)
if not os.path.exists(conv_path):
return {}
try:
with open(conv_path, 'r', encoding='utf-8') as f:
conversations = json.load(f)
# Ensure all conversation objects have required fields
for conv_id, conv_data in conversations.items():
if 'versions' not in conv_data:
conv_data['versions'] = {}
if 'current_version_index' not in conv_data:
conv_data['current_version_index'] = {}
if 'branch_root' not in conv_data:
conv_data['branch_root'] = None
if 'version_branches' not in conv_data:
conv_data['version_branches'] = {}
if 'pending_attachments' not in conv_data:
conv_data['pending_attachments'] = []
return conversations
except Exception as e:
logger.error(f"Error loading conversations for user {user_id}: {e}")
return {}
def save_conversations(user_id, conversations):
"""
Save user's conversations to JSON file.
"""
conv_path = get_conversations_path(user_id)
try:
with open(conv_path, 'w', encoding='utf-8') as f:
json.dump(conversations, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
logger.error(f"Error saving conversations for user {user_id}: {e}")
return False
def get_conversation(user_id, conversation_id):
"""Get a single conversation by ID"""
conversations = load_conversations(user_id)
return conversations.get(conversation_id)
def save_conversation(user_id, conversation_id, conversation_data):
"""Save a single conversation"""
conversations = load_conversations(user_id)
conversations[conversation_id] = conversation_data
return save_conversations(user_id, conversations)
def delete_conversation(user_id, conversation_id):
"""Delete a conversation"""
conversations = load_conversations(user_id)
if conversation_id in conversations:
del conversations[conversation_id]
return save_conversations(user_id, conversations)
return False
def get_user_conversations_list(user_id):
"""Get list of conversations (id, title, last_updated) for sidebar"""
conversations = load_conversations(user_id)
conv_list = []
for conv_id, conv_data in conversations.items():
conv_list.append({
'id': conv_id,
'title': conv_data.get('title', 'New Chat'),
'message_count': len(conv_data.get('messages', [])),
'last_updated': conv_data.get('last_updated', datetime.now().isoformat())
})
# Sort by last_updated descending (newest first)
conv_list.sort(key=lambda x: x['last_updated'], reverse=True)
return conv_list
# ===========================================
# WORKSPACE
# ===========================================
def load_workspace(user_id):
"""
Load user's workspace structure from file.
Returns default workspace structure if file doesn't exist.
"""
workspace_path = get_workspace_path(user_id)
if not os.path.exists(workspace_path):
return {"folders": []}
try:
with open(workspace_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading workspace for user {user_id}: {e}")
return {"folders": []}
def save_workspace(user_id, workspace):
"""
Save user's workspace structure to file.
"""
workspace_path = get_workspace_path(user_id)
try:
with open(workspace_path, 'w', encoding='utf-8') as f:
json.dump(workspace, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
logger.error(f"Error saving workspace for user {user_id}: {e}")
return False
def get_file_content(user_id, file_path):
"""
Get content of a specific file in user's workspace.
file_path is relative to workspace root.
"""
workspace = load_workspace(user_id)
parts = file_path.split('/')
file_name = parts[-1]
folder_parts = parts[:-1] if len(parts) > 1 else []
# Navigate to the correct folder
current = workspace
for folder_name in folder_parts:
found = False
for folder in current.get('folders', []):
if folder.get('name') == folder_name:
current = folder
found = True
break
if not found:
return None
# Find the file
for file_item in current.get('files', []):
if file_item.get('name') == file_name:
return file_item.get('content', '')
return None
def save_file_content(user_id, file_path, content):
"""
Save content to a specific file in user's workspace.
"""
workspace = load_workspace(user_id)
parts = file_path.split('/')
file_name = parts[-1]
folder_parts = parts[:-1] if len(parts) > 1 else []
# Navigate to the correct folder
current = workspace
for folder_name in folder_parts:
found = False
for folder in current.get('folders', []):
if folder.get('name') == folder_name:
current = folder
found = True
break
if not found:
return False
# Find and update the file
for file_item in current.get('files', []):
if file_item.get('name') == file_name:
file_item['content'] = content
file_item['updated_at'] = datetime.now().isoformat()
return save_workspace(user_id, workspace)
return False
def delete_file(user_id, file_path):
"""
Delete a file from user's workspace.
"""
workspace = load_workspace(user_id)
parts = file_path.split('/')
file_name = parts[-1]
folder_parts = parts[:-1] if len(parts) > 1 else []
# Navigate to the correct folder
current = workspace
for folder_name in folder_parts:
found = False
for folder in current.get('folders', []):
if folder.get('name') == folder_name:
current = folder
found = True
break
if not found:
return False
# Find and delete the file
files = current.get('files', [])
for i, file_item in enumerate(files):
if file_item.get('name') == file_name:
del files[i]
return save_workspace(user_id, workspace)
return False
# ===========================================
# USER PROFILE
# ===========================================
def load_user_profile(user_id):
"""
Load user's profile information.
"""
profile_path = get_user_profile_path(user_id)
if not os.path.exists(profile_path):
# Return default profile
return {
'user_id': user_id,
'username': f'user_{user_id[:8]}',
'email': '',
'avatar': None,
'theme': 'dark',
'created_at': datetime.now().isoformat(),
'last_active': datetime.now().isoformat()
}
try:
with open(profile_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading profile for user {user_id}: {e}")
return {'user_id': user_id, 'username': f'user_{user_id[:8]}'}
def save_user_profile(user_id, profile):
"""
Save user's profile information.
"""
profile_path = get_user_profile_path(user_id)
try:
with open(profile_path, 'w', encoding='utf-8') as f:
json.dump(profile, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
logger.error(f"Error saving profile for user {user_id}: {e}")
return False
def update_user_last_active(user_id):
"""Update user's last active timestamp"""
profile = load_user_profile(user_id)
profile['last_active'] = datetime.now().isoformat()
return save_user_profile(user_id, profile)
# ===========================================
# IMAGES (Generated)
# ===========================================
def save_generated_image(user_id, image_data, filename):
"""
Save a generated image for a user.
Returns the full path to the saved image.
"""
user_images_dir = get_user_generated_images_dir(user_id)
filepath = os.path.join(user_images_dir, filename)
with open(filepath, 'wb') as f:
f.write(image_data)
return filepath
def get_generated_image_path(user_id, filename):
"""
Get the full path to a user's generated image.
Returns None if file doesn't exist.
"""
user_images_dir = get_user_generated_images_dir(user_id)
filepath = os.path.join(user_images_dir, filename)
if os.path.exists(filepath):
return filepath
return None
def delete_generated_image(user_id, filename):
"""Delete a user's generated image"""
user_images_dir = get_user_generated_images_dir(user_id)
filepath = os.path.join(user_images_dir, filename)
if os.path.exists(filepath):
os.remove(filepath)
return True
return False
def list_user_generated_images(user_id):
"""List all generated images for a user"""
user_images_dir = get_user_generated_images_dir(user_id)
if not os.path.exists(user_images_dir):
return []
images = []
for filename in os.listdir(user_images_dir):
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
filepath = os.path.join(user_images_dir, filename)
images.append({
'filename': filename,
'path': filepath,
'size': os.path.getsize(filepath),
'created_at': datetime.fromtimestamp(os.path.getctime(filepath)).isoformat()
})
# Sort by creation date (newest first)
images.sort(key=lambda x: x['created_at'], reverse=True)
return images
# ===========================================
# UPLOADS
# ===========================================
def save_uploaded_file(user_id, file_data, filename):
"""
Save an uploaded file for a user.
Returns the full path to the saved file.
"""
user_uploads_dir = get_user_uploads_dir(user_id)
# Ensure unique filename
base, ext = os.path.splitext(filename)
counter = 1
filepath = os.path.join(user_uploads_dir, filename)
while os.path.exists(filepath):
filepath = os.path.join(user_uploads_dir, f"{base}_{counter}{ext}")
counter += 1
with open(filepath, 'wb') as f:
f.write(file_data)
return filepath
def get_uploaded_file_path(user_id, filename):
"""Get the full path to a user's uploaded file"""
user_uploads_dir = get_user_uploads_dir(user_id)
filepath = os.path.join(user_uploads_dir, filename)
if os.path.exists(filepath):
return filepath
return None
def delete_uploaded_file(user_id, filename):
"""Delete a user's uploaded file"""
user_uploads_dir = get_user_uploads_dir(user_id)
filepath = os.path.join(user_uploads_dir, filename)
if os.path.exists(filepath):
os.remove(filepath)
return True
return False
# ===========================================
# DATA MIGRATION (from legacy files)
# ===========================================
def migrate_legacy_data(user_id, legacy_conversations_path, legacy_workspace_path=None):
"""
Migrate existing legacy JSON data to a user account.
Use this when converting from single-user to multi-user.
Args:
user_id: The user ID to assign the data to
legacy_conversations_path: Path to old conversations.json
legacy_workspace_path: Path to old workspace.json (optional)
"""
migrated = {'conversations': 0, 'workspace': False}
# Migrate conversations
if os.path.exists(legacy_conversations_path):
try:
with open(legacy_conversations_path, 'r', encoding='utf-8') as f:
legacy_conversations = json.load(f)
# Save to user's conversations
if save_conversations(user_id, legacy_conversations):
migrated['conversations'] = len(legacy_conversations)
logger.info(f"Migrated {len(legacy_conversations)} conversations to user {user_id}")
except Exception as e:
logger.error(f"Error migrating conversations: {e}")
# Migrate workspace
if legacy_workspace_path and os.path.exists(legacy_workspace_path):
try:
with open(legacy_workspace_path, 'r', encoding='utf-8') as f:
legacy_workspace = json.load(f)
if save_workspace(user_id, legacy_workspace):
migrated['workspace'] = True
logger.info(f"Migrated workspace to user {user_id}")
except Exception as e:
logger.error(f"Error migrating workspace: {e}")
return migrated
def get_user_storage_stats(user_id):
"""Get storage statistics for a user"""
stats = {
'conversations': 0,
'workspace_files': 0,
'uploaded_files': 0,
'generated_images': 0,
'total_size_bytes': 0
}
# Count conversations
conversations = load_conversations(user_id)
stats['conversations'] = len(conversations)
# Count workspace files
workspace = load_workspace(user_id)
def count_files(node):
count = 0
for folder in node.get('folders', []):
count += count_files(folder)
count += len(node.get('files', []))
return count
stats['workspace_files'] = count_files(workspace)
# Count uploaded files
user_uploads_dir = get_user_uploads_dir(user_id)
if os.path.exists(user_uploads_dir):
files = [f for f in os.listdir(user_uploads_dir) if os.path.isfile(os.path.join(user_uploads_dir, f))]
stats['uploaded_files'] = len(files)
for f in files:
stats['total_size_bytes'] += os.path.getsize(os.path.join(user_uploads_dir, f))
# Count generated images
stats['generated_images'] = len(list_user_generated_images(user_id))
return stats
# ===========================================
# CLEANUP UTILITIES
# ===========================================
def delete_user_data(user_id):
"""
Delete all data for a user (admin function).
"""
user_dir = get_user_dir(user_id)
if os.path.exists(user_dir):
shutil.rmtree(user_dir)
logger.info(f"Deleted all data for user {user_id}")
return True
return False
def get_all_user_ids():
"""Get list of all user IDs with data"""
if not os.path.exists(DATA_ROOT):
return []
user_ids = []
for item in os.listdir(DATA_ROOT):
item_path = os.path.join(DATA_ROOT, item)
if os.path.isdir(item_path):
user_ids.append(item)
return user_ids