Upload/list progress
This commit is contained in:
parent
00351407d5
commit
6ed9088aaa
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
target/
|
||||||
|
data/
|
74
Cargo.lock
generated
74
Cargo.lock
generated
@ -66,6 +66,21 @@ version = "0.2.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.82"
|
version = "1.0.82"
|
||||||
@ -334,6 +349,21 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets 0.52.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@ -411,6 +441,12 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -1056,6 +1092,29 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -2333,6 +2392,7 @@ dependencies = [
|
|||||||
"atoi",
|
"atoi",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"either",
|
"either",
|
||||||
@ -2393,6 +2453,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
|
"sqlx-postgres",
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@ -2411,6 +2472,7 @@ dependencies = [
|
|||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"digest",
|
"digest",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@ -2452,6 +2514,7 @@ dependencies = [
|
|||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
@ -2487,6 +2550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
|
"chrono",
|
||||||
"flume",
|
"flume",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -3016,6 +3080,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
@ -3185,6 +3250,15 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
@ -17,5 +17,6 @@ serde = { version = "1.0.198", features = ["derive"] }
|
|||||||
uuid = { version = "1.8.0", features = ["v4"] }
|
uuid = { version = "1.8.0", features = ["v4"] }
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
sqlx = { version = "0.7.4", features = ["mysql", "runtime-tokio"] }
|
sqlx = { version = "0.7.4", features = ["mysql", "runtime-tokio", "chrono"] }
|
||||||
config = { version = "0.14.0", features = ["toml"] }
|
config = { version = "0.14.0", features = ["toml"] }
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
@ -11,6 +11,7 @@ create table uploads
|
|||||||
user_id integer unsigned not null,
|
user_id integer unsigned not null,
|
||||||
name varchar(256) not null,
|
name varchar(256) not null,
|
||||||
size integer unsigned not null,
|
size integer unsigned not null,
|
||||||
|
mime_type varchar(128) not null,
|
||||||
created timestamp default current_timestamp,
|
created timestamp default current_timestamp,
|
||||||
|
|
||||||
constraint fk_uploads_user
|
constraint fk_uploads_user
|
||||||
|
18
src/auth.rs
18
src/auth.rs
@ -1,12 +1,12 @@
|
|||||||
|
use base64::prelude::*;
|
||||||
|
use log::info;
|
||||||
|
use nostr::{Event, JsonUtil, Kind, Tag, Timestamp};
|
||||||
|
use rocket::{async_trait, Request};
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::request::{FromRequest, Outcome};
|
use rocket::request::{FromRequest, Outcome};
|
||||||
use rocket::{async_trait, Request};
|
|
||||||
|
|
||||||
use base64::prelude::*;
|
|
||||||
use nostr::{Event, JsonUtil, Kind, Tag, TagKind, Timestamp};
|
|
||||||
|
|
||||||
pub struct BlossomAuth {
|
pub struct BlossomAuth {
|
||||||
pub pubkey: String,
|
pub content_type: Option<String>,
|
||||||
pub event: Event,
|
pub event: Event,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,9 +52,15 @@ impl<'r> FromRequest<'r> for BlossomAuth {
|
|||||||
if let Err(_) = event.verify() {
|
if let Err(_) = event.verify() {
|
||||||
return Outcome::Error((Status::new(401), "Event signature invalid"));
|
return Outcome::Error((Status::new(401), "Event signature invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!("{}", event.as_json());
|
||||||
Outcome::Success(BlossomAuth {
|
Outcome::Success(BlossomAuth {
|
||||||
pubkey: event.pubkey.to_string(),
|
|
||||||
event,
|
event,
|
||||||
|
content_type: request.headers().iter().find_map(|h| if h.name == "content-type" {
|
||||||
|
Some(h.value.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Outcome::Error((Status::new(403), "Auth scheme must be Nostr"))
|
Outcome::Error((Status::new(403), "Auth scheme must be Nostr"))
|
||||||
|
14
src/blob.rs
14
src/blob.rs
@ -1,5 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::db::FileUpload;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct BlobDescriptor {
|
pub struct BlobDescriptor {
|
||||||
@ -10,3 +12,15 @@ pub struct BlobDescriptor {
|
|||||||
pub mime_type: Option<String>,
|
pub mime_type: Option<String>,
|
||||||
pub created: u64,
|
pub created: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&FileUpload> for BlobDescriptor {
|
||||||
|
fn from(value: &FileUpload) -> Self {
|
||||||
|
Self {
|
||||||
|
url: "".to_string(),
|
||||||
|
sha256: hex::encode(&value.id),
|
||||||
|
size: value.size,
|
||||||
|
mime_type: Some(value.mime_type.clone()),
|
||||||
|
created: value.created.timestamp() as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/cors.rs
15
src/cors.rs
@ -1,6 +1,7 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
use rocket::fairing::{Fairing, Info, Kind};
|
use rocket::fairing::{Fairing, Info, Kind};
|
||||||
use rocket::http::Header;
|
use rocket::http::{Header, Method, Status};
|
||||||
use rocket::{Request, Response};
|
use rocket::{Data, Request, Response};
|
||||||
|
|
||||||
pub struct CORS;
|
pub struct CORS;
|
||||||
|
|
||||||
@ -13,13 +14,19 @@ impl Fairing for CORS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_response<'r>(&self, _req: &'r Request<'_>, response: &mut Response<'r>) {
|
async fn on_response<'r>(&self, req: &'r Request<'_>, response: &mut Response<'r>) {
|
||||||
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
|
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
|
||||||
response.set_header(Header::new(
|
response.set_header(Header::new(
|
||||||
"Access-Control-Allow-Methods",
|
"Access-Control-Allow-Methods",
|
||||||
"POST, GET, HEAD, DELETE, OPTIONS",
|
"PUT, GET, HEAD, DELETE, OPTIONS",
|
||||||
));
|
));
|
||||||
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
|
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
|
||||||
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
|
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
|
||||||
|
|
||||||
|
// force status 200 for options requests
|
||||||
|
if req.method() == Method::Options {
|
||||||
|
response.set_status(Status::Ok);
|
||||||
|
response.set_sized_body(None, Cursor::new(""))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
44
src/db.rs
44
src/db.rs
@ -1,13 +1,15 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use sqlx::{Error, FromRow, Row};
|
||||||
use sqlx::migrate::MigrateError;
|
use sqlx::migrate::MigrateError;
|
||||||
use sqlx::{Error, Row};
|
|
||||||
|
|
||||||
#[derive(Clone, sqlx::FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
pub struct FileUpload {
|
pub struct FileUpload {
|
||||||
pub id: Vec<u8>,
|
pub id: Vec<u8>,
|
||||||
pub user_id: u64,
|
pub user_id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub created: u64,
|
pub mime_type: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -25,27 +27,49 @@ impl Database {
|
|||||||
sqlx::migrate!("./migrations/").run(&self.pool).await
|
sqlx::migrate!("./migrations/").run(&self.pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_user(&self, pubkey: Vec<u8>) -> Result<u32, Error> {
|
pub async fn upsert_user(&self, pubkey: &Vec<u8>) -> Result<u32, Error> {
|
||||||
let res = sqlx::query("insert into users(pubkey) values(?) returning id")
|
let res = sqlx::query("insert ignore into users(pubkey) values(?) returning id")
|
||||||
|
.bind(pubkey)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
match res {
|
||||||
|
None => sqlx::query("select id from users where pubkey = ?")
|
||||||
.bind(pubkey)
|
.bind(pubkey)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?
|
||||||
res.try_get(0)
|
.try_get(0),
|
||||||
|
Some(res) => res.try_get(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_file(&self, file: FileUpload) -> Result<(), Error> {
|
pub async fn add_file(&self, file: &FileUpload) -> Result<(), Error> {
|
||||||
sqlx::query("insert into uploads(id,user_id,name,size) values(?,?,?,?)")
|
sqlx::query("insert into uploads(id,user_id,name,size,mime_type) values(?,?,?,?,?)")
|
||||||
.bind(&file.id)
|
.bind(&file.id)
|
||||||
.bind(&file.user_id)
|
.bind(&file.user_id)
|
||||||
.bind(&file.name)
|
.bind(&file.name)
|
||||||
.bind(&file.size)
|
.bind(&file.size)
|
||||||
|
.bind(&file.mime_type)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_file(&self, file: &Vec<u8>) -> Result<Option<FileUpload>, Error> {
|
||||||
|
sqlx::query_as("select * from uploads where id = ?")
|
||||||
|
.bind(&file)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_file(&self, file: &Vec<u8>) -> Result<(), Error> {
|
||||||
|
sqlx::query_as("delete from uploads where id = ?")
|
||||||
|
.bind(&file)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_files(&self, pubkey: &Vec<u8>) -> Result<Vec<FileUpload>, Error> {
|
pub async fn list_files(&self, pubkey: &Vec<u8>) -> Result<Vec<FileUpload>, Error> {
|
||||||
let results: Vec<FileUpload> = sqlx::query_as("select * from uploads where user_id = ?")
|
let results: Vec<FileUpload> = sqlx::query_as("select * from uploads where user_id = (select id from users where pubkey = ?)")
|
||||||
.bind(&pubkey)
|
.bind(&pubkey)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -4,10 +4,11 @@ use std::io::SeekFrom;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use log::info;
|
||||||
use rocket::data::DataStream;
|
use rocket::data::DataStream;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
use tokio::io::{AsyncReadExt, AsyncSeekExt, BufWriter};
|
||||||
|
|
||||||
use crate::db::Database;
|
use crate::db::Database;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
@ -40,27 +41,26 @@ impl FileStore {
|
|||||||
let random_id = uuid::Uuid::new_v4();
|
let random_id = uuid::Uuid::new_v4();
|
||||||
let tmp_path = FileStore::map_temp(random_id);
|
let tmp_path = FileStore::map_temp(random_id);
|
||||||
|
|
||||||
let file = stream.into_file(&tmp_path).await;
|
let mut file = File::options()
|
||||||
match file {
|
.create(true).write(true).read(true)
|
||||||
Err(e) => Err(Error::from(e)),
|
.open(tmp_path.clone()).await?;
|
||||||
Ok(file) => {
|
let n = stream.stream_to(&mut BufWriter::new(&mut file)).await?;
|
||||||
let size = file.n.written;
|
|
||||||
let mut file = file.value;
|
info!("File saved to temp path: {}", tmp_path.to_str().unwrap());
|
||||||
let hash = FileStore::hash_file(&mut file).await?;
|
let hash = FileStore::hash_file(&mut file).await?;
|
||||||
let dst_path = self.map_path(&hash);
|
let dst_path = self.map_path(&hash);
|
||||||
|
fs::create_dir_all(dst_path.parent().unwrap())?;
|
||||||
if let Err(e) = fs::rename(&tmp_path, &dst_path) {
|
if let Err(e) = fs::rename(&tmp_path, &dst_path) {
|
||||||
fs::remove_file(&tmp_path)?;
|
fs::remove_file(&tmp_path)?;
|
||||||
Err(Error::from(e))
|
Err(Error::from(e))
|
||||||
} else {
|
} else {
|
||||||
Ok(FileSystemResult {
|
Ok(FileSystemResult {
|
||||||
size,
|
size: n.written,
|
||||||
sha256: hash,
|
sha256: hash,
|
||||||
path: dst_path,
|
path: dst_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn hash_file(file: &mut File) -> Result<Vec<u8>, Error> {
|
async fn hash_file(file: &mut File) -> Result<Vec<u8>, Error> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
|
227
src/routes.rs
227
src/routes.rs
@ -1,19 +1,85 @@
|
|||||||
use crate::auth::BlossomAuth;
|
use std::fs;
|
||||||
use crate::blob::BlobDescriptor;
|
use std::fs::File;
|
||||||
use crate::db::Database;
|
use std::path::{Path, PathBuf};
|
||||||
use crate::filesystem::FileStore;
|
use std::str::FromStr;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use log::{error, info};
|
||||||
|
use nostr::{JsonUtil, Tag, TagKind};
|
||||||
use nostr::prelude::hex;
|
use nostr::prelude::hex;
|
||||||
use nostr::Tag;
|
use rocket::{async_trait, Data, Request, Route, routes, State, uri};
|
||||||
use rocket::data::ToByteUnit;
|
use rocket::data::ToByteUnit;
|
||||||
use rocket::fs::NamedFile;
|
use rocket::fs::NamedFile;
|
||||||
use rocket::http::Status;
|
use rocket::http::{ContentType, Header, Status};
|
||||||
|
use rocket::http::hyper::header::CONTENT_DISPOSITION;
|
||||||
use rocket::request::{FromRequest, Outcome};
|
use rocket::request::{FromRequest, Outcome};
|
||||||
|
use rocket::response::Responder;
|
||||||
use rocket::response::status::NotFound;
|
use rocket::response::status::NotFound;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{async_trait, routes, Data, Request, Route, State};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::auth::BlossomAuth;
|
||||||
|
use crate::blob::BlobDescriptor;
|
||||||
|
use crate::db::{Database, FileUpload};
|
||||||
|
use crate::filesystem::FileStore;
|
||||||
|
use crate::routes::BlossomResponse::BlobDescriptorList;
|
||||||
|
|
||||||
pub fn all() -> Vec<Route> {
|
pub fn all() -> Vec<Route> {
|
||||||
routes![root, get_blob, get_blob_check, upload, list_files]
|
routes![root, get_blob, head_blob, delete_blob, upload, list_files]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct BlossomError {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlossomError {
|
||||||
|
pub fn new(msg: String) -> Self {
|
||||||
|
Self { message: msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BlossomFile {
|
||||||
|
pub file: File,
|
||||||
|
pub info: FileUpload,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> Responder<'r, 'static> for BlossomFile {
|
||||||
|
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'static> {
|
||||||
|
let mut response = self.file.respond_to(request)?;
|
||||||
|
if let Ok(ct) = ContentType::from_str(&self.info.mime_type) {
|
||||||
|
response.set_header(ct);
|
||||||
|
}
|
||||||
|
response.set_header(Header::new("content-disposition", format!("inline; filename=\"{}\"", self.info.name)));
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Responder)]
|
||||||
|
enum BlossomResponse {
|
||||||
|
#[response(status = 403)]
|
||||||
|
Unauthorized(Json<BlossomError>),
|
||||||
|
|
||||||
|
#[response(status = 500)]
|
||||||
|
GenericError(Json<BlossomError>),
|
||||||
|
|
||||||
|
#[response(status = 200)]
|
||||||
|
File(BlossomFile),
|
||||||
|
|
||||||
|
#[response(status = 200)]
|
||||||
|
BlobDescriptor(Json<BlobDescriptor>),
|
||||||
|
|
||||||
|
#[response(status = 200)]
|
||||||
|
BlobDescriptorList(Json<Vec<BlobDescriptor>>),
|
||||||
|
|
||||||
|
StatusOnly(Status),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlossomResponse {
|
||||||
|
pub fn error(msg: impl Into<String>) -> Self {
|
||||||
|
Self::GenericError(Json(BlossomError::new(msg.into())))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_method(event: &nostr::Event, method: &str) -> bool {
|
fn check_method(event: &nostr::Event, method: &str) -> bool {
|
||||||
@ -21,9 +87,7 @@ fn check_method(event: &nostr::Event, method: &str) -> bool {
|
|||||||
Tag::Hashtag(tag) => Some(tag),
|
Tag::Hashtag(tag) => Some(tag),
|
||||||
_ => None,
|
_ => None,
|
||||||
}) {
|
}) {
|
||||||
if t == method {
|
return t == method;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -34,81 +98,146 @@ async fn root() -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::get("/<sha256>")]
|
#[rocket::get("/<sha256>")]
|
||||||
async fn get_blob(sha256: &str, fs: &State<FileStore>) -> Result<NamedFile, Status> {
|
async fn get_blob(sha256: &str, fs: &State<FileStore>, db: &State<Database>) -> BlossomResponse {
|
||||||
|
let sha256 = if sha256.contains(".") {
|
||||||
|
sha256.split('.').next().unwrap()
|
||||||
|
} else {
|
||||||
|
sha256
|
||||||
|
};
|
||||||
let id = if let Ok(i) = hex::decode(sha256) {
|
let id = if let Ok(i) = hex::decode(sha256) {
|
||||||
i
|
i
|
||||||
} else {
|
} else {
|
||||||
return Err(Status::NotFound);
|
return BlossomResponse::error("Invalid file id");
|
||||||
};
|
};
|
||||||
|
|
||||||
if id.len() != 32 {
|
if id.len() != 32 {
|
||||||
return Err(Status::NotFound);
|
return BlossomResponse::error("Invalid file id");
|
||||||
}
|
}
|
||||||
if let Ok(f) = NamedFile::open(fs.get(&id)).await {
|
if let Ok(Some(info)) = db.get_file(&id).await {
|
||||||
Ok(f)
|
if let Ok(f) = File::open(fs.get(&id)) {
|
||||||
} else {
|
return BlossomResponse::File(BlossomFile {
|
||||||
Err(Status::NotFound)
|
file: f,
|
||||||
|
info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
BlossomResponse::StatusOnly(Status::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::head("/<sha256>")]
|
#[rocket::head("/<sha256>")]
|
||||||
async fn get_blob_check(sha256: &str, fs: &State<FileStore>) -> Status {
|
async fn head_blob(sha256: &str, fs: &State<FileStore>) -> BlossomResponse {
|
||||||
|
let sha256 = if sha256.contains(".") {
|
||||||
|
sha256.split('.').next().unwrap()
|
||||||
|
} else {
|
||||||
|
sha256
|
||||||
|
};
|
||||||
let id = if let Ok(i) = hex::decode(sha256) {
|
let id = if let Ok(i) = hex::decode(sha256) {
|
||||||
i
|
i
|
||||||
} else {
|
} else {
|
||||||
return Status::NotFound;
|
return BlossomResponse::error("Invalid file id");
|
||||||
};
|
};
|
||||||
|
|
||||||
if id.len() != 32 {
|
if id.len() != 32 {
|
||||||
return Status::NotFound;
|
return BlossomResponse::error("Invalid file id");
|
||||||
}
|
}
|
||||||
if fs.get(&id).exists() {
|
if fs.get(&id).exists() {
|
||||||
Status::Ok
|
BlossomResponse::StatusOnly(Status::Ok)
|
||||||
} else {
|
} else {
|
||||||
Status::NotFound
|
BlossomResponse::StatusOnly(Status::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::delete("/<sha256>")]
|
||||||
|
async fn delete_blob(sha256: &str, fs: &State<FileStore>, db: &State<Database>) -> BlossomResponse {
|
||||||
|
let sha256 = if sha256.contains(".") {
|
||||||
|
sha256.split('.').next().unwrap()
|
||||||
|
} else {
|
||||||
|
sha256
|
||||||
|
};
|
||||||
|
let id = if let Ok(i) = hex::decode(sha256) {
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
return BlossomResponse::error("Invalid file id");
|
||||||
|
};
|
||||||
|
|
||||||
|
if id.len() != 32 {
|
||||||
|
return BlossomResponse::error("Invalid file id");
|
||||||
|
}
|
||||||
|
if let Ok(Some(_info)) = db.get_file(&id).await {
|
||||||
|
db.delete_file(&id).await?;
|
||||||
|
fs::remove_file(fs.get(&id))?;
|
||||||
|
BlossomResponse::StatusOnly(Status::Ok)
|
||||||
|
} else {
|
||||||
|
BlossomResponse::StatusOnly(Status::NotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::put("/upload", data = "<data>")]
|
#[rocket::put("/upload", data = "<data>")]
|
||||||
async fn upload(auth: BlossomAuth, fs: &State<FileStore>, data: Data<'_>) -> Status {
|
async fn upload(auth: BlossomAuth, fs: &State<FileStore>, db: &State<Database>, data: Data<'_>)
|
||||||
|
-> BlossomResponse {
|
||||||
if !check_method(&auth.event, "upload") {
|
if !check_method(&auth.event, "upload") {
|
||||||
return Status::NotFound;
|
return BlossomResponse::error("Invalid request method tag");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = auth.event.tags.iter().find_map(|t| match t {
|
||||||
|
Tag::Name(s) => Some(s.clone()),
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
let size = auth.event.tags.iter().find_map(|t| {
|
||||||
|
let values = t.as_vec();
|
||||||
|
if values.len() == 2 && values[0] == "size" {
|
||||||
|
Some(values[1].parse::<usize>().unwrap())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if size.is_none() {
|
||||||
|
return BlossomResponse::error("Invalid request, no size tag");
|
||||||
|
}
|
||||||
match fs.put(data.open(8.gigabytes())).await {
|
match fs.put(data.open(8.gigabytes())).await {
|
||||||
Ok(blob) => Status::Ok,
|
Ok(blob) => {
|
||||||
Err(e) => Status::InternalServerError,
|
let pubkey_vec = auth.event.pubkey.to_bytes().to_vec();
|
||||||
|
let user_id = match db.upsert_user(&pubkey_vec).await {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => return BlossomResponse::error(format!("Failed to save file (db): {}", e))
|
||||||
|
};
|
||||||
|
let f = FileUpload {
|
||||||
|
id: blob.sha256,
|
||||||
|
user_id: user_id as u64,
|
||||||
|
name: name.unwrap_or("".to_string()),
|
||||||
|
size: blob.size,
|
||||||
|
mime_type: auth.content_type.unwrap_or("application/octet-stream".to_string()),
|
||||||
|
created: Utc::now(),
|
||||||
|
};
|
||||||
|
if let Err(e) = db.add_file(&f).await {
|
||||||
|
error!("{:?}", e);
|
||||||
|
BlossomResponse::error(format!("Error saving file (db): {}", e))
|
||||||
|
} else {
|
||||||
|
BlossomResponse::BlobDescriptor(Json(BlobDescriptor::from(&f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{:?}", e);
|
||||||
|
BlossomResponse::error(format!("Error saving file (disk): {}", e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::get("/list/<pubkey>")]
|
#[rocket::get("/list/<pubkey>")]
|
||||||
async fn list_files(
|
async fn list_files(
|
||||||
auth: BlossomAuth,
|
|
||||||
db: &State<Database>,
|
db: &State<Database>,
|
||||||
pubkey: String,
|
pubkey: &str,
|
||||||
) -> Result<Json<Vec<BlobDescriptor>>, Status> {
|
) -> BlossomResponse {
|
||||||
if !check_method(&auth.event, "list") {
|
|
||||||
return Err(Status::NotFound);
|
|
||||||
}
|
|
||||||
let id = if let Ok(i) = hex::decode(pubkey) {
|
let id = if let Ok(i) = hex::decode(pubkey) {
|
||||||
i
|
i
|
||||||
} else {
|
} else {
|
||||||
return Err(Status::NotFound);
|
return BlossomResponse::error("invalid pubkey");
|
||||||
};
|
};
|
||||||
if let Ok(files) = db.list_files(&id).await {
|
match db.list_files(&id).await {
|
||||||
Ok(Json(
|
Ok(files) => BlobDescriptorList(Json(files.iter()
|
||||||
files
|
.map(|f| BlobDescriptor::from(f))
|
||||||
.iter()
|
.collect())
|
||||||
.map(|f| BlobDescriptor {
|
),
|
||||||
url: "".to_string(),
|
Err(e) => BlossomResponse::error(format!("Could not list files: {}", e))
|
||||||
sha256: hex::encode(&f.id),
|
|
||||||
size: f.size,
|
|
||||||
mime_type: None,
|
|
||||||
created: f.created,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err(Status::InternalServerError)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user