Skip to content

Commit

Permalink
create upload layer endpoint (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
zerj9 authored Oct 14, 2024
1 parent 34c34a0 commit 56090a1
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 79 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ yarn-error.log*
next-env.d.ts

local
uploads
2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ aws-sdk-dynamodb = "1.48"
axum = { version = "0.7", features = ["macros", "multipart"] }
axum-extra = { version = "0.9.4", features = ["typed-header"] }
base64 = "0.22.1"
duckdb-postgis = "0.1.2"
duckdb-postgis = "0.1.4"
martin = { git = "https://github.com/enmeshed-analytics/martin.git", features = ["postgres"] }
martin-tile-utils = { git = "https://github.com/enmeshed-analytics/martin.git" }
rand = "0.8.5"
Expand Down
42 changes: 30 additions & 12 deletions backend/src/core/layer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::core::User;
use crate::data::Database;
use anyhow::{anyhow, Result};
use serde::Serialize;
use duckdb_postgis;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::{get_unix_timestamp, Workspace, WorkspaceRole};
Expand All @@ -15,9 +16,22 @@ pub struct Layer {
pub created_at: u64,
}

#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateLayer {
pub name: String,
pub workspace_id: String,
}

impl Layer {
pub fn from_req(req: CreateLayer, user: User) -> Self {
Layer {
id: Uuid::new_v4().to_string(),
workspace_id: req.workspace_id,
name: req.name,
uploaded_by: user.id,
created_at: get_unix_timestamp(),
}
}
}

impl Layer {
Expand All @@ -26,7 +40,7 @@ impl Layer {
database: D,
user: &User,
wsp: &Workspace,
create_layer: &CreateLayer,
layer: &Layer,
) -> Result<()> {
// Get workspace member record
let requesting_member = wsp
Expand All @@ -37,18 +51,22 @@ impl Layer {
if requesting_member.role == WorkspaceRole::Read {
Err(anyhow!("User does not have permissions to create layers."))?
}
database.create_layer(layer).await?;

// TODO: Load layer into geodatabase
Ok(())
}

let layer = Layer {
id: Uuid::new_v4().to_string(),
workspace_id: wsp.clone().id,
name: create_layer.clone().name,
uploaded_by: user.clone().id,
created_at: get_unix_timestamp(),
};
database.create_layer(&layer).await?;
pub async fn send_to_postgis(self, file_path: &str) -> Result<()> {
let postgis_uri = "postgresql://admin:password@localhost:5432/gridwalk";
let schema =
duckdb_postgis::duckdb_load::launch_process_file(file_path, &self.id, postgis_uri)?;

println!("{schema:?}");
Ok(())
}

pub async fn write_record<D: Database>(self, database: D) -> Result<()> {
database.create_layer(&self).await?;
Ok(())
}
}
146 changes: 83 additions & 63 deletions backend/src/routes/layer.rs
Original file line number Diff line number Diff line change
@@ -1,82 +1,102 @@
use crate::app_state::AppState;
use crate::auth::AuthUser;
use crate::core::{Layer, Workspace, WorkspaceRole};
use crate::data::Database;
use axum::{extract::Multipart, extract::State, http::StatusCode, response::IntoResponse};
use crate::{app_state::AppState, core::CreateLayer};
use axum::{
extract::{Extension, Multipart, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use duckdb_postgis::duckdb_load::launch_process_file;
use std::path::Path;
use std::sync::Arc;
use tokio::{fs::File, io::AsyncWriteExt};
use uuid::Uuid;
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};

pub async fn upload_layer<D: Database>(
State(state): State<Arc<AppState<D>>>,
Extension(auth_user): Extension<AuthUser>,
mut multipart: Multipart,
) -> impl IntoResponse {
const MAX_FILE_SIZE: usize = 1024 * 1024 * 100; // 100 MB limit
let upload_dir = "uploads";
) -> Result<impl IntoResponse, StatusCode> {
let mut file_data = Vec::new();
let mut layer_info: Option<CreateLayer> = None;

// Ensure the upload directory exists
if let Err(err) = tokio::fs::create_dir_all(upload_dir).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create upload directory: {}", err),
)
.into_response();
while let Some(field) = multipart
.next_field()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
{
let name = field.name().ok_or(StatusCode::BAD_REQUEST)?.to_string();
match name.as_str() {
"file" => {
println!("FILE");
file_data = field
.bytes()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
.to_vec();
println!("File data size: {} bytes", file_data.len());
}
"layer_info" => {
let json_str = String::from_utf8(
field
.bytes()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
.to_vec(),
)
.map_err(|_| StatusCode::BAD_REQUEST)?;
layer_info =
Some(serde_json::from_str(&json_str).map_err(|_| StatusCode::BAD_REQUEST)?);
}
_ => {}
}
}

while let Some(mut field) = multipart.next_field().await.unwrap_or(None) {
let file_name = match field.file_name() {
Some(name) => name.to_string(),
None => {
return (StatusCode::BAD_REQUEST, "File name is required".to_string())
.into_response()
}
};
let layer_info = layer_info.ok_or(StatusCode::BAD_REQUEST)?;
let layer = Layer::from_req(layer_info, auth_user.user.clone().unwrap());
let workspace = Workspace::from_id(state.app_data.clone(), &layer.workspace_id)
.await
.unwrap();
let member = workspace
.get_member(state.app_data.clone(), auth_user.user.unwrap())
.await
.unwrap();

let file_uuid = Uuid::new_v4();
let file_extension = Path::new(&file_name)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("bin");
let unique_filename = format!("{}.{}", file_uuid, file_extension);
let file_path = Path::new(upload_dir).join(&unique_filename);
// Check requesting user workspace permissions
if member.role == WorkspaceRole::Read {
return Ok((StatusCode::FORBIDDEN, format!("")));
}

let mut file = match File::create(&file_path).await {
Ok(file) => file,
Err(err) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create file: {}", err),
)
.into_response()
}
};
// Save the file data locally
let file_name = format!("{}", layer.id);
let dir_path = Path::new("uploads");
let file_path = dir_path.join(&file_name);

let mut total_bytes = 0;
fs::create_dir_all(dir_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

while let Some(chunk) = field.chunk().await.unwrap_or(None) {
total_bytes += chunk.len();
if total_bytes > MAX_FILE_SIZE {
return (StatusCode::BAD_REQUEST, "File too large".to_string()).into_response();
}
let mut file = File::create(&file_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

if let Err(err) = file.write_all(&chunk).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to write to file: {}", err),
)
.into_response();
}
}
file.write_all(&file_data)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// Here you might want to add the file information to your database
// state.db.add_file_record(unique_filename, total_bytes).await?;
let postgis_uri = "postgresql://admin:password@localhost:5432/gridwalk";
let _processed_file = launch_process_file(file_path.to_str().unwrap(), &layer.id, postgis_uri);
println!("Uploaded to POSTGIS!");

return (
StatusCode::OK,
format!("File uploaded successfully: {}", unique_filename),
)
.into_response();
match layer.clone().write_record(state.app_data.clone()).await {
Ok(_) => {
let json_response = serde_json::to_value(&layer).unwrap();
Ok((StatusCode::OK, Json(json_response).to_string()))
}
Err(_) => Ok((StatusCode::INTERNAL_SERVER_ERROR, "".to_string())),
}

(StatusCode::BAD_REQUEST, "No file provided".to_string()).into_response()
}

2 changes: 1 addition & 1 deletion backend/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub async fn logout<D: Database>(
}
}

pub async fn profile<D: Database>(
pub async fn profile(
Extension(auth_user): Extension<AuthUser>,
) -> Result<Json<Profile>, (StatusCode, String)> {
match auth_user.user {
Expand Down
5 changes: 3 additions & 2 deletions backend/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::auth::auth_middleware;
use crate::data::Database;
use crate::routes::{
add_workspace_member, create_workspace, health_check, login, logout, profile, register,
remove_workspace_member, tiles,
remove_workspace_member, tiles, upload_layer,
};
use axum::{
middleware,
Expand All @@ -20,13 +20,14 @@ pub fn create_app<D: Database>(app_state: AppState<D>) -> Router {
Router::new()
.route("/health", get(health_check))
.route("/logout", post(logout))
.route("/profile", get(profile::<D>))
.route("/profile", get(profile))
.route("/workspace", post(create_workspace::<D>))
.route("/workspace/members", post(add_workspace_member::<D>))
.route(
"/workspace/:workspace_id/members/:user_id",
delete(remove_workspace_member::<D>),
)
.route("/upload_layer", post(upload_layer::<D>))
.route("/tiles/:z/:x/:y", get(tiles::<D>))
.layer(middleware::from_fn_with_state(
shared_state.clone(),
Expand Down
Binary file added test_files/GLA_High_Street_boundaries.gpkg
Binary file not shown.
1 change: 1 addition & 0 deletions test_files/greater-london.geo.json

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions test_files/london-center-geojson.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Center of London",
"description": "A point representing the approximate center of London"
},
"geometry": {
"type": "Point",
"coordinates": [-0.1276, 51.5074]
}
}
]
}

0 comments on commit 56090a1

Please sign in to comment.