use crate::addressing::{Address, Addressable}; use crate::database::entry::{Entry, InEntry}; use crate::database::hierarchies::{list_roots, resolve_path, UHierPath}; use crate::database::lang::Query; use crate::database::UpEndDatabase; use crate::filesystem::add_file; use crate::previews::PreviewStore; use crate::util::hash::{b58_decode, b58_encode, Hashable}; use crate::util::jobs::JobContainer; use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}; use actix_web::http; use actix_web::http::header::{ContentDisposition, DispositionType}; use actix_web::{delete, error, get, post, put, web, Either, Error, HttpResponse}; use anyhow::{anyhow, Result}; use futures_util::TryStreamExt; use log::{debug, info, trace}; use serde::Deserialize; use serde_json::json; use std::convert::TryFrom; use std::fs; use std::io::Write; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::{collections::HashMap, io}; use tempfile::NamedTempFile; #[cfg(feature = "desktop")] use is_executable::IsExecutable; #[derive(Clone)] pub struct State { pub upend: Arc, pub vault_name: Option, pub job_container: Arc>, pub preview_store: Option>, pub desktop_enabled: bool, } #[derive(Deserialize)] pub struct RawRequest { native: Option, inline: Option, } #[get("/api/raw/{hash}")] pub async fn get_raw( state: web::Data, web::Query(query): web::Query, hash: web::Path, ) -> Result, Error> { let address = Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?) .map_err(ErrorInternalServerError)?; if let Address::Hash(hash) = address { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; // First check if there's an entry with this hash let entry = connection .retrieve_entry(hash.clone()) .map_err(ErrorInternalServerError)?; if let Some(entry) = entry { return Ok(Either::B(HttpResponse::Ok().json(entry))); } // Then, check the files let files = connection .retrieve_file(hash) .map_err(ErrorInternalServerError)?; if let Some(file) = files.get(0) { let file_path = state.upend.vault_path.join(&file.path); if query.native.is_none() { Ok(Either::A(if query.inline.is_some() { NamedFile::open(file_path)?.set_content_disposition(ContentDisposition { disposition: DispositionType::Inline, parameters: vec![], }) } else { NamedFile::open(file_path)? })) } else if state.desktop_enabled { #[cfg(feature = "desktop")] { info!("Opening {:?}...", file_path); let mut response = HttpResponse::NoContent(); let path = if !file_path.is_executable() { file_path } else { response .header( http::header::WARNING, "199 - Opening parent directory due to file being executable.", ) .header( http::header::ACCESS_CONTROL_EXPOSE_HEADERS, http::header::WARNING.to_string(), ); file_path .parent() .ok_or_else(|| { ErrorInternalServerError("No parent to open as fallback.") })? .to_path_buf() }; opener::open(path).map_err(error::ErrorServiceUnavailable)?; Ok(Either::B(response.finish())) } #[cfg(not(feature = "desktop"))] unreachable!() } else { Err(error::ErrorNotImplemented("Desktop features not enabled.")) } } else { Err(error::ErrorNotFound("NOT FOUND")) } } else { Err(ErrorBadRequest( "Address does not refer to a rawable object.", )) } } #[derive(Deserialize)] pub struct QueryRequest { query: String, } #[get("/api/obj")] pub async fn get_query( state: web::Data, web::Query(info): web::Query, ) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let in_query: Query = info.query.as_str().parse().map_err(ErrorBadRequest)?; let entries = connection .query(in_query) .map_err(ErrorInternalServerError)?; let mut result: HashMap = HashMap::new(); for entry in entries { result.insert( b58_encode( entry .address() .map_err(ErrorInternalServerError)? .encode() .map_err(ErrorInternalServerError)?, ), entry, ); } Ok(HttpResponse::Ok().json(&result)) } trait EntriesAsHash { fn as_hash(&self) -> Result>; } impl EntriesAsHash for Vec { fn as_hash(&self) -> Result> { let mut result: HashMap = HashMap::new(); for entry in self { result.insert(b58_encode(entry.address()?.encode()?), entry); } Ok(result) } } #[get("/api/obj/{address_str}")] pub async fn get_object( state: web::Data, address_str: web::Path, ) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let result: Vec = connection .retrieve_object( Address::decode(&b58_decode(address_str.into_inner()).map_err(ErrorBadRequest)?) .map_err(ErrorBadRequest)?, ) .map_err(ErrorInternalServerError)?; debug!("{:?}", result); Ok(HttpResponse::Ok().json(result.as_hash().map_err(ErrorInternalServerError)?)) } #[put("/api/obj")] pub async fn put_object( state: web::Data, payload: Either, Multipart>, ) -> Result { let (result_address, entry) = match payload { Either::A(in_entry) => { let in_entry = in_entry.into_inner(); let entry = Entry::try_from(in_entry).map_err(ErrorInternalServerError)?; let connection = state.upend.connection().map_err(ErrorInternalServerError)?; Ok(( connection .insert_entry(entry.clone()) .map_err(ErrorInternalServerError)?, Some(entry), )) } Either::B(mut multipart) => { if let Some(mut field) = multipart.try_next().await? { let content_disposition = field .content_disposition() .ok_or_else(|| HttpResponse::BadRequest().finish())?; let filename = content_disposition.get_filename(); let mut file = NamedTempFile::new()?; while let Some(chunk) = field.try_next().await? { file = web::block(move || file.write_all(&chunk).map(|_| file)).await?; } let path = PathBuf::from(file.path()); let hash = web::block(move || path.hash()).await?; let address = Address::Hash(hash.clone()); let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let existing_files = connection .retrieve_file(hash.clone()) .map_err(ErrorInternalServerError)?; if existing_files.is_empty() { let addr_str = b58_encode(address.encode().map_err(ErrorInternalServerError)?); let final_name = if let Some(filename) = filename { format!("{addr_str}_{filename}") } else { addr_str }; let final_path = state.upend.vault_path.join(&final_name); let (_, tmp_path) = file.keep().map_err(ErrorInternalServerError)?; let final_path = web::block::<_, _, io::Error>(move || { fs::copy(&tmp_path, &final_path)?; fs::remove_file(tmp_path)?; Ok(final_path) }) .await?; add_file(&connection, &final_path, hash).map_err(ErrorInternalServerError)?; } if let Some(filename) = filename { let _ = upend_insert_val!(&connection, address, "LBL", filename); } Ok((address, None)) } else { Err(ErrorBadRequest(anyhow!("wat"))) } } }?; Ok(HttpResponse::Ok().json( [( b58_encode(result_address.encode().map_err(ErrorInternalServerError)?), entry, )] .iter() .cloned() .collect::>>(), )) } #[delete("/api/obj/{address_str}")] pub async fn delete_object( state: web::Data, address_str: web::Path, ) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let _ = connection .remove_object( Address::decode(&b58_decode(address_str.into_inner()).map_err(ErrorBadRequest)?) .map_err(ErrorInternalServerError)?, ) .map_err(ErrorInternalServerError)?; Ok(HttpResponse::Ok().finish()) } // #[post("api/obj/{address_str}")] // pub async fn update_attribute( // state: web::Data, // address_str: web::Path, // mut payload: web::Payload, // ) -> Result { // let body = load_body(&mut payload) // .await // .map_err(error::ErrorBadRequest)?; // let entry_value = serde_json::from_slice::(&body).map_err(ErrorBadRequest)?; // let connection = state.upend.connection().map_err(ErrorInternalServerError)?; // connection // .transaction::<_, anyhow::Error, _>(|| { // let address = Address::decode(&decode(address_str.into_inner())?)?; // let _ = connection.remove_object(address)?; // Ok(()) // }) // .map_err(ErrorInternalServerError)?; // Ok(HttpResponse::Ok().finish()) // } #[get("/api/all/attributes")] pub async fn get_all_attributes(state: web::Data) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let result = connection .get_all_attributes() .map_err(ErrorInternalServerError)?; Ok(HttpResponse::Ok().json(result)) } #[get("/api/hier/{path:.*}")] pub async fn list_hier( state: web::Data, path: web::Path, ) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; if path.is_empty() { Ok(HttpResponse::MovedPermanently() .header("Location", "/api/hier_roots") .finish()) } else { let upath: UHierPath = path.into_inner().parse().map_err(ErrorBadRequest)?; trace!("Listing path \"{}\"", upath); // todo: 500 if actual error occurs let path = resolve_path(&connection, &upath, false).map_err(ErrorNotFound)?; match path.last() { Some(addr) => Ok(HttpResponse::Found() .header("Location", format!("/api/obj/{}", addr)) .finish()), None => Ok(HttpResponse::NotFound().finish()), } } } #[get("/api/hier_roots")] pub async fn list_hier_roots(state: web::Data) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let result = list_roots(&connection) .map_err(ErrorInternalServerError)? .into_iter() .map(|root| connection.retrieve_object(root)) .collect::>>>() .map_err(ErrorInternalServerError)? .concat(); Ok(HttpResponse::Ok().json(result.as_hash().map_err(ErrorInternalServerError)?)) } #[post("/api/refresh")] pub async fn api_refresh(state: web::Data) -> Result { actix::spawn(crate::filesystem::rescan_vault( state.upend.clone(), state.job_container.clone(), false, )); Ok(HttpResponse::Ok().finish()) } #[get("/api/files/{hash}")] pub async fn get_file( state: web::Data, hash: web::Path, ) -> Result { let address = Address::decode(&b58_decode(hash.into_inner()).map_err(ErrorInternalServerError)?) .map_err(ErrorInternalServerError)?; if let Address::Hash(hash) = address { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let response = connection .retrieve_file(hash) .map_err(ErrorInternalServerError)?; Ok(HttpResponse::Ok().json(response)) } else { Err(ErrorBadRequest("Address does not refer to a file.")) } } #[get("/api/files/latest")] pub async fn latest_files(state: web::Data) -> Result { let connection = state.upend.connection().map_err(ErrorInternalServerError)?; let files = connection .get_latest_files(100) .map_err(ErrorInternalServerError)?; Ok(HttpResponse::Ok().json(&files)) } #[get("/api/jobs")] pub async fn get_jobs(state: web::Data) -> Result { let jobs = state.job_container.read().unwrap().get_jobs(); Ok(HttpResponse::Ok().json(&jobs)) } #[get("/api/info")] pub async fn get_info(state: web::Data) -> Result { Ok(HttpResponse::Ok().json(json!({ "name": state.vault_name, "location": &*state.upend.vault_path, "version": crate::common::PKG_VERSION }))) } #[get("/api/thumb/{hash}")] pub async fn get_thumbnail( state: web::Data, hash: web::Path, ) -> Result, Error> { #[cfg(feature = "previews")] if let Some(preview_store) = &state.preview_store { let hash = hash.into_inner(); let address = Address::decode(&b58_decode(&hash).map_err(ErrorInternalServerError)?) .map_err(ErrorInternalServerError)?; if let Address::Hash(address_hash) = address { let preview_result = preview_store .get(address_hash) .map_err(error::ErrorInternalServerError)?; if let Some(preview_path) = preview_result { let mut file = NamedFile::open(&preview_path)?.disable_content_disposition(); if let Some(mime_type) = tree_magic_mini::from_filepath(&preview_path) { if let Ok(mime) = mime_type.parse() { file = file.set_content_type(mime); } } return Ok(Either::A(file)); } else { return Ok(Either::B( HttpResponse::SeeOther() .header(http::header::LOCATION, format!("/api/raw/{hash}")) .finish(), )); } } else { return Err(ErrorBadRequest( "Address does not refer to a previewable object.", )); } } Err(error::ErrorNotImplemented("Previews not enabled.")) }