Initial commit
This commit is contained in:
11
crates/cgcx-storage/Cargo.toml
Normal file
11
crates/cgcx-storage/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "cgcx-storage"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cgcx-core = { path = "../cgcx-core" }
|
||||
cgcx-config = { path = "../cgcx-config" }
|
||||
tokio = { version = "1", features = ["fs", "io-util"] }
|
||||
tracing = "0.1"
|
||||
tempfile = "3"
|
||||
86
crates/cgcx-storage/src/lib.rs
Normal file
86
crates/cgcx-storage/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use cgcx_config::StoragePaths;
|
||||
use cgcx_core::{ContentId, Result, CgcxError};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Storage {
|
||||
paths: StoragePaths,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new(paths: StoragePaths) -> Self {
|
||||
Self { paths }
|
||||
}
|
||||
|
||||
pub async fn ensure_dirs(&self) -> Result<()> {
|
||||
for dir in [&self.paths.media, &self.paths.documents, &self.paths.text, &self.paths.temp] {
|
||||
fs::create_dir_all(dir).await.map_err(|e| CgcxError::Storage(format!("create dir {:?}: {}", dir, e)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn media_dir(&self) -> &Path {
|
||||
&self.paths.media
|
||||
}
|
||||
|
||||
pub fn documents_dir(&self) -> &Path {
|
||||
&self.paths.documents
|
||||
}
|
||||
|
||||
pub fn text_dir(&self) -> &Path {
|
||||
&self.paths.text
|
||||
}
|
||||
|
||||
pub fn temp_dir(&self) -> &Path {
|
||||
&self.paths.temp
|
||||
}
|
||||
|
||||
pub fn content_dir(&self, content_id: &ContentId, mime_type: &str) -> PathBuf {
|
||||
let base = if mime_type.starts_with("image/") || mime_type.starts_with("video/") || mime_type.starts_with("audio/") {
|
||||
&self.paths.media
|
||||
} else if mime_type.starts_with("text/") {
|
||||
&self.paths.text
|
||||
} else {
|
||||
&self.paths.documents
|
||||
};
|
||||
base.join(content_id.as_str())
|
||||
}
|
||||
|
||||
pub fn file_path(&self, content_id: &ContentId, file_index: u32, mime_type: &str) -> Result<PathBuf> {
|
||||
let base = if mime_type.starts_with("image/") || mime_type.starts_with("video/") || mime_type.starts_with("audio/") {
|
||||
&self.paths.media
|
||||
} else if mime_type.starts_with("text/") {
|
||||
&self.paths.text
|
||||
} else {
|
||||
&self.paths.documents
|
||||
};
|
||||
let dir = base.join(content_id.as_str());
|
||||
let file_name = format!("{}_{:04}.enc", content_id.as_str(), file_index);
|
||||
let path = dir.join(file_name);
|
||||
|
||||
if !path.starts_with(base) {
|
||||
return Err(CgcxError::Storage("path traversal detected".into()));
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn temp_file(&self) -> Result<tempfile::NamedTempFile> {
|
||||
tempfile::NamedTempFile::new_in(&self.paths.temp)
|
||||
.map_err(|e| CgcxError::Storage(format!("create temp file: {}", e)))
|
||||
}
|
||||
|
||||
pub async fn delete_content_files(&self, content_id: &ContentId, mime_type: &str) -> Result<()> {
|
||||
let dir = self.content_dir(content_id, mime_type);
|
||||
if dir.exists() {
|
||||
fs::remove_dir_all(&dir).await.map_err(|e| CgcxError::Storage(format!("remove dir {:?}: {}", dir, e)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn file_size(&self, path: &Path) -> Result<u64> {
|
||||
let meta = fs::metadata(path).await.map_err(|e| CgcxError::Storage(format!("metadata {:?}: {}", path, e)))?;
|
||||
Ok(meta.len())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user