Files
route96/src/routes/mod.rs
2024-10-15 12:17:03 +01:00

238 lines
6.4 KiB
Rust

use std::fs;
use std::fs::File;
use std::str::FromStr;
use crate::db::{Database, FileUpload};
use crate::filesystem::FileStore;
pub use crate::routes::admin::admin_routes;
#[cfg(feature = "blossom")]
pub use crate::routes::blossom::blossom_routes;
#[cfg(feature = "nip96")]
pub use crate::routes::nip96::nip96_routes;
use crate::settings::Settings;
#[cfg(feature = "void-cat-redirects")]
use crate::void_db::VoidCatDb;
use anyhow::Error;
use nostr::Event;
use rocket::fs::NamedFile;
use rocket::http::{ContentType, Header, Status};
#[cfg(feature = "void-cat-redirects")]
use rocket::response::Redirect;
use rocket::response::Responder;
use rocket::serde::Serialize;
use rocket::{Request, State};
#[cfg(feature = "blossom")]
mod blossom;
#[cfg(feature = "nip96")]
mod nip96;
mod admin;
pub struct FilePayload {
pub file: File,
pub info: FileUpload,
}
#[derive(Clone, Debug, Serialize, Default)]
#[serde(crate = "rocket::serde")]
struct Nip94Event {
pub created_at: i64,
pub content: String,
pub tags: Vec<Vec<String>>,
}
#[derive(Serialize, Default)]
#[serde(crate = "rocket::serde")]
struct PagedResult<T> {
pub count: u32,
pub page: u32,
pub total: u32,
pub files: Vec<T>,
}
impl Nip94Event {
pub fn from_upload(settings: &Settings, upload: &FileUpload) -> Self {
let hex_id = hex::encode(&upload.id);
let mut tags = vec![
vec![
"url".to_string(),
format!("{}/{}", &settings.public_url, &hex_id),
],
vec!["x".to_string(), hex_id],
vec!["m".to_string(), upload.mime_type.clone()],
vec!["size".to_string(), upload.size.to_string()],
];
if let Some(bh) = &upload.blur_hash {
tags.push(vec!["blurhash".to_string(), bh.clone()]);
}
if let (Some(w), Some(h)) = (upload.width, upload.height) {
tags.push(vec!["dim".to_string(), format!("{}x{}", w, h)])
}
#[cfg(feature = "labels")]
for l in &upload.labels {
let val = if l.label.contains(',') {
let split_val: Vec<&str> = l.label.split(',').collect();
split_val[0].to_string()
} else {
l.label.clone()
};
tags.push(vec!["t".to_string(), val])
}
Self {
content: upload.name.clone(),
created_at: upload.created.timestamp(),
tags,
}
}
}
impl<'r> Responder<'r, 'static> for FilePayload {
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)
}
}
async fn delete_file(
sha256: &str,
auth: &Event,
fs: &FileStore,
db: &Database,
) -> Result<(), Error> {
let sha256 = if sha256.contains(".") {
sha256.split('.').next().unwrap()
} else {
sha256
};
let id = if let Ok(i) = hex::decode(sha256) {
i
} else {
return Err(Error::msg("Invalid file id"));
};
if id.len() != 32 {
return Err(Error::msg("Invalid file id"));
}
if let Ok(Some(_info)) = db.get_file(&id).await {
let pubkey_vec = auth.pubkey.to_bytes().to_vec();
let owners = db.get_file_owners(&id).await?;
let this_owner = match owners.iter().find(|o| o.pubkey.eq(&pubkey_vec)) {
Some(o) => o,
None => return Err(Error::msg("You dont own this file, you cannot delete it")),
};
if let Err(e) = db.delete_file_owner(&id, this_owner.id).await {
return Err(Error::msg(format!("Failed to delete (db): {}", e)));
}
// only 1 owner was left, delete file completely
if owners.len() == 1 {
if let Err(e) = db.delete_file(&id).await {
return Err(Error::msg(format!("Failed to delete (fs): {}", e)));
}
if let Err(e) = fs::remove_file(fs.get(&id)) {
return Err(Error::msg(format!("Failed to delete (fs): {}", e)));
}
}
Ok(())
} else {
Err(Error::msg("File not found"))
}
}
#[rocket::get("/")]
pub async fn root() -> Result<NamedFile, Status> {
#[cfg(debug_assertions)]
let index = "./ui_src/dist/index.html";
#[cfg(not(debug_assertions))]
let index = "./ui/index.html";
if let Ok(f) = NamedFile::open(index).await {
Ok(f)
} else {
Err(Status::InternalServerError)
}
}
#[rocket::get("/<sha256>")]
pub async fn get_blob(
sha256: &str,
fs: &State<FileStore>,
db: &State<Database>,
) -> Result<FilePayload, Status> {
let sha256 = if sha256.contains(".") {
sha256.split('.').next().unwrap()
} else {
sha256
};
let id = if let Ok(i) = hex::decode(sha256) {
i
} else {
return Err(Status::NotFound);
};
if id.len() != 32 {
return Err(Status::NotFound);
}
if let Ok(Some(info)) = db.get_file(&id).await {
if let Ok(f) = File::open(fs.get(&id)) {
return Ok(FilePayload { file: f, info });
}
}
Err(Status::NotFound)
}
#[rocket::head("/<sha256>")]
pub async fn head_blob(sha256: &str, fs: &State<FileStore>) -> Status {
let sha256 = if sha256.contains(".") {
sha256.split('.').next().unwrap()
} else {
sha256
};
let id = if let Ok(i) = hex::decode(sha256) {
i
} else {
return Status::NotFound;
};
if id.len() != 32 {
return Status::NotFound;
}
if fs.get(&id).exists() {
Status::Ok
} else {
Status::NotFound
}
}
#[cfg(feature = "void-cat-redirects")]
#[rocket::get("/d/<id>")]
pub async fn void_cat_redirect(
id: &str,
settings: &State<Settings>,
vdb: &State<VoidCatDb>,
) -> Option<Redirect> {
let id = if id.contains(".") {
id.split('.').next().unwrap()
} else {
id
};
let uuid =
uuid::Uuid::from_slice_le(nostr::bitcoin::base58::decode(id).unwrap().as_slice()).unwrap();
if let Ok(Some(d)) = vdb.get_digest(&uuid).await {
Some(Redirect::permanent(format!(
"{}/{}",
&settings.public_url, &d
)))
} else {
None
}
}