From c2a63cf4257c11cd869a48f225eed37be8853397 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:15:29 +0000 Subject: [PATCH] java frontend (#5097) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Reece Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> --- .dockerignore | 1 + .../workflows/PR-Demo-Comment-with-react.yml | 2 +- .github/workflows/build.yml | 4 +- .github/workflows/push-docker-v2.yml | 19 +-- .github/workflows/push-docker.yml | 6 +- .github/workflows/testdriver.yml | 2 +- .../software/common/util/RequestUriUtils.java | 1 + .../web/ReactRoutingController.java | 55 ++++++- .../configuration/SecurityConfiguration.java | 6 +- docker/embedded/Dockerfile | 138 +++++++++++++++++ docker/embedded/Dockerfile.fat | 142 ++++++++++++++++++ docker/embedded/Dockerfile.ultra-lite | 104 +++++++++++++ frontend/index.html | 8 +- frontend/src/core/constants/app.ts | 23 ++- .../contexts/TourOrchestrationContext.tsx | 2 +- .../configSections/AdminGeneralSection.tsx | 4 +- 16 files changed, 483 insertions(+), 34 deletions(-) create mode 100644 docker/embedded/Dockerfile create mode 100644 docker/embedded/Dockerfile.fat create mode 100644 docker/embedded/Dockerfile.ultra-lite diff --git a/.dockerignore b/.dockerignore index 54b49fd80..50f737668 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ frontend/dist frontend/build frontend/.vite frontend/.tauri +frontend/src-tauri/target # Gradle build artifacts .gradle diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 82f1e0140..bfde13275 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -180,7 +180,7 @@ jobs: uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . - file: ./Dockerfile + file: ./docker/embedded/Dockerfile push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }} build-args: VERSION_TAG=alpha diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03f684c4f..9a049bb90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -262,7 +262,7 @@ jobs: strategy: fail-fast: false matrix: - docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"] + docker-rev: ["docker/embedded/Dockerfile", "docker/embedded/Dockerfile.ultra-lite", "docker/embedded/Dockerfile.fat"] steps: - name: Harden Runner uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 @@ -301,7 +301,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./docker/backend/${{ matrix.docker-rev }} + file: ./${{ matrix.docker-rev }} push: false cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/push-docker-v2.yml b/.github/workflows/push-docker-v2.yml index 23d89ef88..5f2b70f50 100644 --- a/.github/workflows/push-docker-v2.yml +++ b/.github/workflows/push-docker-v2.yml @@ -5,6 +5,7 @@ on: push: branches: - V2-master + - alljavadocker # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed @@ -93,10 +94,10 @@ jobs: type=raw,value=${{ steps.versionNumber.outputs.versionNumber }} type=raw,value=latest - - name: Generate tags for latest (V2-demo branch - test) + - name: Generate tags for latest (alljavadocker branch - test) id: meta-test uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 - if: github.ref == 'refs/heads/V2-demo' + if: github.ref == 'refs/heads/alljavadocker' with: images: | ghcr.io/stirling-tools/stirling-pdf-test @@ -110,7 +111,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./docker/Dockerfile.unified + file: ./docker/embedded/Dockerfile push: true cache-from: type=gha cache-to: type=gha,mode=max @@ -149,10 +150,10 @@ jobs: type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat type=raw,value=latest-fat - - name: Generate tags for latest-fat (V2-demo branch - test) + - name: Generate tags for latest-fat (alljavadocker branch - test) id: meta-fat-test uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 - if: github.ref == 'refs/heads/V2-demo' + if: github.ref == 'refs/heads/alljavadocker' with: images: | ghcr.io/stirling-tools/stirling-pdf-test @@ -166,7 +167,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./docker/Dockerfile.unified + file: ./docker/embedded/Dockerfile.fat push: true cache-from: type=gha cache-to: type=gha,mode=max @@ -203,10 +204,10 @@ jobs: type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite type=raw,value=latest-ultra-lite - - name: Generate tags for ultra-lite (V2-demo branch - test) + - name: Generate tags for ultra-lite (alljavadocker branch - test) id: meta-lite-test uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 - if: github.ref == 'refs/heads/V2-demo' + if: github.ref == 'refs/heads/alljavadocker' with: images: | ghcr.io/stirling-tools/stirling-pdf-test @@ -220,7 +221,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./docker/Dockerfile.unified-lite + file: ./docker/embedded/Dockerfile.ultra-lite push: true cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 48e351433..3f2a0c8d0 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -107,7 +107,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./Dockerfile + file: ./docker/embedded/Dockerfile push: true cache-from: type=gha cache-to: type=gha,mode=max @@ -152,7 +152,7 @@ jobs: if: github.ref != 'refs/heads/main' with: context: . - file: ./Dockerfile.ultra-lite + file: ./docker/embedded/Dockerfile.ultra-lite push: true cache-from: type=gha cache-to: type=gha,mode=max @@ -183,7 +183,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./Dockerfile.fat + file: ./docker/embedded/Dockerfile.fat push: true cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 2bd47bee3..828c84d62 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -66,7 +66,7 @@ jobs: uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . - file: ./Dockerfile + file: ./docker/embedded/Dockerfile push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index 2dd3d420a..a0adec7de 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -39,6 +39,7 @@ public class RequestUriUtils { // Specific static files bundled with the frontend if (normalizedUri.equals("/robots.txt") || normalizedUri.equals("/favicon.ico") + || normalizedUri.equals("/manifest.json") || normalizedUri.equals("/site.webmanifest") || normalizedUri.equals("/manifest-classic.json") || normalizedUri.equals("/index.html")) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index daa6233ce..db0d95a36 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -1,20 +1,61 @@ package stirling.software.SPDF.controller.web; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import jakarta.servlet.http.HttpServletRequest; + @Controller public class ReactRoutingController { - @GetMapping( - "/{path:^(?!api|static|robots\\.txt|favicon\\.ico|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js)[^\\.]*$}") - public String forwardRootPaths() { - return "forward:/index.html"; + @Value("${server.servlet.context-path:/}") + private String contextPath; + + @GetMapping(value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE) + public ResponseEntity serveIndexHtml(HttpServletRequest request) + throws IOException { + ClassPathResource resource = new ClassPathResource("static/index.html"); + + try (InputStream inputStream = resource.getInputStream()) { + String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + // Replace %BASE_URL% with the actual context path for base href + String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/"; + html = html.replace("%BASE_URL%", baseUrl); + // Also rewrite any existing tag (Vite may have baked one in) + html = + html.replaceFirst( + "", + ""); + + // Inject context path as a global variable for API calls + String contextPathScript = + ""; + html = html.replace("", contextPathScript + ""); + + return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(html); + } } @GetMapping( - "/{path:^(?!api|static|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js)[^\\.]*}/{subpath:^(?!.*\\.).*$}") - public String forwardNestedPaths() { - return "forward:/index.html"; + "/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}") + public ResponseEntity forwardRootPaths(HttpServletRequest request) + throws IOException { + return serveIndexHtml(request); + } + + @GetMapping( + "/{path:^(?!api|static|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*}/{subpath:^(?!.*\\.).*$}") + public ResponseEntity forwardNestedPaths(HttpServletRequest request) + throws IOException { + return serveIndexHtml(request); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index e683fa8dc..a3a5eee4f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -324,10 +324,14 @@ public class SecurityConfiguration { .authenticated()); // Handle User/Password Logins if (securityProperties.isUserPass()) { + // v2: Authentication is handled via API (/api/v1/auth/login), not form login + // We configure form login to handle Spring Security redirects, + // but use /perform_login as the processing URL so /login remains a React route http.formLogin( formLogin -> formLogin - .loginPage("/login") + .loginPage("/login") // Redirect here when unauthenticated + .loginProcessingUrl("/perform_login") // Process form posts here (not /login) .successHandler( new CustomAuthenticationSuccessHandler( loginAttemptService, diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile new file mode 100644 index 000000000..a38aee9b4 --- /dev/null +++ b/docker/embedded/Dockerfile @@ -0,0 +1,138 @@ +# Stirling-PDF Dockerfile - Full version with embedded frontend +# Single JAR contains both frontend and backend + +# Stage 1: Build application with embedded frontend +FROM gradle:8.14-jdk21 AS build + +# Install Node.js and npm for frontend build +RUN apt-get update && apt-get install -y \ + curl \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && npm --version \ + && node --version \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy gradle files for dependency resolution +COPY build.gradle . +COPY settings.gradle . +COPY gradlew . +COPY gradle gradle/ +COPY app/core/build.gradle core/. +COPY app/common/build.gradle common/. +COPY app/proprietary/build.gradle proprietary/. +RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 + +# Set working directory +WORKDIR /app + +# Copy entire project +COPY . . + +# Build JAR with embedded frontend (includes security features controlled at runtime) +RUN DISABLE_ADDITIONAL_FEATURES=false \ + STIRLING_PDF_DESKTOP_UI=false \ + ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube + +# Stage 2: Runtime image +FROM alpine:3.22.1 + +ARG VERSION_TAG + +# Labels +LABEL org.opencontainers.image.title="Stirling-PDF" +LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Full version" +LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.vendor="Stirling-Tools" +LABEL org.opencontainers.image.url="https://www.stirlingpdf.com" +LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com" +LABEL maintainer="Stirling-Tools" +LABEL org.opencontainers.image.authors="Stirling-Tools" +LABEL org.opencontainers.image.version="${VERSION_TAG}" +LABEL org.opencontainers.image.keywords="PDF, manipulation, API, Spring Boot, React" + +# Copy scripts and fonts +COPY scripts /scripts +COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ + +# Copy built JAR from build stage +COPY --from=build /app/app/core/build/libs/*.jar /app.jar +COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar + +# Environment Variables +ENV VERSION_TAG=$VERSION_TAG \ + JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ + JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ + UNO_PATH=/usr/lib/libreoffice/program \ + URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ + PATH=$PATH:/opt/venv/bin \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf + +# Install all dependencies +RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ + echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ + echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ + apk upgrade --no-cache -a && \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + tini \ + bash \ + curl \ + shadow \ + su-exec \ + openssl \ + openssl-dev \ + openjdk21-jre \ + # Doc conversion + gcompat \ + libc6-compat \ + libreoffice \ + ghostscript \ + fontforge \ + # pdftohtml + poppler-utils \ + # OCR MY PDF + unpaper \ + tesseract-ocr-data-eng \ + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ + ocrmypdf \ + # CV + py3-opencv \ + python3 \ + py3-pip \ + py3-pillow@testing \ + py3-pdf2image@testing && \ + python3 -m venv /opt/venv && \ + /opt/venv/bin/pip install --upgrade pip setuptools && \ + /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ + ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ + mv /usr/share/tessdata /usr/share/tessdata-original && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ + fc-cache -f -v && \ + chmod +x /scripts/* && \ + # User permissions + addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /tmp/stirling-pdf && \ + chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar + +EXPOSE 8080/tcp + +# Set user and run command +ENTRYPOINT ["tini", "--", "/scripts/init.sh"] +CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat new file mode 100644 index 000000000..67e648aee --- /dev/null +++ b/docker/embedded/Dockerfile.fat @@ -0,0 +1,142 @@ +# Stirling-PDF Dockerfile - Fat version with embedded frontend +# Single JAR contains both frontend and backend with extra fonts for air-gapped environments + +# Stage 1: Build application with embedded frontend +FROM gradle:8.14-jdk21 AS build + +# Install Node.js and npm for frontend build +RUN apt-get update && apt-get install -y \ + curl \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && npm --version \ + && node --version \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy gradle files for dependency resolution +COPY build.gradle . +COPY settings.gradle . +COPY gradlew . +COPY gradle gradle/ +COPY app/core/build.gradle core/. +COPY app/common/build.gradle common/. +COPY app/proprietary/build.gradle proprietary/. +RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 + +# Set working directory +WORKDIR /app + +# Copy entire project +COPY . . + +# Build JAR with embedded frontend (includes security features controlled at runtime) +RUN DISABLE_ADDITIONAL_FEATURES=false \ + STIRLING_PDF_DESKTOP_UI=false \ + ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube + +# Stage 2: Runtime image +FROM alpine:3.22.1 + +ARG VERSION_TAG + +# Labels +LABEL org.opencontainers.image.title="Stirling-PDF Fat" +LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Fat version with extra fonts for air-gapped environments" +LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.vendor="Stirling-Tools" +LABEL org.opencontainers.image.url="https://www.stirlingpdf.com" +LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com" +LABEL maintainer="Stirling-Tools" +LABEL org.opencontainers.image.authors="Stirling-Tools" +LABEL org.opencontainers.image.version="${VERSION_TAG}" +LABEL org.opencontainers.image.keywords="PDF, manipulation, fat, air-gapped, API, Spring Boot, React" + +# Copy scripts and fonts +COPY scripts /scripts +COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ + +# Copy built JAR from build stage +COPY --from=build /app/app/core/build/libs/*.jar /app.jar +COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar + +# Environment Variables +ENV VERSION_TAG=$VERSION_TAG \ + JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ + JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + FAT_DOCKER=true \ + INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ + PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ + UNO_PATH=/usr/lib/libreoffice/program \ + URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ + PATH=$PATH:/opt/venv/bin \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf + +# Install all dependencies plus extra fonts for air-gapped environments +RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ + echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ + echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ + apk upgrade --no-cache -a && \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + tini \ + bash \ + curl \ + shadow \ + su-exec \ + openssl \ + openssl-dev \ + openjdk21-jre \ + # Doc conversion + gcompat \ + libc6-compat \ + libreoffice \ + ghostscript \ + fontforge \ + # pdftohtml + poppler-utils \ + # OCR MY PDF + unpaper \ + tesseract-ocr-data-eng \ + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ + ocrmypdf \ + # Extra fonts for fat version + font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \ + # CV + py3-opencv \ + python3 \ + py3-pip \ + py3-pillow@testing \ + py3-pdf2image@testing && \ + python3 -m venv /opt/venv && \ + /opt/venv/bin/pip install --upgrade pip setuptools && \ + /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ + ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ + ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ + mv /usr/share/tessdata /usr/share/tessdata-original && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ + fc-cache -f -v && \ + chmod +x /scripts/* && \ + # User permissions + addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /tmp/stirling-pdf && \ + chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar + +EXPOSE 8080/tcp + +# Set user and run command +ENTRYPOINT ["tini", "--", "/scripts/init.sh"] +CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite new file mode 100644 index 000000000..a39a522eb --- /dev/null +++ b/docker/embedded/Dockerfile.ultra-lite @@ -0,0 +1,104 @@ +# Stirling-PDF Dockerfile - Ultra-lite version with embedded frontend +# Single JAR contains both frontend and backend with minimal dependencies + +# Stage 1: Build application with embedded frontend +FROM gradle:8.14-jdk21 AS build + +# Install Node.js and npm for frontend build +RUN apt-get update && apt-get install -y \ + curl \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && npm --version \ + && node --version \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy gradle files for dependency resolution +COPY build.gradle . +COPY settings.gradle . +COPY gradlew . +COPY gradle gradle/ +COPY app/core/build.gradle core/. +COPY app/common/build.gradle common/. +COPY app/proprietary/build.gradle proprietary/. +RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 + +# Set working directory +WORKDIR /app + +# Copy entire project +COPY . . + +# Build ultra-lite JAR with embedded frontend (minimal features) +RUN DISABLE_ADDITIONAL_FEATURES=true \ + STIRLING_PDF_DESKTOP_UI=false \ + ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube + +# Stage 2: Runtime image +FROM alpine:3.22.1 + +ARG VERSION_TAG + +# Labels +LABEL org.opencontainers.image.title="Stirling-PDF Ultra-Lite" +LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Ultra-lite version with minimal dependencies" +LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.vendor="Stirling-Tools" +LABEL org.opencontainers.image.url="https://www.stirlingpdf.com" +LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com" +LABEL maintainer="Stirling-Tools" +LABEL org.opencontainers.image.authors="Stirling-Tools" +LABEL org.opencontainers.image.version="${VERSION_TAG}" +LABEL org.opencontainers.image.keywords="PDF, manipulation, ultra-lite, API, Spring Boot, React" + +# Copy scripts +COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh +COPY scripts/installFonts.sh /scripts/installFonts.sh + +# Copy built JAR from build stage +COPY --from=build /app/app/core/build/libs/*.jar /app.jar +COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar + +# Environment Variables +ENV VERSION_TAG=$VERSION_TAG \ + JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ + JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + ENDPOINTS_GROUPS_TO_REMOVE=CLI + +# Install minimal dependencies +RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ + echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ + echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ + apk upgrade --no-cache -a && \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + tini \ + bash \ + curl \ + shadow \ + su-exec \ + openjdk21-jre && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ + mkdir -p /usr/share/fonts/opentype/noto && \ + chmod +x /scripts/*.sh && \ + # User permissions + addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \ + chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar + +EXPOSE 8080/tcp + +# Set user and run command +ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"] +CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"] diff --git a/frontend/index.html b/frontend/index.html index 5dbaa1893..5042c6a41 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,21 +3,21 @@ - + - - + + Stirling PDF
- + diff --git a/frontend/src/core/constants/app.ts b/frontend/src/core/constants/app.ts index 04057fa37..aedb81a68 100644 --- a/frontend/src/core/constants/app.ts +++ b/frontend/src/core/constants/app.ts @@ -1,7 +1,24 @@ -// Base path from Vite config - build-time constant, normalized (no trailing slash) -// When no subpath, use empty string instead of '.' to avoid relative path issues -export const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '').replace(/^\.$/, ''); +// Base path - read from tag at runtime to support dynamic subpaths +// Falls back to Vite's BASE_URL for build-time subpaths +// Normalized: no trailing slash, empty string instead of '.' or '/' +function getBasePath(): string { + // Try to read from tag first (runtime subpath support) + if (typeof document !== 'undefined') { + const baseElement = document.querySelector('base'); + if (baseElement) { + const href = baseElement.getAttribute('href'); + if (href && href !== '%BASE_URL%') { + return href.replace(/\/$/, '').replace(/^\.$/, '').replace(/^\/$/, ''); + } + } + } + + // Fall back to Vite's BASE_URL (build-time subpath) + return (import.meta.env.BASE_URL || '/').replace(/\/$/, '').replace(/^\.$/, '').replace(/^\/$/, ''); +} + +export const BASE_PATH = getBasePath(); // EmbedPDF needs time to remove annotations internally before a recreation runs. // Without the buffer we occasionally end up with duplicate annotations or stale image data. diff --git a/frontend/src/core/contexts/TourOrchestrationContext.tsx b/frontend/src/core/contexts/TourOrchestrationContext.tsx index d8b7afdc0..f8364ba5c 100644 --- a/frontend/src/core/contexts/TourOrchestrationContext.tsx +++ b/frontend/src/core/contexts/TourOrchestrationContext.tsx @@ -110,7 +110,7 @@ export const TourOrchestrationProvider: React.FC<{ children: React.ReactNode }> const loadSampleFile = useCallback(async () => { try { - const response = await fetch('/samples/Sample.pdf'); + const response = await fetch('samples/Sample.pdf'); const blob = await response.blob(); const file = new File([blob], 'Sample.pdf', { type: 'application/pdf' }); diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx index 0d71d4cac..faebf4d48 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminGeneralSection.tsx @@ -335,7 +335,7 @@ export default function AdminGeneralSection() { label: (
{t('admin.settings.general.logoStyle.classicAlt', @@ -348,7 +348,7 @@ export default function AdminGeneralSection() { label: (
{t('admin.settings.general.logoStyle.modernAlt',