Skip to content

Commit bea8473

Browse files
committed
feat: add local storage driver
This commit introduces a new local storage driver to the application. This allows users to store files and metadata on the local filesystem instead of relying on cloud-based storage solutions like Azure. The local storage driver provides the following features: - File storage: Files are stored in a configurable local directory. - Metadata storage: File metadata, including content type and user-defined tags, is stored in a separate .metadata directory. - Directory listing: The driver can list files and directories within the storage path. - Configuration: The local storage path can be configured in the config.toml file. This change improves the flexibility and versatility of the application, making it easier for users to deploy and test it in different environments, especially self-hosted setup. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
1 parent dd0457d commit bea8473

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)