adk_studio/storage/
filesystem.rs1use crate::schema::{ProjectMeta, ProjectSchema};
2use anyhow::{Context, Result, bail};
3use std::path::{Path, PathBuf};
4use tokio::fs;
5use uuid::Uuid;
6
7pub struct FileStorage {
9 base_dir: PathBuf,
10}
11
12impl FileStorage {
13 pub async fn new(base_dir: PathBuf) -> Result<Self> {
14 let base_dir = base_dir.canonicalize().unwrap_or(base_dir.clone());
15 fs::create_dir_all(&base_dir).await?;
16 let base_dir = base_dir.canonicalize().unwrap_or(base_dir);
17 Ok(Self { base_dir })
18 }
19
20 fn project_path(&self, id: Uuid) -> Result<PathBuf> {
22 let filename = format!("{}.json", id);
25 let path = self.base_dir.join(&filename);
26 let canonical = path
27 .canonicalize()
28 .unwrap_or_else(|_| self.base_dir.join(&filename));
29 if !canonical.starts_with(&self.base_dir) {
30 bail!("Path traversal detected for project id {}", id);
31 }
32 Ok(canonical)
33 }
34
35 pub async fn list(&self) -> Result<Vec<ProjectMeta>> {
36 let mut projects = Vec::new();
37 let mut entries = fs::read_dir(&self.base_dir).await?;
38
39 while let Some(entry) = entries.next_entry().await? {
40 let path = entry.path();
41 if !path.is_file() {
43 continue;
44 }
45 if let Ok(canonical) = path.canonicalize() {
46 if !canonical.starts_with(&self.base_dir) {
47 continue;
48 }
49 }
50 if path.extension().is_some_and(|e| e == "json") {
51 if let Ok(content) = fs::read_to_string(&path).await {
52 if let Ok(project) = serde_json::from_str::<ProjectSchema>(&content) {
53 projects.push(ProjectMeta::from(&project));
54 }
55 }
56 }
57 }
58
59 projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
60 Ok(projects)
61 }
62
63 pub async fn get(&self, id: Uuid) -> Result<ProjectSchema> {
64 let path = self.project_path(id)?;
65 let content = fs::read_to_string(&path)
66 .await
67 .with_context(|| format!("Project {} not found", id))?;
68 serde_json::from_str(&content).context("Invalid project format")
69 }
70
71 pub async fn save(&self, project: &ProjectSchema) -> Result<()> {
72 let path = self.project_path(project.id)?;
73 let content = serde_json::to_string_pretty(project)?;
74 let tmp_path = path.with_extension("json.tmp");
76 fs::write(&tmp_path, content).await?;
77 fs::rename(&tmp_path, &path)
78 .await
79 .with_context(|| format!("Failed to rename temp file to {}", path.display()))?;
80 Ok(())
81 }
82
83 pub async fn delete(&self, id: Uuid) -> Result<()> {
84 let path = self.project_path(id)?;
85 fs::remove_file(&path)
86 .await
87 .with_context(|| format!("Project {} not found", id))
88 }
89
90 pub async fn exists(&self, id: Uuid) -> bool {
91 self.project_path(id).map(|p| p.exists()).unwrap_or(false)
92 }
93
94 pub fn base_dir(&self) -> &Path {
95 &self.base_dir
96 }
97}