| 97 | #[async_trait::async_trait] |
| 98 | impl SessionStore for FileSessionStore { |
| 99 | async fn save(&self, session: &SessionData) -> Result<()> { |
| 100 | let path = self.session_path(&session.id); |
| 101 | |
| 102 | // Serialize to JSON with pretty printing for readability |
| 103 | let json = serde_json::to_string_pretty(session) |
| 104 | .with_context(|| format!("Failed to serialize session: {}", session.id))?; |
| 105 | |
| 106 | // Write atomically: write to temp file with unique name, then rename |
| 107 | // Use timestamp + process ID to ensure uniqueness for concurrent saves |
| 108 | let unique_suffix = format!( |
| 109 | "{}.{}", |
| 110 | std::time::SystemTime::now() |
| 111 | .duration_since(std::time::UNIX_EPOCH) |
| 112 | .unwrap() |
| 113 | .as_nanos(), |
| 114 | std::process::id() |
| 115 | ); |
| 116 | let temp_path = path.with_extension(format!("json.{}.tmp", unique_suffix)); |
| 117 | |
| 118 | let mut file = fs::File::create(&temp_path) |
| 119 | .await |
| 120 | .with_context(|| format!("Failed to create temp file: {}", temp_path.display()))?; |
| 121 | |
| 122 | file.write_all(json.as_bytes()) |
| 123 | .await |
| 124 | .with_context(|| format!("Failed to write session data: {}", session.id))?; |
| 125 | |
| 126 | file.sync_all() |
| 127 | .await |
| 128 | .with_context(|| format!("Failed to sync session file: {}", session.id))?; |
| 129 | |
| 130 | // Rename temp file to final path (atomic on most filesystems) |
| 131 | fs::rename(&temp_path, &path) |
| 132 | .await |
| 133 | .with_context(|| format!("Failed to rename session file: {}", session.id))?; |
| 134 | |
| 135 | tracing::debug!("Saved session {} to {}", session.id, path.display()); |
| 136 | Ok(()) |
| 137 | } |
| 138 | |
| 139 | async fn load(&self, id: &str) -> Result<Option<SessionData>> { |
| 140 | let path = self.session_path(id); |