Skip to content

Commit d96ff86

Browse files
authored
feat: implement simple abstraction for storage (#64)
1 parent 5a410b4 commit d96ff86

File tree

6 files changed

+130
-1
lines changed

6 files changed

+130
-1
lines changed

src/config.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::time::Duration;
22

33
use moka::future::Cache;
44

5+
use crate::storage::{PrivateStorage, StaticStorage};
56
use crate::{
67
endpoints::mods::IndexQueryParams,
78
types::{
@@ -17,6 +18,8 @@ pub struct AppData {
1718
front_url: String,
1819
github: GitHubClientData,
1920
webhook_url: String,
21+
static_storage: StaticStorage,
22+
private_storage: PrivateStorage,
2023
disable_downloads: bool,
2124
max_download_mb: u32,
2225
port: u16,
@@ -34,7 +37,8 @@ pub struct GitHubClientData {
3437
pub async fn build_config() -> anyhow::Result<AppData> {
3538
let env_url = dotenvy::var("DATABASE_URL")?;
3639

37-
let pg_connections = dotenvy::var("DATABASE_CONNECTIONS").map_or(10, |x: String| x.parse::<u32>().unwrap_or(10));
40+
let pg_connections =
41+
dotenvy::var("DATABASE_CONNECTIONS").map_or(10, |x: String| x.parse::<u32>().unwrap_or(10));
3842

3943
let pool = sqlx::postgres::PgPoolOptions::default()
4044
.max_connections(pg_connections)
@@ -68,6 +72,8 @@ pub async fn build_config() -> anyhow::Result<AppData> {
6872
client_secret: github_secret,
6973
},
7074
webhook_url,
75+
static_storage: StaticStorage::new(),
76+
private_storage: PrivateStorage::new(),
7177
disable_downloads,
7278
max_download_mb,
7379
port,
@@ -123,6 +129,14 @@ impl AppData {
123129
self.debug
124130
}
125131

132+
pub fn static_storage(&self) -> &StaticStorage {
133+
&self.static_storage
134+
}
135+
136+
pub fn private_storage(&self) -> &PrivateStorage {
137+
&self.private_storage
138+
}
139+
126140
pub fn mods_cache(&self) -> &Cache<IndexQueryParams, ApiResponse<PaginatedData<Mod>>> {
127141
&self.mods_cache
128142
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod mod_zip;
2121
mod openapi;
2222
mod types;
2323
mod webhook;
24+
mod storage;
2425

2526
#[tokio::main]
2627
async fn main() -> anyhow::Result<()> {

src/storage.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::path::{Path, PathBuf};
2+
3+
#[derive(Clone, Debug)]
4+
pub struct StaticStorage {
5+
base_path: PathBuf,
6+
}
7+
8+
impl StaticStorage {
9+
pub fn new() -> Self {
10+
Self {
11+
base_path: PathBuf::from("storage/static"),
12+
}
13+
}
14+
}
15+
16+
impl StorageDisk for StaticStorage {
17+
fn base_path(&self) -> &Path {
18+
&self.base_path
19+
}
20+
}
21+
22+
#[derive(Clone, Debug)]
23+
pub struct PrivateStorage {
24+
base_path: PathBuf,
25+
}
26+
27+
impl PrivateStorage {
28+
pub fn new() -> Self {
29+
Self {
30+
base_path: PathBuf::from("storage/private"),
31+
}
32+
}
33+
}
34+
35+
impl StorageDisk for PrivateStorage {
36+
fn base_path(&self) -> &Path {
37+
&self.base_path
38+
}
39+
}
40+
41+
trait StorageDisk {
42+
async fn init(&self) -> std::io::Result<()> {
43+
tokio::fs::create_dir_all(self.base_path()).await?;
44+
Ok(())
45+
}
46+
fn base_path(&self) -> &Path;
47+
fn path(&self, relative_path: &str) -> PathBuf {
48+
self.base_path().join(relative_path)
49+
}
50+
async fn store(&self, relative_path: &str, data: &[u8]) -> std::io::Result<()> {
51+
let path = self.path(relative_path);
52+
if let Some(parent) = path.parent() {
53+
tokio::fs::create_dir_all(parent).await?;
54+
}
55+
56+
tokio::fs::write(path, data).await
57+
}
58+
/// Store data at a path calculated from the hash of the data. Uses content-addressable storage with 2 levels
59+
async fn store_hashed(&self, relative_path: &str, data: &[u8]) -> std::io::Result<()> {
60+
self.store_hashed_with_extension(relative_path, data, None)
61+
.await
62+
}
63+
/// Store data at a path calculated from the hash of the data. Uses content-addressable storage with 2 levels.
64+
/// Extension should not include the dot, and will be added to the end of the filename if provided.
65+
async fn store_hashed_with_extension(
66+
&self,
67+
relative_path: &str,
68+
data: &[u8],
69+
extension: Option<&str>,
70+
) -> std::io::Result<()> {
71+
let hash = sha256::digest(data);
72+
73+
let hashed_path = format!(
74+
"{}/{}/{}{}",
75+
relative_path,
76+
&hash[0..2],
77+
hash,
78+
extension.map_or("".to_string(), |ext| format!(
79+
".{}",
80+
ext.trim_start_matches('.')
81+
))
82+
);
83+
self.store(&hashed_path, data).await
84+
}
85+
async fn read(&self, relative_path: &str) -> std::io::Result<Vec<u8>> {
86+
match tokio::fs::read(self.path(relative_path)).await {
87+
Ok(data) => Ok(data),
88+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
89+
Err(e) => Err(e),
90+
}
91+
}
92+
async fn read_stream(&self, relative_path: &str) -> std::io::Result<tokio::fs::File> {
93+
tokio::fs::File::open(self.path(relative_path)).await
94+
}
95+
async fn exists(&self, relative_path: &str) -> std::io::Result<bool> {
96+
let path = self.path(relative_path);
97+
Ok(tokio::fs::metadata(path).await.is_ok())
98+
}
99+
async fn delete(&self, relative_path: &str) -> std::io::Result<()> {
100+
match tokio::fs::remove_file(self.path(relative_path)).await {
101+
Ok(()) => Ok(()),
102+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
103+
Err(e) => Err(e),
104+
}
105+
}
106+
}

storage/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
!
2+
!.gitignore
3+
!public
4+
!private

storage/private/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

storage/public/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

0 commit comments

Comments
 (0)