fraiseql_server/files/storage/
local.rs1use std::{collections::HashMap, path::PathBuf, time::Duration};
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use tokio::fs;
8
9use crate::files::{
10 config::StorageConfig,
11 error::StorageError,
12 traits::{StorageBackend, StorageMetadata, StorageResult},
13};
14
15pub struct LocalStorage {
16 base_path: PathBuf,
17 serve_url: String,
18}
19
20impl LocalStorage {
21 pub fn new(config: &StorageConfig) -> Result<Self, StorageError> {
22 let base_path = config
23 .base_path
24 .as_ref()
25 .map(PathBuf::from)
26 .unwrap_or_else(|| PathBuf::from("./uploads"));
27
28 let serve_url = config.serve_path.clone().unwrap_or_else(|| "/files".to_string());
29
30 std::fs::create_dir_all(&base_path).map_err(|e| StorageError::Configuration {
32 message: format!("Failed to create upload directory: {}", e),
33 })?;
34
35 Ok(Self {
36 base_path,
37 serve_url,
38 })
39 }
40}
41
42#[async_trait]
43impl StorageBackend for LocalStorage {
44 fn name(&self) -> &'static str {
45 "local"
46 }
47
48 async fn upload(
49 &self,
50 key: &str,
51 data: Bytes,
52 _content_type: &str,
53 _metadata: Option<&StorageMetadata>,
54 ) -> Result<StorageResult, StorageError> {
55 let path = self.base_path.join(key);
56
57 if let Some(parent) = path.parent() {
59 fs::create_dir_all(parent).await.map_err(|e| StorageError::UploadFailed {
60 message: e.to_string(),
61 })?;
62 }
63
64 fs::write(&path, &data).await.map_err(|e| StorageError::UploadFailed {
65 message: e.to_string(),
66 })?;
67
68 Ok(StorageResult {
69 key: key.to_string(),
70 url: self.public_url(key),
71 etag: None,
72 size: data.len() as u64,
73 })
74 }
75
76 async fn download(&self, key: &str) -> Result<Bytes, StorageError> {
77 let path = self.base_path.join(key);
78
79 let data = fs::read(&path).await.map_err(|e| {
80 if e.kind() == std::io::ErrorKind::NotFound {
81 StorageError::NotFound {
82 key: key.to_string(),
83 }
84 } else {
85 StorageError::DownloadFailed {
86 message: e.to_string(),
87 }
88 }
89 })?;
90
91 Ok(Bytes::from(data))
92 }
93
94 async fn delete(&self, key: &str) -> Result<(), StorageError> {
95 let path = self.base_path.join(key);
96
97 fs::remove_file(&path).await.map_err(|e| StorageError::Provider {
98 message: e.to_string(),
99 })?;
100
101 Ok(())
102 }
103
104 async fn exists(&self, key: &str) -> Result<bool, StorageError> {
105 let path = self.base_path.join(key);
106 Ok(path.exists())
107 }
108
109 async fn metadata(&self, key: &str) -> Result<StorageMetadata, StorageError> {
110 let path = self.base_path.join(key);
111
112 let meta = fs::metadata(&path).await.map_err(|e| StorageError::Provider {
113 message: e.to_string(),
114 })?;
115
116 let last_modified = meta.modified().ok().and_then(|t| {
117 let duration = t.duration_since(std::time::UNIX_EPOCH).ok()?;
118 chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0)
119 });
120
121 Ok(StorageMetadata {
122 content_type: mime_guess::from_path(&path).first_or_octet_stream().to_string(),
123 content_length: meta.len(),
124 etag: None,
125 last_modified,
126 custom: HashMap::new(),
127 })
128 }
129
130 async fn signed_url(&self, key: &str, _expiry: Duration) -> Result<String, StorageError> {
131 Ok(self.public_url(key))
134 }
135
136 fn public_url(&self, key: &str) -> String {
137 format!("{}/{}", self.serve_url, key)
138 }
139}