Coverage for session_buddy / backends / local_backend.py: 18.69%
89 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
1"""Local file system session storage backend.
3**DEPRECATED**: This module is deprecated and will be removed in v1.0.
4Use ServerlessStorageAdapter with backend="file" instead, which uses ACB's
5native file storage adapter with async support and better performance.
7Migration:
8 Old: LocalFileStorage(config)
9 New: ServerlessStorageAdapter(config, backend="file")
11This module provides a local file system implementation of the SessionStorage interface
12for storing and retrieving session state in local files (useful for development/testing).
13"""
15from __future__ import annotations
17import gzip
18import json
19import warnings
20from datetime import datetime
21from pathlib import Path
22from typing import Any
24from session_buddy.backends.base import SessionState, SessionStorage
27class LocalFileStorage(SessionStorage):
28 """Local file-based session storage (for development/testing).
30 .. deprecated:: 0.9.3
31 LocalFileStorage is deprecated. Use ``ServerlessStorageAdapter(backend="file")``
32 which provides async file operations and better performance via ACB.
34 """
36 def __init__(self, config: dict[str, Any]) -> None:
37 warnings.warn(
38 "LocalFileStorage is deprecated and will be removed in v1.0. "
39 "Use ServerlessStorageAdapter(backend='file') instead for "
40 "async file operations and ACB integration.",
41 DeprecationWarning,
42 stacklevel=2,
43 )
44 super().__init__(config)
45 self.storage_dir = Path(
46 config.get("storage_dir", Path.home() / ".claude" / "data" / "sessions"),
47 )
48 self.storage_dir.mkdir(parents=True, exist_ok=True)
50 def _get_session_file(self, session_id: str) -> Path:
51 """Get file path for session."""
52 return self.storage_dir / f"{session_id}.json.gz"
54 async def store_session(
55 self,
56 session_state: SessionState,
57 ttl_seconds: int | None = None,
58 ) -> bool:
59 """Store session in local file."""
60 try:
61 # Serialize and compress session state
62 serialized = json.dumps(session_state.to_dict())
63 compressed = gzip.compress(serialized.encode("utf-8"))
65 # Write to file
66 session_file = self._get_session_file(session_state.session_id)
67 with session_file.open("wb") as f:
68 f.write(compressed)
70 return True
72 except Exception as e:
73 self.logger.exception(
74 f"Failed to store session {session_state.session_id}: {e}",
75 )
76 return False
78 async def retrieve_session(self, session_id: str) -> SessionState | None:
79 """Retrieve session from local file."""
80 try:
81 session_file = self._get_session_file(session_id)
83 if not session_file.exists():
84 return None
86 # Read and decompress
87 with session_file.open("rb") as f:
88 compressed_data = f.read()
90 serialized = gzip.decompress(compressed_data).decode("utf-8")
91 session_data = json.loads(serialized)
93 return SessionState.from_dict(session_data)
95 except Exception as e:
96 self.logger.exception(f"Failed to retrieve session {session_id}: {e}")
97 return None
99 async def delete_session(self, session_id: str) -> bool:
100 """Delete session file."""
101 try:
102 session_file = self._get_session_file(session_id)
104 if session_file.exists():
105 session_file.unlink()
106 return True
108 return False
110 except Exception as e:
111 self.logger.exception(f"Failed to delete session {session_id}: {e}")
112 return False
114 async def list_sessions(
115 self,
116 user_id: str | None = None,
117 project_id: str | None = None,
118 ) -> list[str]:
119 """List session files."""
120 try:
121 session_ids = []
122 for session_file in self.storage_dir.glob("*.json.gz"):
123 session_id = self._extract_session_id(session_file)
124 if await self._should_include_session(session_id, user_id, project_id):
125 session_ids.append(session_id)
126 return session_ids
127 except Exception as e:
128 self.logger.exception(f"Failed to list sessions: {e}")
129 return []
131 def _extract_session_id(self, session_file: Path) -> str:
132 """Extract session ID from file path."""
133 return session_file.stem.replace(".json", "")
135 async def _should_include_session(
136 self,
137 session_id: str,
138 user_id: str | None,
139 project_id: str | None,
140 ) -> bool:
141 """Check if session should be included based on filters."""
142 if not user_id and not project_id:
143 return True
145 session_state = await self.retrieve_session(session_id)
146 if not session_state:
147 return False
149 return self._matches_filters(session_state, user_id, project_id)
151 def _matches_filters(
152 self,
153 session_state: SessionState,
154 user_id: str | None,
155 project_id: str | None,
156 ) -> bool:
157 """Check if session matches the given filters."""
158 if user_id and session_state.user_id != user_id:
159 return False
160 return not (project_id and session_state.project_id != project_id)
162 async def cleanup_expired_sessions(self) -> int:
163 """Clean up old session files."""
164 try:
165 now = datetime.now()
166 cleaned = 0
168 for session_file in self.storage_dir.glob("*.json.gz"):
169 # Check file age
170 file_age = now - datetime.fromtimestamp(session_file.stat().st_mtime)
172 if file_age.days > 7: # Cleanup sessions older than 7 days
173 session_file.unlink()
174 cleaned += 1
176 return cleaned
178 except Exception as e:
179 self.logger.exception(f"Failed to cleanup expired sessions: {e}")
180 return 0
182 async def is_available(self) -> bool:
183 """Check if local storage is available."""
184 return self.storage_dir.exists() and self.storage_dir.is_dir()