diff --git a/apps/backend/config/config.go b/apps/backend/config/config.go index b013ce5..b55f28b 100644 --- a/apps/backend/config/config.go +++ b/apps/backend/config/config.go @@ -36,6 +36,9 @@ type Config struct { RootUser string Password string } + GOFS struct { + Endpoint string + } } Server struct { @@ -79,7 +82,8 @@ func Load() *Config { Cfg.Storage.TempLimit = getEnvAsInt("TEMP_STORAGE_LIMIT", 5) strategy := getEnv("STORAGE_STRATEGY", "fs") - if strategy == "s3" || strategy == "minio" { + switch strategy { + case "s3", "minio": Cfg.Storage.Minio.URL = getEnv("MINIO_URL", "") Cfg.Storage.Minio.RootUser = getEnv("MINIO_ROOT_USER", "") Cfg.Storage.Minio.Password = getEnv("MINIO_ROOT_PASSWORD", "") @@ -90,7 +94,10 @@ func Load() *Config { } else { Cfg.Storage.Strategy = typesdefs.S3 } - } else { + case "gofs": + Cfg.Storage.GOFS.Endpoint = getEnv("GOFS_ENDPOINT", "localhost:8080") + Cfg.Storage.Strategy = typesdefs.GOFS + default: Cfg.Storage.Strategy = typesdefs.FS } diff --git a/apps/backend/storage_handlers/fs.go b/apps/backend/storage_handlers/fs.go index 7bc4aeb..d194532 100644 --- a/apps/backend/storage_handlers/fs.go +++ b/apps/backend/storage_handlers/fs.go @@ -9,6 +9,8 @@ import ( "path/filepath" ) +var _ StorageHandler = (*Fs_Handler)(nil) + type Fs_Handler struct { } diff --git a/apps/backend/storage_handlers/go_fs.go b/apps/backend/storage_handlers/go_fs.go new file mode 100644 index 0000000..b6d0599 --- /dev/null +++ b/apps/backend/storage_handlers/go_fs.go @@ -0,0 +1,93 @@ +package storagehandlers + +import ( + "backend/config" + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" +) + +var _ StorageHandler = (*GOFS_Handler)(nil) + +type GOFS_Handler struct { + Endpoint string +} + +func NewGoFsHandler() *GOFS_Handler { + + return &GOFS_Handler{ + Endpoint: config.Cfg.Storage.GOFS.Endpoint, + } +} +func (g *GOFS_Handler) Init() error { + resp, err := http.Get(g.Endpoint + "/health") + if err != nil { + return fmt.Errorf("storage server unreachable: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("storage server unhealthy: %s", resp.Status) + } + return nil +} +func (g *GOFS_Handler) Add(file *multipart.FileHeader, fileId string) error { + f, err := file.Open() + if err != nil { + return err + } + defer f.Close() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", file.Filename) + if err != nil { + return err + } + if _, err := io.Copy(part, f); err != nil { + return err + } + _ = writer.WriteField("fileId", fileId) + writer.Close() + + req, err := http.NewRequest("POST", g.Endpoint+"/upload", body) + if err != nil { + return err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("upload failed: %s", resp.Status) + } + return nil +} + +func (g *GOFS_Handler) Delete(fileId string) error { + req, err := http.NewRequest("DELETE", g.Endpoint+"/delete/"+fileId, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("delete failed: %s", resp.Status) + } + return nil +} + +func (g *GOFS_Handler) Get(filename string) (string, error) { + panic("unimplemented") +} + +func (g *GOFS_Handler) List() ([]string, error) { + panic("unimplemented") +} diff --git a/apps/backend/storage_handlers/minio.go b/apps/backend/storage_handlers/minio.go index 93efc4f..a99ce4b 100644 --- a/apps/backend/storage_handlers/minio.go +++ b/apps/backend/storage_handlers/minio.go @@ -16,6 +16,7 @@ var ( useSSL = false bucketName = "storage" ) +var _ StorageHandler = (*S3_Handler)(nil) type S3_Handler struct { minioClient *minio.Client diff --git a/apps/backend/storage_handlers/storage_handler.go b/apps/backend/storage_handlers/storage_handler.go index bd41a09..792c636 100644 --- a/apps/backend/storage_handlers/storage_handler.go +++ b/apps/backend/storage_handlers/storage_handler.go @@ -39,6 +39,8 @@ func NewStorageHandler(strategy typesdefs.StorageStrategy) (StorageHandler, erro return nil, err } return h, nil + case typesdefs.GOFS: + return nil, nil default: return nil, errors.New("invalid storage strategy") } diff --git a/apps/backend/typesdefs/types.go b/apps/backend/typesdefs/types.go index d0e8c11..8fa1533 100644 --- a/apps/backend/typesdefs/types.go +++ b/apps/backend/typesdefs/types.go @@ -3,6 +3,7 @@ package typesdefs type StorageStrategy string const ( - FS StorageStrategy = "fs" - S3 StorageStrategy = "s3" + FS StorageStrategy = "fs" + S3 StorageStrategy = "s3" + GOFS StorageStrategy = "gofs" ) diff --git a/apps/infoalloggi/Dockerfile b/apps/infoalloggi/Dockerfile index 9716600..394850e 100644 --- a/apps/infoalloggi/Dockerfile +++ b/apps/infoalloggi/Dockerfile @@ -65,6 +65,7 @@ ENV TILES_URL="localhost:6379" ENV SKEBBY_USER="mock" ENV SKEBBY_PASS="mock" ENV REVALIDATION_SECRET="mock" +ENV STORAGE_TOKEN="mock" RUN SKIP_ENV_VALIDATION=1 npm run build @@ -131,6 +132,8 @@ ENV SKEBBY_USER=$SKEBBY_USER ARG SKEBBY_PASS ENV SKEBBY_PASS=$SKEBBY_PASS ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c" +ARG STORAGE_TOKEN +ENV STORAGE_TOKEN=$STORAGE_TOKEN RUN addgroup -g 1001 -S nodejs diff --git a/apps/infoalloggi/next.config.ts b/apps/infoalloggi/next.config.ts index 560d1e6..315b2d7 100644 --- a/apps/infoalloggi/next.config.ts +++ b/apps/infoalloggi/next.config.ts @@ -3,9 +3,9 @@ * This is especially useful for Docker builds. */ -import { env } from "node:process"; import { fileURLToPath } from "node:url"; import type { NextConfig } from "next"; +import { env } from "~/env"; async function createNextConfig(): Promise { const { createJiti } = await import("jiti"); @@ -60,8 +60,8 @@ async function createNextConfig(): Promise { rewrites: async () => { return [ { - source: "/storage/:slug*", - destination: "http://localhost:8080/:slug*", + source: "/storage-api/:slug*", + destination: `http://localhost:8080/:slug*?token=${env.STORAGE_TOKEN}`, }, { destination: env.NODE_ENV === "production" ? "/404" : "/api/panel", diff --git a/apps/infoalloggi/src/env.ts b/apps/infoalloggi/src/env.ts index fcd8549..1a07d54 100644 --- a/apps/infoalloggi/src/env.ts +++ b/apps/infoalloggi/src/env.ts @@ -28,6 +28,7 @@ export const env = createEnv({ SKEBBY_USER: z.string(), SKEBBY_PASS: z.string(), REVALIDATION_SECRET: z.string(), + STORAGE_TOKEN: z.string(), }, client: { NEXT_PUBLIC_BASE_URL: z.string(), diff --git a/apps/infoalloggi/src/middlewares/apis_middleware.ts b/apps/infoalloggi/src/middlewares/apis_middleware.ts deleted file mode 100644 index 117d011..0000000 --- a/apps/infoalloggi/src/middlewares/apis_middleware.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "~/env"; - -export const apisMiddleware = async (req: NextRequest) => { - const path = req.nextUrl.pathname; - - const rewrite_paths = ["/go-api/images/", "/go-api/storage/"]; - if (!rewrite_paths.some((p) => path.startsWith(p))) { - return; - } - - if ( - path.startsWith("/go-api/images/") || - path.startsWith("/go-api/storage/") - ) { - // Handle image and storage requests - const newPath = path - .replace("/go-api/images/", "/images/") - .replace("/go-api/storage/", "/storage/"); - - const url = new URL(`${env.BACKENDSERVER_URL}${newPath}`, req.url); - const res = NextResponse.rewrite(url); - res.headers.set("Access-Control-Allow-Origin", env.NEXT_PUBLIC_BASE_URL); - - res.headers.set( - "Access-Control-Allow-Methods", - "GET, POST, PUT, DELETE, OPTIONS", - ); - res.headers.set( - "Access-Control-Allow-Headers", - "Content-Type, Authorization", - ); - return res; - - // Fetch the content from the backend and serve it through the middleware - /* - const response = await fetch(url.toString(), { - headers: req.headers, - }); - - return new NextResponse(response.body, { - headers: response.headers, - status: response.status, - });*/ - } -}; diff --git a/apps/infoalloggi/src/pages/test.tsx b/apps/infoalloggi/src/pages/test.tsx index 47edd42..974fbe9 100644 --- a/apps/infoalloggi/src/pages/test.tsx +++ b/apps/infoalloggi/src/pages/test.tsx @@ -243,7 +243,7 @@ function FileManagementPage() { {file.originalName} )}
@@ -260,7 +260,7 @@ function FileManagementPage() {
Download diff --git a/apps/infoalloggi/src/server/api/routers/storage.ts b/apps/infoalloggi/src/server/api/routers/storage.ts index 3baca8d..2cab9a7 100644 --- a/apps/infoalloggi/src/server/api/routers/storage.ts +++ b/apps/infoalloggi/src/server/api/routers/storage.ts @@ -17,10 +17,10 @@ import { db } from "~/server/db"; import { RateLimiterHandler } from "~/server/services/ratelimiter"; import { addUserStorageJunc, - deleteFile, + deleteHandler, deleteUserStorageJunc, getNewTokenHandler, - updateFile, + updateHandler, } from "~/server/services/storage.service"; import { zStorageIndexId, zUserId } from "~/server/utils/zod_types"; @@ -58,7 +58,7 @@ export const storageRouter = createTRPCRouter({ deleteStorage: protectedProcedure .input(z.object({ storageId: zStorageIndexId })) .mutation(async ({ input }) => { - return await deleteFile({ + return await deleteHandler({ cb: async (fileId) => { await db .deleteFrom("storageindex") @@ -133,7 +133,7 @@ export const storageRouter = createTRPCRouter({ renameFile: adminProcedure .input(z.object({ fileId: zStorageIndexId, name: z.string() })) .mutation(async ({ input }) => { - return await updateFile({ + return await updateHandler({ data: { filename: input.name }, db, fileId: input.fileId, diff --git a/apps/infoalloggi/src/server/controllers/storage.controller.ts b/apps/infoalloggi/src/server/controllers/storage.controller.ts index 00eff44..f64fa95 100644 --- a/apps/infoalloggi/src/server/controllers/storage.controller.ts +++ b/apps/infoalloggi/src/server/controllers/storage.controller.ts @@ -8,7 +8,7 @@ import type { UsersId } from "~/schemas/public/Users"; import type { Querier } from "~/server/db"; import { addUserStorageJunc, - deleteFile, + deleteHandler, deleteUserStorageJunc, } from "~/server/services/storage.service"; @@ -98,7 +98,7 @@ export const deleteUserStorageHandler = async ({ return "no file"; } - await deleteFile({ + await deleteHandler({ cb: async (fileId) => { await db.transaction().execute(async (trx) => { await deleteUserStorageJunc({ diff --git a/apps/infoalloggi/src/server/services/storage.service.ts b/apps/infoalloggi/src/server/services/storage.service.ts index 7baf580..33f9c6d 100644 --- a/apps/infoalloggi/src/server/services/storage.service.ts +++ b/apps/infoalloggi/src/server/services/storage.service.ts @@ -7,6 +7,7 @@ import type { import type { TempTokensToken } from "~/schemas/public/TempTokens"; import type { UsersId } from "~/schemas/public/Users"; import type { Querier } from "~/server/db"; +import { deleteFile } from "../storage"; export const getNewTokenHandler = async ({ db, @@ -43,7 +44,7 @@ const newToken = async ({ db }: { db: Querier }) => { } }; -export const deleteFile = async ({ +export const deleteHandler = async ({ db, fileId, cb, @@ -58,22 +59,8 @@ export const deleteFile = async ({ .select(["id", "ext"]) .where("id", "=", fileId) .executeTakeFirstOrThrow(); + await deleteFile(storage.id); - const authToken = (await getNewTokenHandler({ db }))[0]; - if (!authToken) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error while getting auth token", - }); - } - const url = `${env.BACKENDSERVER_URL}/storage/delete/${storage.id}${storage.ext}?token=${authToken}`; - const response = await fetch(url, { - method: "DELETE", - }); - - if (!response.ok) { - throw new Error("File remove failed"); - } if (cb) { await cb(fileId); } @@ -86,32 +73,7 @@ export const deleteFile = async ({ } }; -// export const deleteFiles = async ({ -// db, -// refs, -// cb, -// }: { -// db: Querier; -// refs: { storageid: StorageindexId }[]; -// cb?: (refs: { storageid: StorageindexId }[]) => Promise; -// }) => { -// try { -// await Promise.all( -// refs.map((ref) => deleteFile({ db, fileId: ref.storageid })), -// ); -// if (cb) { -// await cb(refs); -// } -// } catch (error) { -// console.error("Error deleting files:", error); -// throw new TRPCError({ -// code: "INTERNAL_SERVER_ERROR", -// message: "Error while deleting files", -// }); -// } -// }; - -export const updateFile = async ({ +export const updateHandler = async ({ db, fileId, data, diff --git a/apps/infoalloggi/src/server/storage.ts b/apps/infoalloggi/src/server/storage.ts index c79c1bb..42b10ab 100644 --- a/apps/infoalloggi/src/server/storage.ts +++ b/apps/infoalloggi/src/server/storage.ts @@ -15,9 +15,10 @@ export type FileMetadata = z.infer; // Helper to fetch the list of all files export async function fetchFiles(): Promise { try { - const response = await fetch(`/storage/api/files`); + const response = await fetch(`/storage-api/api/files`); if (!response.ok) { - throw new Error(`Failed to fetch files: ${response.statusText}`); + console.error("Failed to fetch file list:", response.statusText); + return []; } const parse = FileMetadataSchema.array().safeParse(await response.json()); @@ -44,7 +45,7 @@ export async function uploadFile( } try { - const response = await fetch(`/storage/upload`, { + const response = await fetch(`/storage-api/upload`, { method: "POST", body: formData, }); @@ -66,17 +67,10 @@ export async function uploadFile( // Helper to delete a file export async function deleteFile(fileID: string): Promise { - const formData = new URLSearchParams(); - formData.append("id", fileID); - try { // The Go server accepts POST with form data for delete - const response = await fetch(`/storage/delete`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: formData.toString(), + const response = await fetch(`/storage-api/delete/${fileID}`, { + method: "DELETE", }); // Treat redirect as success diff --git a/apps/infoalloggi/src/utils/api.ts b/apps/infoalloggi/src/utils/api.ts index a8fdade..ce0fcdf 100644 --- a/apps/infoalloggi/src/utils/api.ts +++ b/apps/infoalloggi/src/utils/api.ts @@ -15,7 +15,6 @@ import type { AppRouter } from "~/server/api/root"; const getUrl = () => { const base = (() => { if (typeof window !== "undefined") return window.location.origin; - if (process.env.APP_URL) return process.env.APP_URL; return `http://localhost:${process.env.PORT ?? 3000}`; })(); diff --git a/build_and_push_ghcr.sh b/build_and_push_ghcr.sh index c06a5b3..a926e69 100644 --- a/build_and_push_ghcr.sh +++ b/build_and_push_ghcr.sh @@ -19,6 +19,7 @@ set -euo pipefail : "${TAG:?TAG is required}" : "${IMAGE_REGISTRY:?IMAGE_REGISTRY is required}" +: "${PREFIX:?PREFIX is required}" : "${OWNER:?OWNER is required}" : "${SERVICES:?SERVICES is required}" : "${USERNAME:?USERNAME is required}" @@ -55,12 +56,12 @@ for svc in ${SERVICES}; do ;; esac - IMAGE="${IMAGE_REGISTRY}/${OWNER}/infoalloggi-${svc}:${TAG}" - echo "Building ${svc} -> ${IMAGE} (context: ${ctx})" + IMAGE="${IMAGE_REGISTRY}/${OWNER}/${PREFIX}-${svc}:${TAG}" + echo "Building ${PREFIX}-${svc} -> ${IMAGE} (context: ${ctx})" docker build --build-arg ENV_FILE="${ENV_FILE}" -t "${IMAGE}" -f "$ctx/$dockerfile" "$ctx" echo "Pushing ${IMAGE}" docker push "${IMAGE}" done -echo "All done. Built and pushed services: ${SERVICES} with tag ${TAG} to ${IMAGE_REGISTRY}/${OWNER}/infoalloggi-:${TAG}" +echo "All done. Built and pushed services: ${SERVICES} with tag ${TAG} to ${IMAGE_REGISTRY}/${OWNER}/${PREFIX}-:${TAG}" diff --git a/deploy_infoalloggi.sh b/deploy_infoalloggi.sh new file mode 100644 index 0000000..8f6c082 --- /dev/null +++ b/deploy_infoalloggi.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Preset for staging environment +export TAG="latest" +export IMAGE_REGISTRY="ghcr.io" +export OWNER="marcopedone" +export SERVICES="web backend db" +export USERNAME="marcopedone" +export TOKEN="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" +export ENV_FILE=".env.infoalloggi" +export PREFIX="infoalloggi" + +./build_and_push_ghcr.sh \ No newline at end of file diff --git a/deploy_marcopedone.sh b/deploy_marcopedone.sh index b47af77..4532bdc 100644 --- a/deploy_marcopedone.sh +++ b/deploy_marcopedone.sh @@ -9,5 +9,6 @@ export SERVICES="web backend db" export USERNAME="marcopedone" export TOKEN="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" export ENV_FILE=".env.marcopedone" +export PREFIX="marcopedone" ./build_and_push_ghcr.sh \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index de175fb..d1f354a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -150,6 +150,7 @@ services: JWT_SECRET: ${JWT_SECRET} SKEBBY_USER: ${SKEBBY_USER} SKEBBY_PASS: ${SKEBBY_PASS} + STORAGE_TOKEN: ${STORAGE_TOKEN} working_dir: /app environment: INTERNAL_BASE_URL: http://web:3000 @@ -176,6 +177,7 @@ services: JWT_SECRET: ${JWT_SECRET} SKEBBY_USER: ${SKEBBY_USER} SKEBBY_PASS: ${SKEBBY_PASS} + STORAGE_TOKEN: ${STORAGE_TOKEN} networks: - dokploy-network ports: diff --git a/ref-docker-compose.yml b/ref-docker-compose.yml index ca35951..7c26508 100644 --- a/ref-docker-compose.yml +++ b/ref-docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/infoalloggi-db:${TAG:-latest}" + image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-db:${TAG:-latest}" environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} @@ -78,7 +78,7 @@ services: cpus: "0.5" backend: - image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/infoalloggi-backend:${TAG:-latest}" + image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}" networks: - dokploy-network expose: @@ -116,7 +116,7 @@ services: condition: service_healthy web: - image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/infoalloggi-web:${TAG:-latest}" + image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-web:${TAG:-latest}" working_dir: /app pull_policy: "always" environment: @@ -145,6 +145,7 @@ services: JWT_SECRET: ${JWT_SECRET} SKEBBY_USER: ${SKEBBY_USER} SKEBBY_PASS: ${SKEBBY_PASS} + STORAGE_TOKEN: ${STORAGE_TOKEN} networks: - dokploy-network ports: