diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml index 765e233..0dcdec9 100644 --- a/.github/workflows/docker-build-and-push.yml +++ b/.github/workflows/docker-build-and-push.yml @@ -24,14 +24,6 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.22 - - name: Install mupdf - run: sudo apt-get install -y mupdf - - name: Set library path - run: echo "/usr/lib" | sudo tee -a /etc/ld.so.conf.d/mupdf.conf && sudo ldconfig - - name: Install dependencies - run: go mod download - - name: Run Go tests - run: go test ./... - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -46,10 +38,20 @@ jobs: - name: Install frontend dependencies run: npm install working-directory: web-app + - name: Build frontend + run: npm run build && cp -r dist/ ../dist/ + working-directory: web-app - name: Run frontend tests run: npm test working-directory: web-app - + - name: Install mupdf + run: sudo apt-get install -y mupdf + - name: Set library path + run: echo "/usr/lib" | sudo tee -a /etc/ld.so.conf.d/mupdf.conf && sudo ldconfig + - name: Install dependencies + run: go mod download + - name: Run Go tests + run: go test ./... build-amd64: runs-on: ubuntu-latest needs: test diff --git a/Dockerfile b/Dockerfile index f824be2..9dae65a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,28 @@ ARG VERSION=docker-dev ARG COMMIT=unknown ARG BUILD_DATE=unknown -# Stage 1: Build the Go binary +# Stage 1: Build Vite frontend +FROM node:20-alpine AS frontend + +# Set the working directory inside the container +WORKDIR /app + +# Install necessary packages +RUN apk add --no-cache git + +# Copy package.json and package-lock.json +COPY web-app/package.json web-app/package-lock.json ./ + +# Install dependencies +RUN npm install + +# Copy the frontend code +COPY web-app /app/ + +# Build the frontend +RUN npm run build + +# Stage 2: Build the Go binary FROM golang:1.23.5-alpine3.21 AS builder # Set the working directory inside the container @@ -28,6 +49,7 @@ RUN apk add --no-cache \ "mupdf=${MUPDF_VERSION}" \ "mupdf-dev=${MUPDF_DEV_VERSION}" \ "sed=${SED_VERSION}" + # Copy go.mod and go.sum files COPY go.mod go.sum ./ @@ -37,7 +59,10 @@ RUN go mod download # Pre-compile go-sqlite3 to avoid doing this every time RUN CGO_ENABLED=1 go build -tags musl -o /dev/null github.com/mattn/go-sqlite3 -# Now copy the actual source files +# Copy the frontend build +COPY --from=frontend /app/dist /app/dist + +# Copy the Go source files COPY *.go . # Import ARGs from top level @@ -55,28 +80,7 @@ RUN sed -i \ # Build the binary using caching for both go modules and build cache RUN CGO_ENABLED=1 GOMAXPROCS=$(nproc) go build -tags musl -o paperless-gpt . -# Stage 2: Build Vite frontend -FROM node:20-alpine AS frontend - -# Set the working directory inside the container -WORKDIR /app - -# Install necessary packages -RUN apk add --no-cache git - -# Copy package.json and package-lock.json -COPY web-app/package.json web-app/package-lock.json ./ - -# Install dependencies -RUN npm install - -# Copy the frontend code -COPY web-app /app/ - -# Build the frontend -RUN npm run build - -# Stage 3: Create a lightweight image with the Go binary and frontend +# Stage 3: Create a lightweight image with just the binary FROM alpine:latest ENV GIN_MODE=release @@ -91,9 +95,6 @@ WORKDIR /app/ # Copy the Go binary from the builder stage COPY --from=builder /app/paperless-gpt . -# Copy the frontend build -COPY --from=frontend /app/dist /app/web-app/dist - # Expose the port the app runs on EXPOSE 8080 diff --git a/cline_docs/techContext.md b/cline_docs/techContext.md index a2f2631..148a91c 100644 --- a/cline_docs/techContext.md +++ b/cline_docs/techContext.md @@ -36,6 +36,20 @@ 4. Build and run paperless-gpt 5. Access web interface +### Testing Steps (Required Before Commits) +1. **Unit Tests**: + ```bash + go test . + ``` + +2. **E2E Tests**: + ```bash + docker build . -t icereed/paperless-gpt:e2e + cd web-app && npm run test:e2e + ``` + +These tests MUST be run and pass before considering any task complete. + ## Configuration ### Environment Variables diff --git a/embedded_assets.go b/embedded_assets.go new file mode 100644 index 0000000..019296d --- /dev/null +++ b/embedded_assets.go @@ -0,0 +1,54 @@ +package main + +import ( + "embed" + "io" + "io/fs" + "net/http" + "path" + "strings" + + "github.com/gin-gonic/gin" +) + +//go:embed dist/* +var webappContent embed.FS + +// CreateEmbeddedFileServer creates a http.FileSystem from our embedded files +func createEmbeddedFileServer() http.FileSystem { + // Strip the "dist" prefix from the embedded files + stripped, err := fs.Sub(webappContent, "dist") + if err != nil { + panic(err) + } + return http.FS(stripped) +} + +// ServeEmbeddedFile serves a file from the embedded filesystem +func serveEmbeddedFile(c *gin.Context, prefix string, filepath string) { + // If the path is empty or ends with "/", serve index.html + if filepath == "" || strings.HasSuffix(filepath, "/") { + filepath = path.Join(filepath, "index.html") + } + + // Try to open the file from our embedded filesystem + fullPath := path.Join("dist", prefix, filepath) + f, err := webappContent.Open(fullPath) + if err != nil { + // If file not found, serve 404 + log.Warnf("File not found: %s", fullPath) + http.Error(c.Writer, http.StatusText(http.StatusNotFound), http.StatusNotFound) + c.Status(http.StatusNotFound) + return + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + // Serve the file + http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), f.(io.ReadSeeker)) +} diff --git a/main.go b/main.go index 767d2b5..18968d8 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,6 @@ var ( visionLlmModel = os.Getenv("VISION_LLM_MODEL") logLevel = strings.ToLower(os.Getenv("LOG_LEVEL")) listenInterface = os.Getenv("LISTEN_INTERFACE") - webuiPath = os.Getenv("WEBUI_PATH") autoGenerateTitle = os.Getenv("AUTO_GENERATE_TITLE") autoGenerateTags = os.Getenv("AUTO_GENERATE_TAGS") autoGenerateCorrespondents = os.Getenv("AUTO_GENERATE_CORRESPONDENTS") @@ -247,16 +246,29 @@ func main() { }) } - if webuiPath == "" { - webuiPath = "./web-app/dist" - } - // Serve static files for the frontend under /assets - router.StaticFS("/assets", gin.Dir(webuiPath+"/assets", true)) - router.StaticFile("/vite.svg", webuiPath+"/vite.svg") + // Serve embedded web-app files + // router.GET("/*filepath", func(c *gin.Context) { + // filepath := c.Param("filepath") + // // Remove leading slash from filepath + // filepath = strings.TrimPrefix(filepath, "/") + // // Handle static assets under /assets/ + // serveEmbeddedFile(c, "", filepath) + // }) - // Catch-all route for serving the frontend - router.NoRoute(func(c *gin.Context) { - c.File(webuiPath + "/index.html") + // Instead of wildcard, serve specific files + router.GET("/favicon.ico", func(c *gin.Context) { + serveEmbeddedFile(c, "", "favicon.ico") + }) + router.GET("/vite.svg", func(c *gin.Context) { + serveEmbeddedFile(c, "", "vite.svg") + }) + router.GET("/assets/*filepath", func(c *gin.Context) { + filepath := c.Param("filepath") + fmt.Printf("Serving asset: %s\n", filepath) + serveEmbeddedFile(c, "assets", filepath) + }) + router.GET("/", func(c *gin.Context) { + serveEmbeddedFile(c, "", "index.html") }) // Start OCR worker pool