Skip to content

Commit 95ac486

Browse files
authored
Merge pull request #10 from nuclearcat/add-local-storage
feat: add local storage driver
2 parents dd0457d + bea8473 commit 95ac486

3 files changed

Lines changed: 319 additions & 7 deletions

File tree

config-local.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
driver="local"
2+
jwt_secret="test-secret-for-local-storage-testing"
3+
4+
[local]
5+
storage_path="./test-storage"

src/local.rs

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// SPDX-License-Identifier: LGPL-2.1-or-later
2+
// Copyright (C) 2024-2025 Collabora, Ltd.
3+
// Author: Denys Fedoryshchenko <denys.f@collabora.com>
4+
5+
use crate::{get_config_content, ReceivedFile};
6+
use axum::http::{HeaderName, HeaderValue};
7+
use chksum_hash_sha2_512 as sha2_512;
8+
use headers::HeaderMap;
9+
use serde::Deserialize;
10+
use std::env;
11+
use std::fs::{self, File, OpenOptions};
12+
use std::io::Write;
13+
use std::path::{Path, PathBuf};
14+
use toml::Table;
15+
16+
macro_rules! debug_log {
17+
($($arg:tt)*) => {
18+
if env::var("STORAGE_DEBUG").is_ok() {
19+
println!($($arg)*);
20+
}
21+
};
22+
}
23+
24+
pub struct LocalDriver;
25+
26+
impl LocalDriver {
27+
pub fn new() -> Self {
28+
LocalDriver
29+
}
30+
}
31+
32+
#[derive(Deserialize)]
33+
struct LocalConfig {
34+
storage_path: String,
35+
}
36+
37+
/// Get local storage configuration from config.toml
38+
fn get_local_config() -> LocalConfig {
39+
let cfg_content = get_config_content();
40+
let cfg: Table = toml::from_str(&cfg_content).unwrap();
41+
42+
// Default to "./storage" if no local config section exists
43+
let default_config = LocalConfig {
44+
storage_path: "./storage".to_string(),
45+
};
46+
47+
let local_cfg = match cfg.get("local") {
48+
Some(local_cfg) => local_cfg,
49+
None => {
50+
debug_log!("No local section in config.toml, using default storage path: ./storage");
51+
return default_config;
52+
}
53+
};
54+
55+
let storage_path = local_cfg
56+
.get("storage_path")
57+
.and_then(|v| v.as_str())
58+
.unwrap_or("./storage")
59+
.to_string();
60+
61+
LocalConfig { storage_path }
62+
}
63+
64+
/// Create directory structure for a file path
65+
fn ensure_directory_exists(file_path: &Path) -> Result<(), std::io::Error> {
66+
if let Some(parent) = file_path.parent() {
67+
fs::create_dir_all(parent)?;
68+
}
69+
Ok(())
70+
}
71+
72+
/// Get full file path in storage directory
73+
fn get_storage_file_path(filename: &str) -> PathBuf {
74+
let config = get_local_config();
75+
let storage_path = Path::new(&config.storage_path);
76+
storage_path.join(filename)
77+
}
78+
79+
/// Get metadata file path for storing headers
80+
fn get_metadata_file_path(filename: &str) -> PathBuf {
81+
let config = get_local_config();
82+
let storage_path = Path::new(&config.storage_path);
83+
let metadata_path = storage_path.join(".metadata");
84+
85+
// Create metadata directory if it doesn't exist
86+
if !metadata_path.exists() {
87+
let _ = fs::create_dir_all(&metadata_path);
88+
}
89+
90+
// Generate hash-based filename for metadata
91+
let hash = sha2_512::default().update(filename.as_bytes()).finalize();
92+
let digest = hash.digest();
93+
metadata_path.join(format!("{}.headers", digest.to_hex_lowercase()))
94+
}
95+
96+
/// Calculate SHA-512 checksum of file data
97+
fn calculate_checksum(filename: &str, data: &[u8]) {
98+
let hash = sha2_512::default().update(data).finalize();
99+
let digest = hash.digest();
100+
debug_log!("File: {} Checksum: {}", filename, digest.to_hex_lowercase());
101+
}
102+
103+
/// Write file to local storage
104+
fn write_file_to_local(filename: String, data: Vec<u8>, cont_type: String) -> Result<String, String> {
105+
let file_path = get_storage_file_path(&filename);
106+
107+
// Ensure directory structure exists
108+
if let Err(e) = ensure_directory_exists(&file_path) {
109+
return Err(format!("Failed to create directory structure: {}", e));
110+
}
111+
112+
// Write the file
113+
match File::create(&file_path) {
114+
Ok(mut file) => {
115+
if let Err(e) = file.write_all(&data) {
116+
return Err(format!("Failed to write file: {}", e));
117+
}
118+
calculate_checksum(&filename, &data);
119+
}
120+
Err(e) => {
121+
return Err(format!("Failed to create file: {}", e));
122+
}
123+
}
124+
125+
// Create and write metadata (headers)
126+
let metadata_path = get_metadata_file_path(&filename);
127+
if let Ok(mut metadata_file) = File::create(&metadata_path) {
128+
let headers_content = format!("content-type:{}\n", cont_type);
129+
let _ = metadata_file.write_all(headers_content.as_bytes());
130+
}
131+
132+
debug_log!("File written to local storage: {}", file_path.display());
133+
Ok(filename)
134+
}
135+
136+
/// Read headers from metadata file
137+
fn get_headers_from_metadata_file(filename: &str) -> HeaderMap {
138+
let mut headers = HeaderMap::new();
139+
let metadata_path = get_metadata_file_path(filename);
140+
141+
if let Ok(content) = fs::read_to_string(&metadata_path) {
142+
for line in content.lines() {
143+
if let Some((name, value)) = line.split_once(':') {
144+
if let (Ok(key), Ok(val)) = (
145+
HeaderName::from_bytes(name.trim().as_bytes()),
146+
HeaderValue::from_str(value.trim())
147+
) {
148+
headers.insert(key, val);
149+
}
150+
}
151+
}
152+
} else {
153+
// Default content-type if metadata file doesn't exist
154+
if let Ok(val) = HeaderValue::from_str("application/octet-stream") {
155+
headers.insert("content-type", val);
156+
}
157+
}
158+
159+
headers
160+
}
161+
162+
/// Get file from local storage
163+
fn get_file_from_local(filename: String) -> ReceivedFile {
164+
let file_path = get_storage_file_path(&filename);
165+
166+
let mut received_file = ReceivedFile {
167+
original_filename: filename.clone(),
168+
cached_filename: String::new(),
169+
headers: HeaderMap::new(),
170+
valid: false,
171+
};
172+
173+
// Check if file exists
174+
if !file_path.exists() {
175+
debug_log!("File not found in local storage: {}", file_path.display());
176+
return received_file;
177+
}
178+
179+
// For local storage, we use the same file as both original and cached
180+
received_file.cached_filename = file_path.to_string_lossy().to_string();
181+
received_file.headers = get_headers_from_metadata_file(&filename);
182+
received_file.valid = true;
183+
184+
debug_log!("File found in local storage: {}", file_path.display());
185+
received_file
186+
}
187+
188+
/// List files in local storage directory
189+
fn list_files_in_local(directory: String) -> Vec<String> {
190+
let config = get_local_config();
191+
let storage_path = Path::new(&config.storage_path);
192+
let search_path = if directory == "/" || directory.is_empty() {
193+
storage_path.to_path_buf()
194+
} else {
195+
storage_path.join(directory.trim_start_matches('/'))
196+
};
197+
198+
let mut files = Vec::new();
199+
200+
fn collect_files_recursive(path: &Path, base_path: &Path, files: &mut Vec<String>) {
201+
if let Ok(entries) = fs::read_dir(path) {
202+
for entry in entries {
203+
if let Ok(entry) = entry {
204+
let entry_path = entry.path();
205+
206+
// Skip metadata directory
207+
if entry_path.file_name().map_or(false, |name| name == ".metadata") {
208+
continue;
209+
}
210+
211+
if entry_path.is_file() {
212+
// Get relative path from storage root
213+
if let Ok(relative_path) = entry_path.strip_prefix(base_path) {
214+
files.push(relative_path.to_string_lossy().to_string());
215+
}
216+
} else if entry_path.is_dir() {
217+
collect_files_recursive(&entry_path, base_path, files);
218+
}
219+
}
220+
}
221+
}
222+
}
223+
224+
if search_path.exists() && search_path.is_dir() {
225+
collect_files_recursive(&search_path, storage_path, &mut files);
226+
}
227+
228+
files.sort();
229+
debug_log!("Listed {} files from local storage", files.len());
230+
files
231+
}
232+
233+
/// Set tags for local storage (stored in metadata)
234+
fn set_tags_for_local_file(filename: String, user_tags: Vec<(String, String)>) -> Result<String, String> {
235+
let metadata_path = get_metadata_file_path(&filename);
236+
237+
// Read existing metadata
238+
let mut existing_content = String::new();
239+
if metadata_path.exists() {
240+
existing_content = fs::read_to_string(&metadata_path)
241+
.map_err(|e| format!("Failed to read metadata file: {}", e))?;
242+
}
243+
244+
// Open metadata file for writing (create if doesn't exist)
245+
let mut metadata_file = OpenOptions::new()
246+
.create(true)
247+
.write(true)
248+
.truncate(true)
249+
.open(&metadata_path)
250+
.map_err(|e| format!("Failed to open metadata file: {}", e))?;
251+
252+
// Write existing content first
253+
metadata_file.write_all(existing_content.as_bytes())
254+
.map_err(|e| format!("Failed to write existing metadata: {}", e))?;
255+
256+
// Write tags
257+
for (tag, value) in user_tags {
258+
let tag_line = format!("tag-{}:{}\n", tag, value);
259+
metadata_file.write_all(tag_line.as_bytes())
260+
.map_err(|e| format!("Failed to write tag: {}", e))?;
261+
}
262+
263+
debug_log!("Tags written to metadata file: {}", metadata_path.display());
264+
Ok("OK".to_string())
265+
}
266+
267+
/// Implement Driver trait for LocalDriver
268+
impl super::Driver for LocalDriver {
269+
fn write_file(&self, filename: String, data: Vec<u8>, cont_type: String) -> String {
270+
match write_file_to_local(filename.clone(), data, cont_type) {
271+
Ok(_) => filename,
272+
Err(e) => {
273+
eprintln!("Local storage write error: {}", e);
274+
String::new()
275+
}
276+
}
277+
}
278+
279+
fn get_file(&self, filename: String) -> ReceivedFile {
280+
get_file_from_local(filename)
281+
}
282+
283+
fn tag_file(
284+
&self,
285+
filename: String,
286+
user_tags: Vec<(String, String)>,
287+
) -> Result<String, String> {
288+
set_tags_for_local_file(filename, user_tags)
289+
}
290+
291+
fn list_files(&self, directory: String) -> Vec<String> {
292+
list_files_in_local(directory)
293+
}
294+
}

src/main.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
mod azure;
14+
mod local;
1415
mod storcaching;
1516
mod storjwt;
1617

@@ -116,9 +117,10 @@ trait Driver {
116117
fn init_driver(driver_type: &str) -> Box<dyn Driver> {
117118
let driver: Box<dyn Driver> = match driver_type {
118119
"azure" => Box::new(azure::AzureDriver::new()),
120+
"local" => Box::new(local::LocalDriver::new()),
119121
//"google" => Box::new(google::GoogleDriver::new()),
120122
_ => {
121-
eprintln!("Unknown driver type");
123+
eprintln!("Unknown driver type: {}", driver_type);
122124
std::process::exit(1);
123125
}
124126
};
@@ -135,6 +137,17 @@ pub fn get_config_content() -> String {
135137
std::fs::read_to_string(&cfg_file).unwrap()
136138
}
137139

140+
/// Get driver type from config.toml, defaults to "azure" for backward compatibility
141+
fn get_driver_type() -> String {
142+
let cfg_content = get_config_content();
143+
let cfg: Table = toml::from_str(&cfg_content).unwrap();
144+
145+
cfg.get("driver")
146+
.and_then(|v| v.as_str())
147+
.unwrap_or("azure")
148+
.to_string()
149+
}
150+
138151
/// Initial variables configuration and checks
139152
async fn initial_setup() -> Option<RustlsConfig> {
140153
let cache_dir = "cache";
@@ -693,14 +706,14 @@ async fn ax_get_file(
693706
}
694707

695708
fn driver_get_file(filepath: String) -> ReceivedFile {
696-
let driver_name = "azure";
697-
let driver = init_driver(driver_name);
709+
let driver_name = get_driver_type();
710+
let driver = init_driver(&driver_name);
698711
driver.get_file(filepath)
699712
}
700713

701714
fn write_file_driver(filename: String, data: Vec<u8>, cont_type: String) -> String {
702-
let driver_name = "azure";
703-
let driver = init_driver(driver_name);
715+
let driver_name = get_driver_type();
716+
let driver = init_driver(&driver_name);
704717
driver.write_file(filename, data, cont_type);
705718
"".to_string()
706719
}
@@ -759,8 +772,8 @@ fn verify_auth_hdr(headers: &HeaderMap) -> Result<String, Option<String>> {
759772
}
760773

761774
async fn ax_list_files() -> (StatusCode, String) {
762-
let driver_name = "azure";
763-
let driver = init_driver(driver_name);
775+
let driver_name = get_driver_type();
776+
let driver = init_driver(&driver_name);
764777
let files = driver.list_files("/".to_string());
765778
// generate nice list of files, with one file per line
766779
let files_str = files.join("\n");

0 commit comments

Comments
 (0)