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