Skip to content

Commit

Permalink
feat(Backend and UI): Progress made on fileupload logic. Changes made…
Browse files Browse the repository at this point in the history
… to upload_layer Axum endpoint and file upload modal in the front end. File uploads but more work needed to refactor code so state is tracked correctly and workspace id / auth token are passed in correctly and not dummy values used hardcoded. [2024-10-30]
  • Loading branch information
CHRISCARLON committed Oct 30, 2024
1 parent 3918108 commit a47a88b
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 154 deletions.
12 changes: 4 additions & 8 deletions gridwalk-backend/src/core/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use anyhow::{anyhow, Result};
use duckdb_postgis;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateLayer {
Expand All @@ -15,7 +14,6 @@ pub struct CreateLayer {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Layer {
pub id: String,
pub workspace_id: String,
pub name: String,
pub uploaded_by: String,
Expand All @@ -25,7 +23,6 @@ pub struct Layer {
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.clone(),
Expand All @@ -39,26 +36,25 @@ impl Layer {
user: &User,
workspace: &Workspace,
) -> Result<()> {
// Get workspace member record
// Get workspace member
let requesting_member = workspace.get_member(database, user).await?;
if requesting_member.role == WorkspaceRole::Read {
return Err(anyhow!("User does not have permissions to create layers."));
}
database.create_layer(self).await?;
Ok(())
}

pub async fn send_to_postgis(&self, file_path: &str) -> Result<()> {
let postgis_uri = std::env::var("POSTGIS_URI")
.map_err(|_| anyhow!("PostGIS URI not set in environment variables"))?;
let postgis_uri = "postgresql://admin:password@localhost:5432/gridwalk";
let schema = duckdb_postgis::duckdb_load::launch_process_file(
file_path,
&self.id,
&self.name,
&postgis_uri,
&self.workspace_id,
)
.map_err(|e| anyhow!("Failed to send file to PostGIS: {:?}", e))?;
println!("{:?}", schema);
println!("Uploaded to POSTGIS BABY!");
Ok(())
}

Expand Down
2 changes: 0 additions & 2 deletions gridwalk-backend/src/data/dynamodb/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,14 +420,12 @@ impl UserStore for Dynamodb {
}

async fn create_layer(&self, layer: &Layer) -> Result<()> {
// Create the workspace member item to insert
let mut item = std::collections::HashMap::new();

item.insert(
String::from("PK"),
AV::S(format!("WSP#{}", layer.workspace_id)),
);
item.insert(String::from("SK"), AV::S(format!("LAYER#{}", layer.id)));
item.insert(String::from("name"), AV::S(layer.clone().name));
item.insert(
String::from("uploaded_by"),
Expand Down
28 changes: 13 additions & 15 deletions gridwalk-backend/src/routes/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ use axum::{
response::IntoResponse,
Json,
};
use duckdb_postgis::duckdb_load::launch_process_file;
use std::path::Path;
use std::sync::Arc;
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};

// Remove the generic type parameter and use Arc<AppState> directly
pub async fn upload_layer(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Expand Down Expand Up @@ -77,7 +75,9 @@ pub async fn upload_layer(

// Save file locally
let dir_path = Path::new("uploads");
let file_path = dir_path.join(&layer.id);
let file_path = dir_path.join(&layer.name);
let file_path_str = file_path.to_str().unwrap();

fs::create_dir_all(dir_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Expand All @@ -90,23 +90,21 @@ pub async fn upload_layer(
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// Process file and upload to PostGIS
let postgis_uri = "postgresql://admin:password@localhost:5432/gridwalk";
launch_process_file(
file_path.to_str().unwrap(),
&layer.id,
postgis_uri,
&layer.workspace_id,
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
println!("Uploaded to POSTGIS!");

// Write layer record to database using Arc<dyn Database>
// Check permissions of user
layer
.create(&state.app_data, user, &workspace)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// Write layer table to PostGis
layer
.send_to_postgis(file_path_str)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// Write layer record to App Database
layer.write_record(&state.app_data).await.ok();

// Return success response
let json_response =
serde_json::to_value(&layer).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Expand Down
66 changes: 40 additions & 26 deletions gridwalk-ui/src/app/api/upload/layer/route.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
import { NextRequest } from "next/server";

// Hardcoded auth token - replace with your actual token
const HARDCODED_AUTH_TOKEN = "";
const HARDCODED_AUTH_TOKEN = "VVPME0BYEDG7LJYGLL9PKJ8AS1GABM";

interface LayerInfo {
workspace_id: string;
name: string;
}

export async function POST(request: NextRequest) {
try {
const formData = await request.formData();

// Validation step
const file = formData.get("file");
const workspaceId = formData.get("workspace_id");
const layerName = formData.get("name");

// Early validation return
if (!file || !(file instanceof File)) {
return new Response(
JSON.stringify({
success: false,
error: "No file provided",
}),
return Response.json(
{ success: false, error: "No file provided" },
{ status: 400 },
);
}

if (!workspaceId || !layerName) {
return Response.json(
{ success: false, error: "Missing workspace_id or name" },
{ status: 400 },
);
}

// Create a new FormData for the API call
// Transform for Rust backend
const layerInfo: LayerInfo = {
workspace_id: workspaceId.toString(),
name: layerName.toString(),
};

const apiFormData = new FormData();
apiFormData.append("file", file);
apiFormData.append(
"layer_info",
new Blob([JSON.stringify(layerInfo)], { type: "application/json" }),
);

// Hardcoded values
apiFormData.append("workspace_id", "");
apiFormData.append("name", "my-awesome-layer");

// Send to backend
const response = await fetch("http://localhost:3001/upload_layer", {
method: "POST",
headers: {
Expand All @@ -35,27 +54,22 @@ export async function POST(request: NextRequest) {
});

if (!response.ok) {
throw new Error("Upload failed");
const errorText = await response.text();
throw new Error(errorText || response.statusText);
}

console.log("SUCCESS");

const data = await response.json();
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
});
return Response.json({ success: true, data });
} catch (error) {
console.error("Upload error:", error);
return new Response(
JSON.stringify({
return Response.json(
{
success: false,
error: "Upload failed",
}),
error: error instanceof Error ? error.message : "Upload failed",
},
{ status: 500 },
);
}
}

export const config = {
api: {
bodyParser: false,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
File,
X,
Upload,
Trash2,
CheckCircle,
} from "lucide-react";
import { ModalProps, MainMapNav } from "./types";

Expand All @@ -16,11 +16,12 @@ const MapModal: React.FC<ModalProps> = ({
onClose,
onNavItemClick,
selectedItem,
layers,
// layers,
onLayerUpload,
onLayerDelete,
// onLayerDelete,
isUploading,
error,
uploadSuccess,
}) => {
const MainMapNavs: MainMapNav[] = [
{
Expand Down Expand Up @@ -99,43 +100,32 @@ const MapModal: React.FC<ModalProps> = ({
type="file"
className="hidden"
onChange={handleFileChange}
accept=".geojson,.json,.kml,.gpx"
accept=".geojson,.json,.gpkg"
disabled={isUploading}
/>
</label>
</div>

{/* Status Messages */}
{isUploading && (
<div className="mb-4 text-blue-500">Uploading...</div>
<div className="mb-4 text-blue-500 flex items-center">
<span className="animate-spin mr-2"></span>
Uploading...
</div>
)}
{error && <div className="mb-4 text-red-500">{error}</div>}

{/* Layers List */}
<div className="space-y-2">
{layers.length === 0 ? (
<p className="text-gray-500 text-sm">No layers uploaded yet</p>
) : (
layers.map((layer) => (
<div
key={layer.id}
className="flex items-center justify-between p-3 bg-white rounded-lg shadow-sm"
>
<div>
<p className="font-medium text-gray-800">{layer.name}</p>
<p className="text-sm text-gray-500">{layer.type}</p>
</div>
<button
onClick={() => onLayerDelete(layer.id)}
className="p-1 hover:bg-red-50 rounded-full text-red-500 transition-colors"
aria-label="Delete layer"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))
)}
</div>
{error && (
<div className="mb-4 text-red-500 flex items-center">
{error}
</div>
)}

{uploadSuccess && !isUploading && !error && (
<div className="mb-4 text-green-500 flex items-center">
<CheckCircle className="w-4 h-4 mr-2" />
File uploaded successfully!
</div>
)}
</div>
);

Expand Down
3 changes: 2 additions & 1 deletion gridwalk-ui/src/app/project/components/navBars/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ export interface ModalProps {
onClose: () => void;
onNavItemClick: (item: MainMapNav) => void;
selectedItem: MainMapNav | null;
layers: LayerUpload[];
// layers: LayerUpload[];
onLayerUpload: (file: File) => Promise<void>;
onLayerDelete: (layerId: string) => void;
isUploading: boolean;
error: string | null;
uploadSuccess: boolean;
}

/* Map Edit Items */
Expand Down
Loading

0 comments on commit a47a88b

Please sign in to comment.