From 6117cc55c9f54fbf6b32f81b1c15baf23975418a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 6 Jun 2025 13:31:41 +0100 Subject: [PATCH 1/2] Implemented openmvgo --- app/Dockerfile.dev | 49 ++- app/go.mod | 5 +- app/go.sum | 4 + app/services/task_service.go | 2 +- app/services/task_service_impl.go | 701 +++++++++++++++++------------- 5 files changed, 433 insertions(+), 328 deletions(-) diff --git a/app/Dockerfile.dev b/app/Dockerfile.dev index 34c623e..193d917 100644 --- a/app/Dockerfile.dev +++ b/app/Dockerfile.dev @@ -43,40 +43,49 @@ RUN git clone --branch develop --depth=1 https://github.com/cdcseacave/openMVS.g # ------------------------------ # Stage 3: Build Blender (minimal) # ------------------------------ -FROM ubuntu:22.04 AS blender_builder - -RUN apt-get update && apt-get install -y --no-install-recommends \ - wget xz-utils && \ - rm -rf /var/lib/apt/lists/* - -RUN wget -q https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz --no-check-certificate && \ - mkdir -p /opt/blender && \ - tar -xf blender-4.4.0-linux-x64.tar.xz -C /opt/blender --strip-components=1 && \ - rm blender-4.4.0-linux-x64.tar.xz +FROM linuxserver/blender:4.4.3 AS blender_builder # ------------------------- # Stage 4: Dev Environment # ------------------------- -FROM ubuntu:latest AS final +FROM ubuntu:22.04 AS final + +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN sed -i'' 's/archive\.ubuntu\.com/us\.archive\.ubuntu\.com/' /etc/apt/sources.list -RUN apt-get update && apt-get install -y wget xz-utils libcgal-qt5-dev \ - libceres-dev libboost-all-dev libopencv-dev build-essential && \ - wget https://download.blender.org/release/Blender4.4/blender-4.4.0-linux-x64.tar.xz && \ - wget https://go.dev/dl/go1.23.8.linux-amd64.tar.gz && \ +# Create non-root user +RUN groupadd -r appuser && useradd -r -g appuser -s /bin/false appuser + +ENV DEBIAN_FRONTEND=noninteractive +ARG DEBIAN_FRONTEND=noninteractive + +# Install only runtime dependencies +# Seperate for cache issues +RUN apt-get -y update +RUN apt-get install -y --no-install-recommends \ + libcgal-qt5-dev libceres2 libboost-system1.74.0 libboost-filesystem1.74.0 \ + build-essential \ + libboost-program-options1.74.0 libboost-serialization1.74.0 \ + libopencv-core4.5d libopencv-imgproc4.5d libopencv-imgcodecs4.5d \ + libjpeg8 libpng16-16 libtiff5 libglu1-mesa libglew2.2 \ + libglfw3 libgomp1 ca-certificates curl wget libboost-all-dev libopencv-dev \ + xorg && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN wget https://go.dev/dl/go1.23.8.linux-amd64.tar.gz && \ tar -C /usr/local -xzf go1.23.8.linux-amd64.tar.gz && \ rm -rf go1.23.8.linux-amd64.tar.gz && \ apt-get clean && rm -rf /var/lib/apt/lists/* -# RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server -# RUN chmod +x ./server - # Copy OpenMVG and OpenMVS builds from cv_builder COPY --from=cv_builder /openMVG_build/Linux-x86_64-RELEASE /usr/local/bin COPY --from=cv_builder /openMVS_build/bin /usr/local/bin # Copy Blender (only essential parts) -COPY --from=blender_builder /opt/blender/blender /usr/local/bin/blender -COPY --from=blender_builder /opt/blender/4.4/ /opt/blender/4.4/ +COPY --from=blender_builder /blender /opt/blender +ENV PATH="/opt/blender:$PATH" ENV PATH="$PATH:/usr/local/go/bin:/root/go/bin" diff --git a/app/go.mod b/app/go.mod index 1e41eed..e4d3a8b 100644 --- a/app/go.mod +++ b/app/go.mod @@ -1,11 +1,10 @@ module github.com/Soup666/modelmaker -go 1.24 - -toolchain go1.24.2 +go 1.24.3 require ( firebase.google.com/go/v4 v4.15.2 + github.com/2024-dissertation/openmvgo v1.0.5 github.com/appleboy/go-fcm v1.2.5 github.com/bodgit/sevenzip v1.6.1 github.com/gin-gonic/gin v1.10.0 diff --git a/app/go.sum b/app/go.sum index a840b59..af41c3e 100644 --- a/app/go.sum +++ b/app/go.sum @@ -43,6 +43,8 @@ cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8W dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= firebase.google.com/go/v4 v4.15.2 h1:KJtV4rAfO2CVCp40hBfVk+mqUqg7+jQKx7yOgFDnXBg= firebase.google.com/go/v4 v4.15.2/go.mod h1:qkD/HtSumrPMTLs0ahQrje5gTw2WKFKrzVFoqy4SbKA= +github.com/2024-dissertation/openmvgo v1.0.5 h1:juKy2McCKfqj7eZSLbKhxk1UQqDxu+6JdwB/4hn41fQ= +github.com/2024-dissertation/openmvgo v1.0.5/go.mod h1:J/Jz76PZhkdjLc7+p3mp7BnpkxbWWVeKQDM5SlvpcCo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= @@ -304,6 +306,8 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= diff --git a/app/services/task_service.go b/app/services/task_service.go index fa636db..23bec42 100644 --- a/app/services/task_service.go +++ b/app/services/task_service.go @@ -18,7 +18,7 @@ type TaskService interface { UnarchiveTask(taskID uint) (*model.Task, error) SaveTask(task *model.Task) error FailTask(task *model.Task, message string) error - RunPhotogrammetryProcess(task *model.Task) error + // RunPhotogrammetryProcess(task *model.Task) error UpdateMeta(task *model.Task, key string, value interface{}) error FullyLoadTask(task *model.Task) (*model.Task, error) SendMessage(taskID uint, message string, sender string) (*model.ChatMessage, error) diff --git a/app/services/task_service_impl.go b/app/services/task_service_impl.go index 4da29dc..b25808a 100644 --- a/app/services/task_service_impl.go +++ b/app/services/task_service_impl.go @@ -9,6 +9,9 @@ import ( "path/filepath" "time" + "github.com/2024-dissertation/openmvgo/pkg/openmvg" + "github.com/2024-dissertation/openmvgo/pkg/openmvs" + "github.com/2024-dissertation/openmvgo/pkg/utils" models "github.com/Soup666/modelmaker/model" repositories "github.com/Soup666/modelmaker/repository" "gorm.io/gorm" @@ -154,303 +157,303 @@ func (s *TaskServiceImpl) FailTask(task *models.Task, message string) error { return nil } -func (s *TaskServiceImpl) RunPhotogrammetryProcess(task *models.Task) error { - - if task.Status == models.INPROGRESS { - log.Printf("Task %d is already in progress\n", task.ID) - return nil - } - - startTime := time.Now() - - TASK_COUNT := 7.0 - CURRENT_TASK := 0.0 - - inputPath := filepath.Join("uploads", fmt.Sprintf("%d", task.ID)) - outputPath := filepath.Join("objects", fmt.Sprintf("%d", task.ID)) - mvsPath := filepath.Join(outputPath, "mvs") - - task.Status = models.INPROGRESS - if err := s.UpdateTask(task); err != nil { - log.Printf("Failed to update task status to INPROGRESS: %v\n", err) - return err - } - - // Clear the build directory - if err := os.RemoveAll(outputPath); err != nil { - s.FailTask(task, fmt.Sprintf("Failed to clear directory %s: %v", outputPath, err)) - return err - } - - if err := os.MkdirAll(outputPath, os.ModePerm); err != nil { - s.FailTask(task, fmt.Sprintf("Failed to create directory %s: %v", outputPath, err)) - return err - } - - if err := os.MkdirAll(mvsPath, os.ModePerm); err != nil { - s.FailTask(task, fmt.Sprintf("Failed to create directory %s: %v", mvsPath, err)) - return err - } - - CURRENT_TASK = CURRENT_TASK + 1.0 - - // 1 - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - - log.Println("# 1 ./bin/SfM_SequentialPipeline.py", inputPath, outputPath, "--opensfm-processes", "8") - cmd := exec.Command("./bin/SfM_SequentialPipeline.py", inputPath, outputPath, "--opensfm-processes", "8") - - var stdoutBuffer saveOutput - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err := cmd.Run() - - if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - if err != nil { - s.FailTask(task, fmt.Sprintf("SfM_SequentialPipeline failed: %s", err)) - return err - } - - // 2 - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - CURRENT_TASK = CURRENT_TASK + 1.0 - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: task.Title + " - Processing started", - Title: task.Title, - }) - - log.Println("# 2 openMVG_main_openMVG2openMVS", "-i", filepath.Join(outputPath, "reconstruction_sequential/sfm_data.bin"), "-o", filepath.Join(mvsPath, "scene.mvs"), inputPath, outputPath, "-d", mvsPath) - cmd = exec.Command("openMVG_main_openMVG2openMVS", "-i", filepath.Join(outputPath, "reconstruction_sequential/sfm_data.bin"), "-o", filepath.Join(mvsPath, "scene.mvs"), inputPath, outputPath, "-d", mvsPath) - - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err = cmd.Run() - - if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - if err != nil { - log.Println() - s.FailTask(task, fmt.Sprintf("openMVG_main_openMVG2openMVS failed: %s", err)) - return err - } - - // 3 - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - CURRENT_TASK = CURRENT_TASK + 1.0 - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Step 2/7 started", - Title: task.Title, - }) - - log.Println("# 3 DensifyPointCloud", "scene.mvs", "-o", "scene_dense.mvs", "-w", mvsPath, "--max-threads", "1") - cmd = exec.Command("DensifyPointCloud", "scene.mvs", "-o", "scene_dense.mvs", "-w", mvsPath, "--max-threads", "1") - - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err = cmd.Run() - - if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - if err != nil { - s.FailTask(task, fmt.Sprintf("DensifyPointCloud failed: %s", err)) - return err - } - - // 4 - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - CURRENT_TASK = CURRENT_TASK + 1.0 - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Step 3/7 started", - Title: task.Title, - }) - - log.Println("# 4 ReconstructMesh", "scene_dense.mvs", "-o", "scene_mesh.ply", "-w", mvsPath) - cmd = exec.Command("ReconstructMesh", "scene_dense.mvs", "-o", "scene_mesh.ply", "-w", mvsPath) - - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err = cmd.Run() - - if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - if err != nil { - s.FailTask(task, fmt.Sprintf("ReconstructMesh failed: %v", err)) - return err - } - - // 5 - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - if err := s.AddLog(task.ID, "ReconstructMesh started"); err != nil { - log.Printf("Failed to add log: %v\n", err) - } - CURRENT_TASK = CURRENT_TASK + 1.0 - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Step 4/7 started", - Title: task.Title, - }) - - log.Println("# 5 RefineMesh", "scene.mvs", "-m", "scene_mesh.ply", "-o", "scene_dense_mesh_refine.mvs", "-w", mvsPath, "--scales", "1", "--max-face-area", "16", "--max-threads", "1") - cmd = exec.Command("RefineMesh", "scene.mvs", "-m", "scene_mesh.ply", "-o", "scene_dense_mesh_refine.mvs", "-w", mvsPath, "--scales", "1", "--max-face-area", "16", "--max-threads", "1") - - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err = cmd.Run() - - if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - if err != nil { - s.FailTask(task, fmt.Sprintf("RefineMesh failed: %v", err)) - return err - } - - // 6 - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - - if err := s.AddLog(task.ID, "TextureMesh started"); err != nil { - log.Printf("Failed to add log: %v\n", err) - } - CURRENT_TASK = CURRENT_TASK + 1.0 - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Step 5/7 started", - Title: task.Title, - }) - - log.Println("# 6 TextureMesh", "scene_dense.mvs", "-m", "scene_dense_mesh_refine.ply", "-o", "scene_dense_mesh_refine_texture.mvs", "-w", mvsPath, "--export-type", "obj") - cmd = exec.Command("TextureMesh", "scene_dense.mvs", "-m", "scene_dense_mesh_refine.ply", "-o", "scene_dense_mesh_refine_texture.mvs", "-w", mvsPath, "--export-type", "obj") - - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err = cmd.Run() - - if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - if err != nil { - s.FailTask(task, fmt.Sprintf("TextureMesh failed: %v", err)) - return err - } - - // 7 - - CURRENT_TASK = CURRENT_TASK + 1.0 - - log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) - if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { - log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) - return err - } - - if err := s.AddLog(task.ID, "MeshConversion started"); err != nil { - log.Printf("Failed to add log: %v\n", err) - } - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Step 6/7 started", - Title: task.Title, - }) - - fileName := filepath.Join(mvsPath, "final_model") - fmt.Println("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", filepath.Join(mvsPath, "scene_dense_mesh_refine_texture.obj"), fileName) - cmd = exec.Command("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", filepath.Join(mvsPath, "scene_dense_mesh_refine_texture.obj"), fileName) - - cmd.Stdout = &stdoutBuffer - cmd.Stderr = &stdoutBuffer - err = cmd.Run() - - if err != nil { - s.FailTask(task, fmt.Sprintf("MeshConversion failed: %v", err)) - return err - } - - if err := s.UpdateMeta(task, "log", stdoutBuffer); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return err - } - - mesh, err := s.appFileService.Save(&models.AppFile{ - Url: fileName + ".glb", - Filename: "final_model.glb", - TaskId: task.ID, - FileType: "mesh", - }) - - if err != nil { - s.FailTask(task, fmt.Sprintf("Failed to Save mesh: %v", err)) - return err - } - - task.Mesh = mesh - task.Completed = true - task.Status = models.SUCCESS - - if err := s.UpdateTask(task); err != nil { - s.FailTask(task, fmt.Sprintf("Failed to update task: %v", err)) - return err - } - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Step Scan finished", - Title: task.Title, - }) - - log.Println("Task updated successfully.") - log.Printf("Processing completed in %s\n", time.Since(startTime)) - return nil -} +// func (s *TaskServiceImpl) RunPhotogrammetryProcess(task *models.Task) error { + +// if task.Status == models.INPROGRESS { +// log.Printf("Task %d is already in progress\n", task.ID) +// return nil +// } + +// startTime := time.Now() + +// TASK_COUNT := 7.0 +// CURRENT_TASK := 0.0 + +// inputPath := filepath.Join("uploads", fmt.Sprintf("%d", task.ID)) +// outputPath := filepath.Join("objects", fmt.Sprintf("%d", task.ID)) +// mvsPath := filepath.Join(outputPath, "mvs") + +// task.Status = models.INPROGRESS +// if err := s.UpdateTask(task); err != nil { +// log.Printf("Failed to update task status to INPROGRESS: %v\n", err) +// return err +// } + +// // Clear the build directory +// if err := os.RemoveAll(outputPath); err != nil { +// s.FailTask(task, fmt.Sprintf("Failed to clear directory %s: %v", outputPath, err)) +// return err +// } + +// if err := os.MkdirAll(outputPath, os.ModePerm); err != nil { +// s.FailTask(task, fmt.Sprintf("Failed to create directory %s: %v", outputPath, err)) +// return err +// } + +// if err := os.MkdirAll(mvsPath, os.ModePerm); err != nil { +// s.FailTask(task, fmt.Sprintf("Failed to create directory %s: %v", mvsPath, err)) +// return err +// } + +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// // 1 +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } + +// log.Println("# 1 ./bin/SfM_SequentialPipeline.py", inputPath, outputPath, "--opensfm-processes", "8") +// cmd := exec.Command("./bin/SfM_SequentialPipeline.py", inputPath, outputPath, "--opensfm-processes", "8") + +// var stdoutBuffer saveOutput +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err := cmd.Run() + +// if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("SfM_SequentialPipeline failed: %s", err)) +// return err +// } + +// // 2 +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: task.Title + " - Processing started", +// Title: task.Title, +// }) + +// log.Println("# 2 openMVG_main_openMVG2openMVS", "-i", filepath.Join(outputPath, "reconstruction_sequential/sfm_data.bin"), "-o", filepath.Join(mvsPath, "scene.mvs"), inputPath, outputPath, "-d", mvsPath) +// cmd = exec.Command("openMVG_main_openMVG2openMVS", "-i", filepath.Join(outputPath, "reconstruction_sequential/sfm_data.bin"), "-o", filepath.Join(mvsPath, "scene.mvs"), inputPath, outputPath, "-d", mvsPath) + +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err = cmd.Run() + +// if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// if err != nil { +// log.Println() +// s.FailTask(task, fmt.Sprintf("openMVG_main_openMVG2openMVS failed: %s", err)) +// return err +// } + +// // 3 +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: "Step 2/7 started", +// Title: task.Title, +// }) + +// log.Println("# 3 DensifyPointCloud", "scene.mvs", "-o", "scene_dense.mvs", "-w", mvsPath, "--max-threads", "1") +// cmd = exec.Command("DensifyPointCloud", "scene.mvs", "-o", "scene_dense.mvs", "-w", mvsPath, "--max-threads", "1") + +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err = cmd.Run() + +// if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("DensifyPointCloud failed: %s", err)) +// return err +// } + +// // 4 +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: "Step 3/7 started", +// Title: task.Title, +// }) + +// log.Println("# 4 ReconstructMesh", "scene_dense.mvs", "-o", "scene_mesh.ply", "-w", mvsPath) +// cmd = exec.Command("ReconstructMesh", "scene_dense.mvs", "-o", "scene_mesh.ply", "-w", mvsPath) + +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err = cmd.Run() + +// if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("ReconstructMesh failed: %v", err)) +// return err +// } + +// // 5 +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } +// if err := s.AddLog(task.ID, "ReconstructMesh started"); err != nil { +// log.Printf("Failed to add log: %v\n", err) +// } +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: "Step 4/7 started", +// Title: task.Title, +// }) + +// log.Println("# 5 RefineMesh", "scene.mvs", "-m", "scene_mesh.ply", "-o", "scene_dense_mesh_refine.mvs", "-w", mvsPath, "--scales", "1", "--max-face-area", "16", "--max-threads", "1") +// cmd = exec.Command("RefineMesh", "scene.mvs", "-m", "scene_mesh.ply", "-o", "scene_dense_mesh_refine.mvs", "-w", mvsPath, "--scales", "1", "--max-face-area", "16", "--max-threads", "1") + +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err = cmd.Run() + +// if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("RefineMesh failed: %v", err)) +// return err +// } + +// // 6 +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } + +// if err := s.AddLog(task.ID, "TextureMesh started"); err != nil { +// log.Printf("Failed to add log: %v\n", err) +// } +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: "Step 5/7 started", +// Title: task.Title, +// }) + +// log.Println("# 6 TextureMesh", "scene_dense.mvs", "-m", "scene_dense_mesh_refine.ply", "-o", "scene_dense_mesh_refine_texture.mvs", "-w", mvsPath, "--export-type", "obj") +// cmd = exec.Command("TextureMesh", "scene_dense.mvs", "-m", "scene_dense_mesh_refine.ply", "-o", "scene_dense_mesh_refine_texture.mvs", "-w", mvsPath, "--export-type", "obj") + +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err = cmd.Run() + +// if err := s.UpdateMeta(task, "log", stdoutBuffer.savedOutput); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("TextureMesh failed: %v", err)) +// return err +// } + +// // 7 + +// CURRENT_TASK = CURRENT_TASK + 1.0 + +// log.Println("Updating meta for task:", task.ID, " - ", CURRENT_TASK, "/", TASK_COUNT) +// if err := s.UpdateMeta(task, "opensfm-process", (CURRENT_TASK/TASK_COUNT)*100.0); err != nil { +// log.Printf("Failed to update meta: %f: %v\n", CURRENT_TASK, err) +// return err +// } + +// if err := s.AddLog(task.ID, "MeshConversion started"); err != nil { +// log.Printf("Failed to add log: %v\n", err) +// } + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: "Step 6/7 started", +// Title: task.Title, +// }) + +// fileName := filepath.Join(mvsPath, "final_model") +// fmt.Println("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", filepath.Join(mvsPath, "scene_dense_mesh_refine_texture.obj"), fileName) +// cmd = exec.Command("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", filepath.Join(mvsPath, "scene_dense_mesh_refine_texture.obj"), fileName) + +// cmd.Stdout = &stdoutBuffer +// cmd.Stderr = &stdoutBuffer +// err = cmd.Run() + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("MeshConversion failed: %v", err)) +// return err +// } + +// if err := s.UpdateMeta(task, "log", stdoutBuffer); err != nil { +// log.Printf("Failed to update meta: log: %v\n", err) +// return err +// } + +// mesh, err := s.appFileService.Save(&models.AppFile{ +// Url: fileName + ".glb", +// Filename: "final_model.glb", +// TaskId: task.ID, +// FileType: "mesh", +// }) + +// if err != nil { +// s.FailTask(task, fmt.Sprintf("Failed to Save mesh: %v", err)) +// return err +// } + +// task.Mesh = mesh +// task.Completed = true +// task.Status = models.SUCCESS + +// if err := s.UpdateTask(task); err != nil { +// s.FailTask(task, fmt.Sprintf("Failed to update task: %v", err)) +// return err +// } + +// s.notificationService.SendMessage(&models.Notification{ +// UserID: task.UserId, +// Message: "Step Scan finished", +// Title: task.Title, +// }) + +// log.Println("Task updated successfully.") +// log.Printf("Processing completed in %s\n", time.Since(startTime)) +// return nil +// } func (s *TaskServiceImpl) GetTaskFiles(taskID uint, fileType string) ([]models.AppFile, error) { files, err := s.appFileService.GetTaskFiles(taskID, fileType) @@ -526,13 +529,103 @@ func (s *TaskServiceImpl) EnqueueJob(job TaskJob) bool { func (s *TaskServiceImpl) StartWorker() { go func() { for job := range s.jobQueue { - fmt.Printf("Processing job: %+v\n", job) - - task, err := s.taskRepo.GetTaskByID(job.ID) - if err != nil { - continue - } - s.RunPhotogrammetryProcess(task) + go func() { + fmt.Printf("Processing job: %+v\n", job) + + task, err := s.taskRepo.GetTaskByID(job.ID) + if err != nil { + return + } + + // Setup Utils + utils := utils.NewUtils() + + timestamp := time.Now().Unix() + + // Middle directory creation + buildDir, err := os.MkdirTemp("", fmt.Sprintf("%dbuild", timestamp)) + utils.Check(err) + defer os.RemoveAll(buildDir) + + inputDir := filepath.Join("uploads", fmt.Sprintf("%d", task.ID)) + outputDir := filepath.Join("objects", fmt.Sprintf("%d", task.ID)) + cameraDBFile := filepath.Join("bin", "sensor_width_camera_database.txt") + + // Configure openmvg service + openmvgService := openmvg.NewOpenMVGService( + openmvg.NewOpenMVGConfig( + inputDir, + buildDir, + &cameraDBFile, + ), + utils, + ) + + // Configure openmvs service + openmvsService := openmvs.NewOpenMVSService( + openmvs.NewOpenMVSConfig( + outputDir, + buildDir, + 0, + ), + utils, + ) + + // Populate and Run Pipelines + openmvgService.PopulateTmpDir() + defer os.RemoveAll(openmvgService.Config.MatchesDir) + defer os.RemoveAll(openmvgService.Config.ReconstructionDir) + + openmvgService.SfMSequentialPipeline() + openmvsService.RunPipeline() + + fileName := filepath.Join(outputDir, "final.obj") + convertedFileName := filepath.Join(outputDir, "final.glb") + fmt.Println("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", fileName, convertedFileName) + cmd := exec.Command("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", fileName, convertedFileName) + + err = cmd.Run() + + if err != nil { + s.FailTask(task, fmt.Sprintf("MeshConversion failed: %v", err)) + return + } + + if err := s.UpdateMeta(task, "log", "MeshConversion started"); err != nil { + log.Printf("Failed to update meta: log: %v\n", err) + return + } + + mesh, err := s.appFileService.Save(&models.AppFile{ + Url: fileName + ".glb", + Filename: "final.glb", + TaskId: task.ID, + FileType: "mesh", + }) + + if err != nil { + s.FailTask(task, fmt.Sprintf("Failed to Save mesh: %v", err)) + return + } + + task.Mesh = mesh + task.Completed = true + task.Status = models.SUCCESS + + if err := s.UpdateTask(task); err != nil { + s.FailTask(task, fmt.Sprintf("Failed to update task: %v", err)) + return + } + + s.notificationService.SendMessage(&models.Notification{ + UserID: task.UserId, + Message: "Scan finished", + Title: task.Title, + }) + + // Complete + fmt.Println("OpenMVGO pipeline completed successfully!") + }() } }() } From 38b3c4c723083b5e7e6e989a24bf73306af72899 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 6 Jun 2025 20:30:18 +0100 Subject: [PATCH 2/2] aws s3 implementaation --- app/controller/object_controller.go | 47 ++- app/controller/task_controller.go | 67 ++-- app/controller/task_controller_test.go | 3 +- app/controller/upload_controller.go | 72 ++-- app/go.mod | 18 + app/go.sum | 36 ++ app/main.go | 36 +- app/mocks/storage_service_mock.go | 41 +++ app/router/router.go | 27 ++ app/seeds/seeds/files.go | 2 +- app/services/katapult_storage_service.go | 118 ++++++ app/services/storage_service.go | 21 ++ app/services/task_service_impl.go | 445 +++++++++++++---------- app/services/task_service_impl_test.go | 3 +- app/uploads/.keep | 0 docker-compose-dev.yml | 2 + 16 files changed, 665 insertions(+), 273 deletions(-) create mode 100644 app/mocks/storage_service_mock.go create mode 100644 app/services/katapult_storage_service.go create mode 100644 app/services/storage_service.go delete mode 100644 app/uploads/.keep diff --git a/app/controller/object_controller.go b/app/controller/object_controller.go index f15256b..efd0c6a 100644 --- a/app/controller/object_controller.go +++ b/app/controller/object_controller.go @@ -2,31 +2,56 @@ package controller import ( "fmt" + "io" "net/http" - "os" + "strconv" + "github.com/Soup666/modelmaker/services" "github.com/gin-gonic/gin" ) // AuthController is the controller for handling authentication requests -type ObjectController struct{} +type ObjectController struct { + storageService services.StorageService +} -func NewObjectController() *ObjectController { - return &ObjectController{} +func NewObjectController(storageService services.StorageService) *ObjectController { + return &ObjectController{ + storageService: storageService, + } } func (c *ObjectController) GetObject(ctx *gin.Context) { taskId := ctx.Param("taskID") - // Construct the full file path - filePath := fmt.Sprintf("objects/%s/%s", taskId, "mvs/final_model.glb") + // Convert taskId to uint + taskIdInt, err := strconv.ParseUint(taskId, 10, 32) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) + return + } - // Check if the file exists - if _, err := os.Stat(filePath); os.IsNotExist(err) { + // Get file from object storage + file, err := c.storageService.GetFile(fmt.Sprintf("objects/%d/final.glb", taskIdInt)) + if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "Object not found"}) return } - - // Serve the file - ctx.File(filePath) + defer file.Close() + + // Add headers + ctx.Header("Content-Type", "model/gltf-binary") + ctx.Header("Content-Disposition", "attachment; filename=final.glb") + + // // Stream the file to the response + // ctx.Stream(func(w io.Writer) bool { + // _, err := io.Copy(w, file) + // return err == nil + // }) + // Copy file contents to response writer + _, err = io.Copy(ctx.Writer, file) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stream file"}) + return + } } diff --git a/app/controller/task_controller.go b/app/controller/task_controller.go index cff8380..2362958 100644 --- a/app/controller/task_controller.go +++ b/app/controller/task_controller.go @@ -3,6 +3,7 @@ package controller import ( "errors" "fmt" + "io" "log" "mime/multipart" "net/http" @@ -23,10 +24,16 @@ type TaskController struct { TaskService services.TaskService AppFileService services.AppFileService VisionService services.VisionService + StorageService services.StorageService } -func NewTaskController(taskService services.TaskService, appFileService services.AppFileService, visionService services.VisionService) *TaskController { - return &TaskController{TaskService: taskService, AppFileService: appFileService, VisionService: visionService} +func NewTaskController(taskService services.TaskService, appFileService services.AppFileService, visionService services.VisionService, storageService services.StorageService) *TaskController { + return &TaskController{ + TaskService: taskService, + AppFileService: appFileService, + VisionService: visionService, + StorageService: storageService, + } } func (c *TaskController) GetUnarchivedTasks(ctx *gin.Context) { @@ -136,7 +143,6 @@ func (c *TaskController) CreateTask(ctx *gin.Context) { // @Failure 500 {object} map[string]string // @Router /tasks/{taskID}/upload [post] func (c *TaskController) UploadFileToTask(ctx *gin.Context) { - // Get the Task ID from the route taskIdParam := ctx.Param("taskID") taskId, err := strconv.Atoi(taskIdParam) @@ -164,13 +170,6 @@ func (c *TaskController) UploadFileToTask(ctx *gin.Context) { return } - // Define the upload folder - folderPath := fmt.Sprintf("uploads/%d", taskId) - if err := os.MkdirAll(folderPath, os.ModePerm); err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"}) - return - } - var uploadedImages []model.AppFile var wg sync.WaitGroup var mu sync.Mutex @@ -192,12 +191,9 @@ func (c *TaskController) UploadFileToTask(ctx *gin.Context) { return } - // Generate a unique filename - filename := fmt.Sprintf("%d-%d%s", taskId, index, fileExt) - savePath := filepath.Join(folderPath, filename) - - // Save the file - if err := ctx.SaveUploadedFile(file, savePath); err != nil { + // Upload to object storage + url, err := c.StorageService.UploadFile(file, uint(taskId), "upload") + if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save file %s", file.Filename)}) hasError = true return @@ -205,8 +201,8 @@ func (c *TaskController) UploadFileToTask(ctx *gin.Context) { // Save metadata to DB image := model.AppFile{ - Filename: filename, - Url: fmt.Sprintf("/uploads/%d/%s", taskId, filename), + Filename: file.Filename, + Url: url, TaskId: uint(taskId), FileType: "upload", } @@ -232,37 +228,42 @@ func (c *TaskController) UploadFileToTask(ctx *gin.Context) { tx.Commit() go func() { - // Generate caption - result, err := c.VisionService.AnalyseImage(fmt.Sprintf("./uploads/%d/%s", taskId, uploadedImages[0].Filename), "") + // Get the first image URL from object storage + file, err := c.StorageService.GetFile(fmt.Sprintf("uploads/%d/%s", taskId, uploadedImages[0].Filename)) + if err != nil { + log.Printf("Unable to get image for analysis: %v", err) + return + } + defer file.Close() + // Create a temporary file + tempFile, err := os.CreateTemp("", "analysis-*.jpg") if err != nil { - log.Printf("Unable to analyze the image: %v", err) + log.Printf("Unable to create temp file: %v", err) return } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() - if err := c.TaskService.UpdateMeta(&task, "ai-description", result); err != nil { - log.Printf("Failed to update task metadata: %v", err) + // Copy the file content + if _, err := io.Copy(tempFile, file); err != nil { + log.Printf("Unable to copy file content: %v", err) + return } - }() - go func() { // Generate caption - result, err := c.VisionService.AnalyseImage(fmt.Sprintf("./uploads/%d/%s", taskId, uploadedImages[0].Filename), "categorize the model in this image, use one word only") - + result, err := c.VisionService.AnalyseImage(tempFile.Name(), "") if err != nil { log.Printf("Unable to analyze the image: %v", err) return } - if err := c.TaskService.UpdateMeta(&task, "ai-title", result); err != nil { + if err := c.TaskService.UpdateMeta(&task, "ai-description", result); err != nil { log.Printf("Failed to update task metadata: %v", err) } }() - ctx.JSON(http.StatusOK, gin.H{ - "message": "Files uploaded successfully", - "images": uploadedImages, - }) + ctx.JSON(http.StatusOK, gin.H{"message": "Files uploaded successfully", "images": uploadedImages}) } // StartProcess handles the process of starting the photogrammetry process @@ -375,7 +376,7 @@ func (c *TaskController) SendMessage(ctx *gin.Context) { return } - imagePath := fmt.Sprintf("./uploads/%d/%s", taskId, task.Images[0].Filename) + imagePath := fmt.Sprintf("uploads/%d/%s", taskId, task.Images[0].Filename) if _, err := os.Stat(imagePath); os.IsNotExist(err) { log.Printf("Image file does not exist: %v\n", err) diff --git a/app/controller/task_controller_test.go b/app/controller/task_controller_test.go index d748efe..1efe8aa 100644 --- a/app/controller/task_controller_test.go +++ b/app/controller/task_controller_test.go @@ -17,8 +17,9 @@ func TestTaskController(t *testing.T) { mockTaskService := new(mocks.MockTaskService) mockAppFileService := new(mocks.MockAppFileService) mockVisionService := new(mocks.MockVisionService) + mockStorageService := new(mocks.MockStorageService) - taskController := controller.NewTaskController(mockTaskService, mockAppFileService, mockVisionService) + taskController := controller.NewTaskController(mockTaskService, mockAppFileService, mockVisionService, mockStorageService) t.Run("GetUnarchivedTasks", func(t *testing.T) { recorder, c := utils.SetupRecorder() diff --git a/app/controller/upload_controller.go b/app/controller/upload_controller.go index 95ae49f..f2697d0 100644 --- a/app/controller/upload_controller.go +++ b/app/controller/upload_controller.go @@ -4,25 +4,27 @@ import ( "fmt" "io" "net/http" - "os" - "path/filepath" + "strconv" "github.com/Soup666/modelmaker/database" models "github.com/Soup666/modelmaker/model" + "github.com/Soup666/modelmaker/services" "github.com/gin-gonic/gin" ) // AuthController is the controller for handling authentication requests type UploadController struct { + storageService services.StorageService } -func NewUploadController() *UploadController { - return &UploadController{} +func NewUploadController(storageService services.StorageService) *UploadController { + return &UploadController{ + storageService: storageService, + } } func (c *UploadController) UploadFile(ctx *gin.Context) { - file, header, err := ctx.Request.FormFile("file") if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "File upload failed"}) @@ -30,16 +32,8 @@ func (c *UploadController) UploadFile(ctx *gin.Context) { } defer file.Close() - // Save the file - savePath := filepath.Join("uploads", header.Filename) - out, err := os.Create(savePath) - if err != nil { - ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to save the file"}) - return - } - defer out.Close() - - _, err = io.Copy(out, file) + // Upload file to object storage + url, err := c.storageService.UploadFile(header, 0, "upload") if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to save the file"}) return @@ -48,7 +42,7 @@ func (c *UploadController) UploadFile(ctx *gin.Context) { // Save file metadata in the database image := models.AppFile{ Filename: header.Filename, - Url: fmt.Sprintf("/%s", savePath), + Url: url, } database.DB.Create(&image) @@ -59,31 +53,51 @@ func (c *UploadController) GetFile(ctx *gin.Context) { taskId := ctx.Param("taskId") filename := ctx.Param("filename") - // Construct the full file path - filePath := fmt.Sprintf("uploads/%s/%s", taskId, filename) + // Convert taskId to uint + taskIdInt, err := strconv.ParseUint(taskId, 10, 32) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) + return + } - // Check if the file exists - if _, err := os.Stat(filePath); os.IsNotExist(err) { - ctx.JSON(http.StatusNotFound, gin.H{"error": "Image not found", "path": filePath}) + // Get file from object storage + file, err := c.storageService.GetFile(fmt.Sprintf("uploads/%d/%s", taskIdInt, filename)) + if err != nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "Image not found"}) return } + defer file.Close() - // Serve the file - ctx.File(filePath) + // Stream the file directly to the response writer + _, err = io.Copy(ctx.Writer, file) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stream file"}) + return + } } func (c *UploadController) GetObject(ctx *gin.Context) { filename := ctx.Param("filename") + taskId := ctx.Param("taskID") - // Construct the full file path - filePath := filepath.Join("objects", filename) + // Convert taskId to uint + taskIdInt, err := strconv.ParseUint(taskId, 10, 32) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"}) + return + } - // Check if the file exists - if _, err := os.Stat(filePath); os.IsNotExist(err) { + // Get file from object storage + file, err := c.storageService.GetFile(fmt.Sprintf("objects/%d/%s", taskIdInt, filename)) + if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "Object not found"}) return } + defer file.Close() - // Serve the file - ctx.File(filePath) + // Stream the file to the response + ctx.Stream(func(w io.Writer) bool { + _, err := io.Copy(w, file) + return err == nil + }) } diff --git a/app/go.mod b/app/go.mod index e4d3a8b..c998943 100644 --- a/app/go.mod +++ b/app/go.mod @@ -6,6 +6,10 @@ require ( firebase.google.com/go/v4 v4.15.2 github.com/2024-dissertation/openmvgo v1.0.5 github.com/appleboy/go-fcm v1.2.5 + github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2/config v1.29.14 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 + github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0 github.com/bodgit/sevenzip v1.6.1 github.com/gin-gonic/gin v1.10.0 github.com/google/generative-ai-go v0.19.0 @@ -37,6 +41,20 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/bytedance/sonic v1.12.8 // indirect diff --git a/app/go.sum b/app/go.sum index af41c3e..6b2c2fb 100644 --- a/app/go.sum +++ b/app/go.sum @@ -63,6 +63,42 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/appleboy/go-fcm v1.2.5 h1:odo0RnUntW+RQky7N7ZrJ8ECzZUH+igJhySsyTJ8ZhY= github.com/appleboy/go-fcm v1.2.5/go.mod h1:pTqLIQ6s2JCgu87JKzNtmqBg0A0X9PWk1XR7IaEhvi4= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0 h1:fV4XIU5sn/x8gjRouoJpDVHj+ExJaUk4prYF+eb6qTs= +github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= diff --git a/app/main.go b/app/main.go index 3908896..fe78a60 100644 --- a/app/main.go +++ b/app/main.go @@ -45,24 +45,26 @@ func main() { chatRepo := repositories.NewChatRepository(db.DB) userAnalyticsRepo := repositories.NewUserAnalyticsRepository(db.DB) - // Set up the authentication service + // Set up the services authService := services.NewAuthService(authClient, db.DB, userRepo) userService := services.NewUserService(userRepo) notificationService := services.NewNotificationService() appFileService := services.NewAppFileServiceFile(appFileRepo) - taskService := services.NewTaskService(taskRepo, appFileService, chatRepo, notificationService) + storageService := services.NewKatapultStorageService() + taskService := services.NewTaskService(taskRepo, appFileService, chatRepo, notificationService, storageService) visionService := services.NewVisionService() reportsService := services.NewReportsService(reportsRepo) collectionsService := services.NewCollectionsService(collectionsRepo) userAnalyticsService := services.NewUserAnalyticsService(userAnalyticsRepo) - // Initialise Job Queue + // Initialize Job Queue taskService.StartWorker() + // Set up the controllers authController := controller.NewAuthController(authService, userService) - taskController := controller.NewTaskController(&taskService, appFileService, visionService) - uploadController := controller.NewUploadController() - objectController := controller.NewObjectController() + uploadController := controller.NewUploadController(storageService) + objectController := controller.NewObjectController(storageService) + taskController := controller.NewTaskController(&taskService, appFileService, visionService, storageService) visionController := controller.NewVisionController(visionService, taskRepo, &taskService) reportsController := controller.NewReportsController(reportsService) collectionsController := controller.NewCollectionsController(collectionsService) @@ -70,11 +72,27 @@ func main() { notificationController := controller.NewNotificationController(notificationService) // Set up the HTTP router - r := router.NewRouter(authController, taskController, uploadController, objectController, visionController, authService, reportsController, collectionsController, userAnalyticsController, notificationController) + r := router.NewRouter( + authController, + taskController, + uploadController, + objectController, + visionController, + authService, + reportsController, + collectionsController, + userAnalyticsController, + notificationController, + ) // Start the server - if r.Run(":"+os.Getenv("PORT")) != nil { - panic("[Error] failed to start Gin server due to: " + err.Error()) + port := os.Getenv("PORT") + if port == "" { + port = "3333" } + log.Printf("Starting server on port %s...", port) + if err := r.Run(":" + port); err != nil { + log.Fatalf("Failed to start server: %v", err) + } } diff --git a/app/mocks/storage_service_mock.go b/app/mocks/storage_service_mock.go new file mode 100644 index 0000000..662c3ce --- /dev/null +++ b/app/mocks/storage_service_mock.go @@ -0,0 +1,41 @@ +package mocks + +import ( + "io" + "mime/multipart" + + "github.com/stretchr/testify/mock" +) + +// MockVisionService is a mock implementation of VisionService. +type MockStorageService struct { + mock.Mock +} + +func (m *MockStorageService) UploadFile(file *multipart.FileHeader, taskID uint, fileType string) (string, error) { + args := m.Called(file, taskID, fileType) + if args.Get(0) != nil { + return args.Get(0).(string), args.Error(1) + } + return "", args.Error(1) +} +func (m *MockStorageService) UploadFromReader(reader io.Reader, taskID uint, filename string, fileType string) (string, error) { + args := m.Called(reader, taskID, filename, fileType) + if args.Get(0) != nil { + return args.Get(0).(string), args.Error(1) + } + return "", args.Error(1) +} + +func (m *MockStorageService) GetFile(filepath string) (io.ReadCloser, error) { + args := m.Called(filepath) + if args.Get(0) != nil { + return args.Get(0).(io.ReadCloser), args.Error(1) + } + return nil, args.Error(1) +} + +func (m *MockStorageService) DeleteFile(taskID uint, filename string) error { + args := m.Called(taskID, filename) + return args.Error(0) +} diff --git a/app/router/router.go b/app/router/router.go index 5ec67e2..e0f85a8 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -1,6 +1,9 @@ package router import ( + "fmt" + "io" + "github.com/Soup666/modelmaker/controller" "github.com/Soup666/modelmaker/middleware" "github.com/Soup666/modelmaker/services" @@ -74,6 +77,30 @@ func NewRouter( // Debug authRequired.POST("/debug/notification", notificationController.SendMessage) + authRequired.POST("/debug/storage", func(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.JSON(400, gin.H{"error": "No file provided"}) + return + } + storageService := services.NewKatapultStorageService() + url, err := storageService.UploadFile(file, 1, "test") + if err != nil { + c.JSON(400, gin.H{"error": fmt.Sprintf("Failed to upload file: %v", err)}) + return + } + c.JSON(200, gin.H{"status": "ok", "url": url}) + }) + authRequired.GET("/debug/storage", func(c *gin.Context) { + storageService := services.NewKatapultStorageService() + reader, err := storageService.GetFile("uploads/1/00006._c.png") + if err != nil { + c.JSON(400, gin.H{"error": fmt.Sprintf("Failed to get file: %v", err)}) + return + } + io.Copy(c.Writer, reader) + reader.Close() + }) // Unauthenticated routes r.POST("/uploads", uploadController.UploadFile) diff --git a/app/seeds/seeds/files.go b/app/seeds/seeds/files.go index a655495..2d581e7 100644 --- a/app/seeds/seeds/files.go +++ b/app/seeds/seeds/files.go @@ -104,7 +104,7 @@ func CreateDummyFiles(db *gorm.DB, taskId uint) ([]model.AppFile, error) { for i, filename := range filenames { files[i] = model.AppFile{ TaskId: taskId, - Url: fmt.Sprintf("/uploads/%d/%s", taskId, filename), + Url: fmt.Sprintf("uploads/%d/%s", taskId, filename), Filename: filename, FileType: "upload", } diff --git a/app/services/katapult_storage_service.go b/app/services/katapult_storage_service.go new file mode 100644 index 0000000..0a7ad6e --- /dev/null +++ b/app/services/katapult_storage_service.go @@ -0,0 +1,118 @@ +package services + +import ( + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "os" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type KatapultStorageService struct { + client *s3.Client + bucketName string + region string + endpoint string +} + +func NewKatapultStorageService() *KatapultStorageService { + bucketName := os.Getenv("KATAPULT_BUCKET_NAME") + region := os.Getenv("KATAPULT_REGION") + endpoint := os.Getenv("KATAPULT_ENDPOINT") + accessKey := os.Getenv("KATAPULT_ACCESS_KEY") + secretKey := os.Getenv("KATAPULT_SECRET_KEY") + + // Configure AWS SDK + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion(region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")), + ) + if err != nil { + panic(fmt.Sprintf("unable to load SDK config, %v", err)) + } + + // Create S3 client with custom endpoint + client := s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(endpoint) + }) + + return &KatapultStorageService{ + client: client, + bucketName: bucketName, + region: region, + endpoint: endpoint, + } +} + +func (s *KatapultStorageService) UploadFile(file *multipart.FileHeader, taskID uint, fileType string) (string, error) { + src, err := file.Open() + if err != nil { + return "", fmt.Errorf("failed to open file: %w", err) + } + defer src.Close() + + return s.UploadFromReader(src, taskID, file.Filename, fileType) +} + +func (s *KatapultStorageService) UploadFromReader(reader io.Reader, taskID uint, filename string, fileType string) (string, error) { + // Read the entire file into memory + buf := new(bytes.Buffer) + if _, err := io.Copy(buf, reader); err != nil { + return "", fmt.Errorf("failed to read file: %w", err) + } + + objectKey := s.getObjectKey(taskID, filename, fileType) + + // Upload to S3 + _, err := s.client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(objectKey), + Body: bytes.NewReader(buf.Bytes()), + }) + if err != nil { + return "", fmt.Errorf("failed to upload file: %w", err) + } + + return objectKey, nil +} + +func (s *KatapultStorageService) GetFile(filepath string) (io.ReadCloser, error) { + // Get object from S3 + result, err := s.client.GetObject(context.TODO(), &s3.GetObjectInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(filepath), + }) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + return result.Body, nil +} + +func (s *KatapultStorageService) DeleteFile(taskID uint, filename string) error { + objectKey := s.getObjectKey(taskID, filename, "") + + // Delete object from S3 + _, err := s.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(objectKey), + }) + if err != nil { + return fmt.Errorf("failed to delete file: %w", err) + } + + return nil +} + +func (s *KatapultStorageService) getObjectKey(taskID uint, filename string, fileType string) string { + if fileType == "mesh" { + return fmt.Sprintf("objects/%d/%s", taskID, filename) + } + return fmt.Sprintf("uploads/%d/%s", taskID, filename) +} diff --git a/app/services/storage_service.go b/app/services/storage_service.go new file mode 100644 index 0000000..c56e548 --- /dev/null +++ b/app/services/storage_service.go @@ -0,0 +1,21 @@ +package services + +import ( + "io" + "mime/multipart" +) + +// StorageService defines the interface for object storage operations +type StorageService interface { + // UploadFile uploads a file to object storage and returns its public URL + UploadFile(file *multipart.FileHeader, taskID uint, fileType string) (string, error) + + // UploadFromReader uploads data from an io.Reader to object storage + UploadFromReader(reader io.Reader, taskID uint, filename string, fileType string) (string, error) + + // GetFile retrieves a file from object storage + GetFile(filepath string) (io.ReadCloser, error) + + // DeleteFile deletes a file from object storage + DeleteFile(taskID uint, filename string) error +} diff --git a/app/services/task_service_impl.go b/app/services/task_service_impl.go index b25808a..03f12f5 100644 --- a/app/services/task_service_impl.go +++ b/app/services/task_service_impl.go @@ -3,6 +3,7 @@ package services import ( "errors" "fmt" + "io" "log" "os" "os/exec" @@ -22,29 +23,23 @@ type TaskServiceImpl struct { appFileService AppFileService chatRepository repositories.ChatRepository notificationService NotificationService + storageService StorageService jobQueue chan TaskJob } -type saveOutput struct { - savedOutput []byte -} - -func (so *saveOutput) Write(p []byte) (n int, err error) { - so.savedOutput = append(so.savedOutput, p...) - return os.Stdout.Write(p) -} - func NewTaskService( taskRepo repositories.TaskRepository, appFileService AppFileService, chatRepository repositories.ChatRepository, notificationService NotificationService, + storageService StorageService, ) TaskServiceImpl { return TaskServiceImpl{ taskRepo: taskRepo, appFileService: appFileService, chatRepository: chatRepository, notificationService: notificationService, + storageService: storageService, jobQueue: make(chan TaskJob, 100), } } @@ -157,6 +152,259 @@ func (s *TaskServiceImpl) FailTask(task *models.Task, message string) error { return nil } +func (s *TaskServiceImpl) GetTaskFiles(taskID uint, fileType string) ([]models.AppFile, error) { + files, err := s.appFileService.GetTaskFiles(taskID, fileType) + if err != nil { + return nil, err + } + return files, nil +} + +func (s *TaskServiceImpl) GetTaskFile(taskID uint, fileType string) (*models.AppFile, error) { + file, err := s.appFileService.GetTaskFile(taskID, fileType) + if err != nil { + return nil, err + } + return file, nil +} + +func (s *TaskServiceImpl) FullyLoadTask(task *models.Task) (*models.Task, error) { + files, err := s.GetTaskFiles(task.ID, "upload") + if err != nil { + return nil, err + } + task.Images = files + + mesh, err := s.GetTaskFile(task.ID, "mesh") + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + task.Mesh = nil + } else { + return nil, err + } + } else { + task.Mesh = mesh + } + + return task, nil +} + +func (s *TaskServiceImpl) SendMessage(taskID uint, message string, sender string) (*models.ChatMessage, error) { + chatMessage := &models.ChatMessage{ + Message: message, + TaskId: taskID, + Sender: sender, + } + + err := s.chatRepository.CreateChat(chatMessage) + if err != nil { + return chatMessage, err + } + + return chatMessage, nil +} + +func (s *TaskServiceImpl) AddLog(taskID uint, log string) error { + + err := s.taskRepo.AddLog(taskID, log) + if err != nil { + return err + } + return nil +} + +func (s *TaskServiceImpl) EnqueueJob(job TaskJob) bool { + + select { + case s.jobQueue <- job: + return true + default: + return false + } +} + +func (s *TaskServiceImpl) StartWorker() { + go func() { + for job := range s.jobQueue { + go func() { + fmt.Printf("Processing job: %+v\n", job) + + task, err := s.taskRepo.GetTaskByID(job.ID) + if err != nil { + return + } + + // Set task to in progress + task.Status = models.INPROGRESS + if err := s.UpdateTask(task); err != nil { + s.FailTask(task, fmt.Sprintf("Failed to update task: %v\n", err)) + return + } + + // Start processing + if err := s.UpdateMeta(task, "opensfm-process", 0.0); err != nil { + log.Printf("Failed to update meta: %f: %v\n", 0.0, err) + s.FailTask(task, fmt.Sprintf("Failed to update meta: %f: %v\n", 0.0, err)) + return + } + + // Send notification + s.notificationService.SendMessage(&models.Notification{ + UserID: task.UserId, + Message: "Scan started", + Title: task.Title, + }) + + // Setup Utils + utils := utils.NewUtils() + + timestamp := time.Now().Unix() + + // Middle directory creation + buildDir, err := os.MkdirTemp("", fmt.Sprintf("%d-build", timestamp)) + utils.Check(err) + + // Final file conversion directory + outputDir, err := os.MkdirTemp("", fmt.Sprintf("%d-convert", timestamp)) + utils.Check(err) + + // Remove directories + defer os.RemoveAll(buildDir) + defer os.RemoveAll(outputDir) + + inputDir, err := os.MkdirTemp("", fmt.Sprintf("%d-input", timestamp)) + utils.Check(err) + + defer os.RemoveAll(inputDir) + + cameraDBFile := filepath.Join("bin", "sensor_width_camera_database.txt") + + // Download input files into tmp directory. Paths are from aws s3 bucket. + for _, taskFile := range task.Images { + fmt.Println("Downloading file: ", taskFile.Url) + file, err := s.storageService.GetFile(taskFile.Url) + if err != nil { + s.FailTask(task, fmt.Sprintf("Failed to get file: %v", err)) + return + } + defer file.Close() + dstFile, err := os.Create(filepath.Join(inputDir, taskFile.Filename)) + if err != nil { + s.FailTask(task, fmt.Sprintf("Failed to create file: %v", err)) + return + } + defer dstFile.Close() + _, err = io.Copy(dstFile, file) + if err != nil { + s.FailTask(task, fmt.Sprintf("Failed to copy file: %v", err)) + return + } + } + + // Configure openmvg service + openmvgService := openmvg.NewOpenMVGService( + openmvg.NewOpenMVGConfig( + inputDir, + buildDir, + &cameraDBFile, + ), + utils, + ) + + // Configure openmvs service + openmvsService := openmvs.NewOpenMVSService( + openmvs.NewOpenMVSConfig( + outputDir, + buildDir, + 1, + ), + utils, + ) + + // Populate and Run Pipelines + openmvgService.PopulateTmpDir() + defer os.RemoveAll(openmvgService.Config.MatchesDir) + defer os.RemoveAll(openmvgService.Config.ReconstructionDir) + + openmvgService.SfMSequentialPipeline() + + // Add try-catch equivalent + func() { + defer func() { + if r := recover(); r != nil { + s.FailTask(task, fmt.Sprintf("OpenMVS pipeline failed: %v", r)) + return + } + }() + openmvsService.RunPipeline() + }() + + // Convert to glb + fileName := filepath.Join(outputDir, "final.obj") + convertedFileName := filepath.Join(outputDir, "final.glb") + fmt.Println("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", fileName, convertedFileName) + cmd := exec.Command("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", fileName, convertedFileName) + + if err := cmd.Run(); err != nil { + s.FailTask(task, fmt.Sprintf("MeshConversion failed: %v", err)) + return + } + + if err := s.UpdateMeta(task, "log", "MeshConversion started"); err != nil { + log.Printf("Failed to update meta: log: %v\n", err) + return + } + + // Save mesh + mesh, err := s.appFileService.Save(&models.AppFile{ + Url: fileName + ".glb", + Filename: "final.glb", + TaskId: task.ID, + FileType: "mesh", + }) + + if err != nil { + s.FailTask(task, fmt.Sprintf("Failed to Save mesh: %v", err)) + return + } + + // Upload GLB file to storage + file, err := os.Open(convertedFileName) + if err != nil { + s.FailTask(task, fmt.Sprintf("Failed to open converted file: %v", err)) + return + } + defer file.Close() + s.storageService.UploadFromReader(file, task.ID, "final.glb", "mesh") + + // Update task + task.Mesh = mesh + task.Completed = true + task.Status = models.SUCCESS + + if err := s.UpdateTask(task); err != nil { + s.FailTask(task, fmt.Sprintf("Failed to update task: %v", err)) + return + } + + // Send notification + s.notificationService.SendMessage(&models.Notification{ + UserID: task.UserId, + Message: "Scan finished", + Title: task.Title, + }) + + // Complete + fmt.Println("OpenMVGO pipeline completed successfully!") + }() + } + }() +} + +func (s *TaskServiceImpl) GetJobQueue() chan TaskJob { + return s.jobQueue +} + // func (s *TaskServiceImpl) RunPhotogrammetryProcess(task *models.Task) error { // if task.Status == models.INPROGRESS { @@ -454,182 +702,3 @@ func (s *TaskServiceImpl) FailTask(task *models.Task, message string) error { // log.Printf("Processing completed in %s\n", time.Since(startTime)) // return nil // } - -func (s *TaskServiceImpl) GetTaskFiles(taskID uint, fileType string) ([]models.AppFile, error) { - files, err := s.appFileService.GetTaskFiles(taskID, fileType) - if err != nil { - return nil, err - } - return files, nil -} - -func (s *TaskServiceImpl) GetTaskFile(taskID uint, fileType string) (*models.AppFile, error) { - file, err := s.appFileService.GetTaskFile(taskID, fileType) - if err != nil { - return nil, err - } - return file, nil -} - -func (s *TaskServiceImpl) FullyLoadTask(task *models.Task) (*models.Task, error) { - files, err := s.GetTaskFiles(task.ID, "upload") - if err != nil { - return nil, err - } - task.Images = files - - mesh, err := s.GetTaskFile(task.ID, "mesh") - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - task.Mesh = nil - } else { - return nil, err - } - } else { - task.Mesh = mesh - } - - return task, nil -} - -func (s *TaskServiceImpl) SendMessage(taskID uint, message string, sender string) (*models.ChatMessage, error) { - chatMessage := &models.ChatMessage{ - Message: message, - TaskId: taskID, - Sender: sender, - } - - err := s.chatRepository.CreateChat(chatMessage) - if err != nil { - return chatMessage, err - } - - return chatMessage, nil -} - -func (s *TaskServiceImpl) AddLog(taskID uint, log string) error { - - err := s.taskRepo.AddLog(taskID, log) - if err != nil { - return err - } - return nil -} - -func (s *TaskServiceImpl) EnqueueJob(job TaskJob) bool { - - select { - case s.jobQueue <- job: - return true - default: - return false - } -} - -func (s *TaskServiceImpl) StartWorker() { - go func() { - for job := range s.jobQueue { - go func() { - fmt.Printf("Processing job: %+v\n", job) - - task, err := s.taskRepo.GetTaskByID(job.ID) - if err != nil { - return - } - - // Setup Utils - utils := utils.NewUtils() - - timestamp := time.Now().Unix() - - // Middle directory creation - buildDir, err := os.MkdirTemp("", fmt.Sprintf("%dbuild", timestamp)) - utils.Check(err) - defer os.RemoveAll(buildDir) - - inputDir := filepath.Join("uploads", fmt.Sprintf("%d", task.ID)) - outputDir := filepath.Join("objects", fmt.Sprintf("%d", task.ID)) - cameraDBFile := filepath.Join("bin", "sensor_width_camera_database.txt") - - // Configure openmvg service - openmvgService := openmvg.NewOpenMVGService( - openmvg.NewOpenMVGConfig( - inputDir, - buildDir, - &cameraDBFile, - ), - utils, - ) - - // Configure openmvs service - openmvsService := openmvs.NewOpenMVSService( - openmvs.NewOpenMVSConfig( - outputDir, - buildDir, - 0, - ), - utils, - ) - - // Populate and Run Pipelines - openmvgService.PopulateTmpDir() - defer os.RemoveAll(openmvgService.Config.MatchesDir) - defer os.RemoveAll(openmvgService.Config.ReconstructionDir) - - openmvgService.SfMSequentialPipeline() - openmvsService.RunPipeline() - - fileName := filepath.Join(outputDir, "final.obj") - convertedFileName := filepath.Join(outputDir, "final.glb") - fmt.Println("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", fileName, convertedFileName) - cmd := exec.Command("blender", "-b", "-P", "./bin/convert_obj_to_glb.py", "--", fileName, convertedFileName) - - err = cmd.Run() - - if err != nil { - s.FailTask(task, fmt.Sprintf("MeshConversion failed: %v", err)) - return - } - - if err := s.UpdateMeta(task, "log", "MeshConversion started"); err != nil { - log.Printf("Failed to update meta: log: %v\n", err) - return - } - - mesh, err := s.appFileService.Save(&models.AppFile{ - Url: fileName + ".glb", - Filename: "final.glb", - TaskId: task.ID, - FileType: "mesh", - }) - - if err != nil { - s.FailTask(task, fmt.Sprintf("Failed to Save mesh: %v", err)) - return - } - - task.Mesh = mesh - task.Completed = true - task.Status = models.SUCCESS - - if err := s.UpdateTask(task); err != nil { - s.FailTask(task, fmt.Sprintf("Failed to update task: %v", err)) - return - } - - s.notificationService.SendMessage(&models.Notification{ - UserID: task.UserId, - Message: "Scan finished", - Title: task.Title, - }) - - // Complete - fmt.Println("OpenMVGO pipeline completed successfully!") - }() - } - }() -} - -func (s *TaskServiceImpl) GetJobQueue() chan TaskJob { - return s.jobQueue -} diff --git a/app/services/task_service_impl_test.go b/app/services/task_service_impl_test.go index b07d21b..acbc7dd 100644 --- a/app/services/task_service_impl_test.go +++ b/app/services/task_service_impl_test.go @@ -18,8 +18,9 @@ func TestTaskService(t *testing.T) { mockAppFileService := new(mocks.MockAppFileService) mockNotificationService := new(mocks.MockNotificationService) + mockStorageService := new(mocks.MockStorageService) - taskService := services.NewTaskService(mockTaskRepository, mockAppFileService, mockChatRepository, mockNotificationService) + taskService := services.NewTaskService(mockTaskRepository, mockAppFileService, mockChatRepository, mockNotificationService, mockStorageService) t.Run("CreateTask", func(t *testing.T) { diff --git a/app/uploads/.keep b/app/uploads/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 28b39bd..355ad3e 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -45,6 +45,8 @@ services: PGADMIN_CONFIG_SERVER_MODE: "False" volumes: - pgadmin:/var/lib/pgadmin + ports: + - "5432:80" networks: - db depends_on: