Skip to main content

fraiseql_server/storage/
local.rs

1//! Local filesystem storage backend.
2
3use std::{path::PathBuf, time::Duration};
4
5use async_trait::async_trait;
6use fraiseql_error::FileError;
7
8use super::{StorageBackend, StorageResult, validate_key};
9
10/// Stores files on the local filesystem under a root directory.
11pub struct LocalStorageBackend {
12    root: PathBuf,
13}
14
15impl LocalStorageBackend {
16    /// Creates a new local storage backend rooted at `root`.
17    #[must_use]
18    pub fn new(root: &str) -> Self {
19        Self {
20            root: PathBuf::from(root),
21        }
22    }
23
24    fn key_path(&self, key: &str) -> StorageResult<PathBuf> {
25        validate_key(key)?;
26        Ok(self.root.join(key))
27    }
28}
29
30#[async_trait]
31impl StorageBackend for LocalStorageBackend {
32    async fn upload(&self, key: &str, data: &[u8], _content_type: &str) -> StorageResult<String> {
33        let path = self.key_path(key)?;
34        if let Some(parent) = path.parent() {
35            tokio::fs::create_dir_all(parent).await.map_err(|e| FileError::Storage {
36                message: format!("Failed to create directory: {e}"),
37                source:  Some(Box::new(e)),
38            })?;
39        }
40        tokio::fs::write(&path, data).await.map_err(|e| FileError::Storage {
41            message: format!("Failed to write file: {e}"),
42            source:  Some(Box::new(e)),
43        })?;
44        Ok(key.to_string())
45    }
46
47    async fn download(&self, key: &str) -> StorageResult<Vec<u8>> {
48        let path = self.key_path(key)?;
49        tokio::fs::read(&path).await.map_err(|e| {
50            if e.kind() == std::io::ErrorKind::NotFound {
51                FileError::NotFound {
52                    id: key.to_string(),
53                }
54            } else {
55                FileError::Storage {
56                    message: format!("Failed to read file: {e}"),
57                    source:  Some(Box::new(e)),
58                }
59            }
60        })
61    }
62
63    async fn delete(&self, key: &str) -> StorageResult<()> {
64        let path = self.key_path(key)?;
65        tokio::fs::remove_file(&path).await.map_err(|e| {
66            if e.kind() == std::io::ErrorKind::NotFound {
67                FileError::NotFound {
68                    id: key.to_string(),
69                }
70            } else {
71                FileError::Storage {
72                    message: format!("Failed to delete file: {e}"),
73                    source:  Some(Box::new(e)),
74                }
75            }
76        })
77    }
78
79    async fn exists(&self, key: &str) -> StorageResult<bool> {
80        let path = self.key_path(key)?;
81        match tokio::fs::metadata(&path).await {
82            Ok(_) => Ok(true),
83            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
84            Err(e) => Err(FileError::Storage {
85                message: format!("Failed to check file existence: {e}"),
86                source:  Some(Box::new(e)),
87            }),
88        }
89    }
90
91    async fn presigned_url(&self, _key: &str, _expiry: Duration) -> StorageResult<String> {
92        Err(FileError::Storage {
93            message: "Presigned URLs are not supported for local storage".to_string(),
94            source:  None,
95        })
96    }
97}