Skip to main content

backbone_storage/
local_simple.rs

1//! Simplified local filesystem storage implementation
2//! This version removes advanced features for compilation stability
3#![allow(dead_code)]
4#![allow(unused_variables)]
5#![allow(unused_imports)]
6
7use async_trait::async_trait;
8use bytes::Bytes;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use chrono::DateTime;
12use tracing::info;
13
14use crate::{
15    StorageResult, StorageError, StorageService, StorageConfig, StorageFile,
16    StorageStats, UploadOptions, DownloadOptions, PresignedUrlOptions,
17    ByteRange, detect_mime_type, sanitize_filename,
18    LocalStorageConfig,
19    traits::FileListResult,
20};
21
22// Simple MD5 implementation for ID generation
23fn md5_compute(input: &str) -> String {
24    use std::collections::hash_map::DefaultHasher;
25    use std::hash::{Hash, Hasher};
26
27    let mut hasher = DefaultHasher::new();
28    input.hash(&mut hasher);
29    format!("{:x}", hasher.finish())
30}
31
32/// Simplified local filesystem storage implementation
33pub struct LocalStorage {
34    base_dir: PathBuf,
35    config: LocalStorageConfig,
36}
37
38impl LocalStorage {
39    /// Create new local storage instance
40    pub fn new(config: LocalStorageConfig) -> StorageResult<Self> {
41        let base_dir = PathBuf::from(&config.base_dir);
42
43        // Ensure base directory exists
44        std::fs::create_dir_all(&base_dir).map_err(|e| {
45            StorageError::FilesystemError(std::io::Error::other(
46                format!("Failed to create base directory {}: {}", base_dir.display(), e)
47            ))
48        })?;
49
50        // Check if directory is writable
51        if !base_dir.exists() || !base_dir.is_dir() {
52            return Err(StorageError::InvalidConfiguration(
53                format!("Base directory is not a valid directory: {}", base_dir.display())
54            ));
55        }
56
57        info!("Local storage initialized with base directory: {}", base_dir.display());
58
59        Ok(Self { base_dir, config })
60    }
61
62    /// Get file path relative to base directory
63    fn get_file_path(&self, key: &str) -> PathBuf {
64        // Sanitize the key to prevent directory traversal
65        let safe_key = sanitize_filename(key);
66        self.base_dir.join(safe_key)
67    }
68
69    /// Get file metadata from filesystem
70    fn get_file_metadata(&self, path: &Path, key: &str) -> StorageResult<StorageFile> {
71        let metadata = std::fs::metadata(path).map_err(|e| {
72            if e.kind() == std::io::ErrorKind::NotFound {
73                StorageError::FileNotFound(key.to_string())
74            } else {
75                StorageError::FilesystemError(e)
76            }
77        })?;
78
79        let modified = metadata.modified()
80            .map_err(StorageError::FilesystemError)?;
81        let created = metadata.created()
82            .unwrap_or(modified); // Use modified time if created time not available
83
84        let content_type = detect_mime_type(key);
85
86        Ok(StorageFile {
87            id: format!("local-{}", md5_compute(key)), // Simple ID generation
88            name: key.to_string(),
89            size: metadata.len(),
90            content_type,
91            checksum: Some(format!("\"{}\"", metadata.len())), // Simple etag
92            checksum_algorithm: Some("size".to_string()),
93            metadata: HashMap::new(),
94            created_at: DateTime::from(created),
95            updated_at: Some(DateTime::from(modified)),
96            version: None,
97            backend: crate::StorageBackend::Local,
98            bucket: "local".to_string(),
99            storage_path: key.to_string(),
100            encrypted: false,
101            compressed: false,
102            expires_at: None,
103        })
104    }
105
106    /// Builder for local storage
107    pub fn builder() -> LocalStorageBuilder {
108        LocalStorageBuilder::new()
109    }
110}
111
112#[async_trait]
113impl StorageService for LocalStorage {
114    async fn upload_bytes(
115        &self,
116        path: &str,
117        data: Bytes,
118        options: Option<UploadOptions>,
119    ) -> StorageResult<StorageFile> {
120        let file_path = self.get_file_path(path);
121
122        // Create parent directories if they don't exist
123        if let Some(parent) = file_path.parent() {
124            std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
125        }
126
127        // Write file
128        tokio::fs::write(&file_path, data).await.map_err(StorageError::FilesystemError)?;
129
130        // Return file metadata
131        self.get_file_metadata(&file_path, path)
132    }
133
134    async fn upload_reader(
135        &self,
136        path: &str,
137        mut reader: Box<dyn tokio::io::AsyncRead + Send + Unpin>,
138        size: Option<u64>,
139        options: Option<UploadOptions>,
140    ) -> StorageResult<StorageFile> {
141        let file_path = self.get_file_path(path);
142
143        // Create parent directories if they don't exist
144        if let Some(parent) = file_path.parent() {
145            std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
146        }
147
148        // Write from reader to file
149        let mut file = tokio::fs::File::create(&file_path).await.map_err(StorageError::FilesystemError)?;
150        
151        tokio::io::copy(&mut reader, &mut file).await.map_err(StorageError::FilesystemError)?;
152
153        // Return file metadata
154        self.get_file_metadata(&file_path, path)
155    }
156
157    async fn upload_file(
158        &self,
159        path: &str,
160        local_path: &str,
161        options: Option<UploadOptions>,
162    ) -> StorageResult<StorageFile> {
163        let file_path = self.get_file_path(path);
164
165        // Create parent directories if they don't exist
166        if let Some(parent) = file_path.parent() {
167            std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
168        }
169
170        // Copy local file
171        tokio::fs::copy(local_path, &file_path).await.map_err(StorageError::FilesystemError)?;
172
173        // Return file metadata
174        self.get_file_metadata(&file_path, path)
175    }
176
177    async fn download_bytes(
178        &self,
179        path: &str,
180        _options: Option<DownloadOptions>,
181    ) -> StorageResult<Bytes> {
182        let file_path = self.get_file_path(path);
183
184        if !file_path.exists() {
185            return Err(StorageError::FileNotFound(path.to_string()));
186        }
187
188        let data = tokio::fs::read(&file_path).await.map_err(StorageError::FilesystemError)?;
189        Ok(Bytes::from(data))
190    }
191
192    async fn download_writer(
193        &self,
194        path: &str,
195        mut writer: Box<dyn tokio::io::AsyncWrite + Send + Unpin>,
196        _options: Option<DownloadOptions>,
197    ) -> StorageResult<()> {
198        let file_path = self.get_file_path(path);
199
200        if !file_path.exists() {
201            return Err(StorageError::FileNotFound(path.to_string()));
202        }
203
204        let mut file = tokio::fs::File::open(&file_path).await.map_err(StorageError::FilesystemError)?;
205        
206        tokio::io::copy(&mut file, &mut writer).await.map_err(StorageError::FilesystemError)?;
207        Ok(())
208    }
209
210    async fn download_file(
211        &self,
212        path: &str,
213        local_path: &str,
214        _options: Option<DownloadOptions>,
215    ) -> StorageResult<()> {
216        let file_path = self.get_file_path(path);
217
218        if !file_path.exists() {
219            return Err(StorageError::FileNotFound(path.to_string()));
220        }
221
222        // Create parent directories for local path
223        if let Some(parent) = std::path::Path::new(local_path).parent() {
224            std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
225        }
226
227        tokio::fs::copy(&file_path, local_path).await.map_err(StorageError::FilesystemError)?;
228        Ok(())
229    }
230
231    async fn stream_file(
232        &self,
233        path: &str,
234        _range: Option<ByteRange>,
235    ) -> StorageResult<Box<dyn futures::stream::Stream<Item = StorageResult<Bytes>> + Send + Unpin>> {
236        let data = self.download_bytes(path, None).await?;
237        let stream = futures::stream::once(async { Ok(data) });
238        Ok(Box::new(Box::pin(stream)) as Box<dyn futures::stream::Stream<Item = StorageResult<Bytes>> + Send + Unpin>)
239    }
240
241    async fn get_file(&self, path: &str) -> StorageResult<StorageFile> {
242        let file_path = self.get_file_path(path);
243        self.get_file_metadata(&file_path, path)
244    }
245
246    async fn file_exists(&self, path: &str) -> StorageResult<bool> {
247        let file_path = self.get_file_path(path);
248        Ok(file_path.exists())
249    }
250
251    async fn delete_file(&self, path: &str) -> StorageResult<bool> {
252        let file_path = self.get_file_path(path);
253
254        if !file_path.exists() {
255            return Ok(false);
256        }
257
258        tokio::fs::remove_file(&file_path).await.map_err(StorageError::FilesystemError)?;
259        Ok(true)
260    }
261
262    async fn copy_file(&self, from_path: &str, to_path: &str) -> StorageResult<StorageFile> {
263        let source_path = self.get_file_path(from_path);
264        let dest_path = self.get_file_path(to_path);
265
266        if !source_path.exists() {
267            return Err(StorageError::FileNotFound(from_path.to_string()));
268        }
269
270        // Create parent directories for destination
271        if let Some(parent) = dest_path.parent() {
272            std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
273        }
274
275        tokio::fs::copy(&source_path, &dest_path).await.map_err(StorageError::FilesystemError)?;
276        self.get_file_metadata(&dest_path, to_path)
277    }
278
279    async fn move_file(&self, from_path: &str, to_path: &str) -> StorageResult<StorageFile> {
280        let result = self.copy_file(from_path, to_path).await?;
281        let _ = self.delete_file(from_path).await?;
282        Ok(result)
283    }
284
285    async fn list_files(
286        &self,
287        prefix: &str,
288        limit: Option<u32>,
289        continuation_token: Option<String>,
290    ) -> StorageResult<FileListResult> {
291        let mut files = Vec::new();
292        let start_after = continuation_token.unwrap_or_default();
293
294        let mut entries = tokio::fs::read_dir(&self.base_dir).await.map_err(StorageError::FilesystemError)?;
295
296        while let Ok(Some(entry)) = entries.next_entry().await {
297            let path = entry.path();
298
299            if path.is_dir() {
300                continue;
301            }
302
303            let key = path.strip_prefix(&self.base_dir)
304                .unwrap_or(&path)
305                .to_string_lossy()
306                .to_string();
307
308            if !key.starts_with(prefix) || key <= start_after {
309                continue;
310            }
311
312            if let Ok(file) = self.get_file_metadata(&path, &key) {
313                files.push(file);
314            }
315
316            if let Some(limit) = limit {
317                if files.len() >= limit as usize {
318                    break;
319                }
320            }
321        }
322
323        let files_count = files.len() as u64;
324        let next_continuation_token = if let Some(limit) = limit {
325            if files.len() >= limit as usize {
326                files.last().map(|f| f.storage_path.clone())
327            } else {
328                None
329            }
330        } else {
331            None
332        };
333
334        Ok(FileListResult {
335            files,
336            prefixes: Vec::new(),
337            truncated: next_continuation_token.is_some(),
338            next_continuation_token,
339            total_count: Some(files_count),
340        })
341    }
342
343    async fn generate_presigned_url(
344        &self,
345        path: &str,
346        options: PresignedUrlOptions,
347    ) -> StorageResult<String> {
348        let file_path = self.get_file_path(path);
349
350        if !file_path.exists() {
351            return Err(StorageError::FileNotFound(path.to_string()));
352        }
353
354        // For local storage, return a file:// URL
355        Ok(format!("file://{}", file_path.display()))
356    }
357
358    async fn get_file_url(
359        &self,
360        path: &str,
361        _expiry_seconds: Option<u64>,
362    ) -> StorageResult<String> {
363        self.generate_presigned_url(path, PresignedUrlOptions::default()).await
364    }
365
366    async fn update_metadata(
367        &self,
368        path: &str,
369        _metadata: HashMap<String, String>,
370    ) -> StorageResult<StorageFile> {
371        // For local storage, metadata is stored in file system attributes
372        // For simplicity, just return current file metadata
373        self.get_file(path).await
374    }
375
376    async fn get_stats(&self) -> StorageResult<StorageStats> {
377        let mut total_files = 0u64;
378        let mut total_size = 0u64;
379        let mut backend_usage = HashMap::new();
380        let mut content_type_counts = HashMap::new();
381
382        let mut entries = tokio::fs::read_dir(&self.base_dir).await.map_err(StorageError::FilesystemError)?;
383
384        while let Ok(Some(entry)) = entries.next_entry().await {
385            let path = entry.path();
386
387            if path.is_file() {
388                total_files += 1;
389                if let Ok(metadata) = entry.metadata().await {
390                    total_size += metadata.len();
391                }
392
393                // Count by content type
394                if let Some(ext) = path.extension() {
395                    let content_type = detect_mime_type(&path.to_string_lossy());
396                    *content_type_counts.entry(content_type).or_insert(0) += 1;
397                }
398            }
399        }
400
401        backend_usage.insert("local".to_string(), total_size);
402
403        Ok(StorageStats {
404            file_count: total_files,
405            total_bytes: total_size,
406            backend_usage,
407            content_type_counts,
408            average_file_size: if total_files > 0 { total_size as f64 / total_files as f64 } else { 0.0 },
409            largest_file_size: 0, // Would need additional scanning to find largest
410            last_updated: chrono::Utc::now(),
411        })
412    }
413
414    async fn test_connection(&self) -> StorageResult<bool> {
415        // Test by checking if base directory exists and is writable
416        Ok(self.base_dir.exists() && self.base_dir.is_dir())
417    }
418
419    fn backend_type(&self) -> crate::StorageBackend {
420        crate::StorageBackend::Local
421    }
422
423    fn bucket(&self) -> &str {
424        "local"
425    }
426
427    fn config(&self) -> &StorageConfig {
428        static DEFAULT_CONFIG: std::sync::OnceLock<StorageConfig> = std::sync::OnceLock::new();
429        DEFAULT_CONFIG.get_or_init(|| StorageConfig {
430            backend: crate::StorageBackend::Local,
431            bucket: "local".to_string(),
432            base_path: Some(self.config.base_dir.clone()),
433            multipart_threshold: 100 * 1024 * 1024, // 100MB
434            chunk_size: 8 * 1024 * 1024, // 8MB
435            versioning: false,
436            compression: false,
437            encryption: None,
438            retention: None,
439            access_control: None,
440            backend_config: HashMap::new(),
441            #[cfg(feature = "compression")]
442            compression_config: None,
443        })
444    }
445}
446
447/// Builder for LocalStorage
448pub struct LocalStorageBuilder {
449    base_dir: Option<String>,
450    compression: bool,
451}
452
453impl LocalStorageBuilder {
454    /// Create new builder
455    pub fn new() -> Self {
456        Self {
457            base_dir: None,
458            compression: false,
459        }
460    }
461
462    /// Set base directory
463    pub fn base_dir(mut self, dir: impl Into<String>) -> Self {
464        self.base_dir = Some(dir.into());
465        self
466    }
467
468    /// Enable/disable compression
469    pub fn compression(mut self, enabled: bool) -> Self {
470        self.compression = enabled;
471        self
472    }
473
474    /// Build LocalStorage
475    pub fn build(self) -> StorageResult<LocalStorage> {
476        let base_dir = self.base_dir
477            .ok_or_else(|| StorageError::InvalidConfiguration("Base directory is required".to_string()))?;
478
479        let config = LocalStorageConfig {
480            base_dir,
481            base_path: None,
482            compression: self.compression,
483            encryption: None,
484        };
485
486        LocalStorage::new(config)
487    }
488}
489
490impl Default for LocalStorageBuilder {
491    fn default() -> Self {
492        Self::new()
493    }
494}