mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Update SaaS to latest main (#6667)
# Description of Changes > [!warning] > **Do not** squash this on merge. It should be merged via a merge commit Fixes conflicts in `pgvector_store.py`. Also since codespell is failing, add comments to ignore the errors in `sync_en_us_spelling.py` --------- Co-authored-by: Ludy <[email protected]> Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
build: &build
|
build: &build
|
||||||
- build.gradle
|
- build.gradle
|
||||||
- app/(common|core|proprietary)/build.gradle
|
- app/(common|core|proprietary)/build.gradle
|
||||||
|
- Taskfile.yml
|
||||||
|
- .taskfiles/backend.yml
|
||||||
|
|
||||||
openapi: &openapi
|
openapi: &openapi
|
||||||
- *build
|
- *build
|
||||||
@@ -38,6 +40,9 @@ project: &project
|
|||||||
- frontend/**
|
- frontend/**
|
||||||
- docker/**
|
- docker/**
|
||||||
- scripts/RestartHelper.java
|
- scripts/RestartHelper.java
|
||||||
|
- Taskfile.yml
|
||||||
|
- .taskfiles/backend.yml
|
||||||
|
- .taskfiles/docker.yml
|
||||||
- scripts/db-migration/**
|
- scripts/db-migration/**
|
||||||
- .github/workflows/db-migration-test.yml
|
- .github/workflows/db-migration-test.yml
|
||||||
|
|
||||||
@@ -55,6 +60,9 @@ frontend: &frontend
|
|||||||
- scripts/summarize_type3_signatures.py
|
- scripts/summarize_type3_signatures.py
|
||||||
- scripts/type3_to_cff.py
|
- scripts/type3_to_cff.py
|
||||||
- scripts/update_type3_library.py
|
- scripts/update_type3_library.py
|
||||||
|
- Taskfile.yml
|
||||||
|
- .taskfiles/frontend.yml
|
||||||
|
- .taskfiles/e2e.yml
|
||||||
|
|
||||||
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
|
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
|
||||||
# job on changes to any of these.
|
# job on changes to any of these.
|
||||||
@@ -66,6 +74,8 @@ tauri: &tauri
|
|||||||
- frontend/package-lock.json
|
- frontend/package-lock.json
|
||||||
- frontend/editor/vite.config.ts
|
- frontend/editor/vite.config.ts
|
||||||
- .github/workflows/tauri-build.yml
|
- .github/workflows/tauri-build.yml
|
||||||
|
- Taskfile.yml
|
||||||
|
- .taskfiles/desktop.yml
|
||||||
|
|
||||||
# Files that affect the AI engine (Python tool models, fixers, tests). Gate
|
# Files that affect the AI engine (Python tool models, fixers, tests). Gate
|
||||||
# the engine validation job on changes to engine sources or to the Java
|
# the engine validation job on changes to engine sources or to the Java
|
||||||
@@ -74,6 +84,8 @@ engine: &engine
|
|||||||
- engine/**
|
- engine/**
|
||||||
- app/(common|core|proprietary)/src/main/java/**
|
- app/(common|core|proprietary)/src/main/java/**
|
||||||
- .github/workflows/ai-engine.yml
|
- .github/workflows/ai-engine.yml
|
||||||
|
- Taskfile.yml
|
||||||
|
- .taskfiles/engine.yml
|
||||||
|
|
||||||
licenses-frontend: &licenses-frontend
|
licenses-frontend: &licenses-frontend
|
||||||
- ".github/workflows/frontend-backend-licenses-update.yml"
|
- ".github/workflows/frontend-backend-licenses-update.yml"
|
||||||
@@ -102,4 +114,4 @@ proprietary: &proprietary
|
|||||||
- configs/settings.yml.template
|
- configs/settings.yml.template
|
||||||
- build.gradle
|
- build.gradle
|
||||||
- app/proprietary/build.gradle
|
- app/proprietary/build.gradle
|
||||||
- .github/workflows/build-enterprise.yml
|
- .github/workflows/build-enterprise.yml
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
# No `-PnoSpotless` here yet because the upstream cache layer matches the
|
# No `-PnoSpotless` here yet because the upstream cache layer matches the
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: matrix.variant.build_frontend == true
|
if: matrix.variant.build_frontend == true
|
||||||
@@ -252,7 +252,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Setup Task
|
- name: Setup Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 9.3.1
|
gradle-version: 9.5.1
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build
|
run: ./gradlew build
|
||||||
|
|||||||
@@ -127,14 +127,15 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- rm -rf runtime/jre
|
- rm -rf runtime/jre
|
||||||
- mkdir -p runtime
|
- mkdir -p runtime
|
||||||
- >-
|
- |
|
||||||
jlink
|
JLINK_COMPRESS="$(jlink --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)"
|
||||||
--add-modules {{.JLINK_MODULES}}
|
jlink \
|
||||||
--strip-debug
|
--add-modules {{.JLINK_MODULES}} \
|
||||||
--compress=zip-6
|
--strip-debug \
|
||||||
--no-header-files
|
--compress="$JLINK_COMPRESS" \
|
||||||
--no-man-pages
|
--no-header-files \
|
||||||
--output runtime/jre
|
--no-man-pages \
|
||||||
|
--output runtime/jre
|
||||||
# jlink emits its files mode 444 (read-only). Tauri's build-script
|
# jlink emits its files mode 444 (read-only). Tauri's build-script
|
||||||
# resource copier preserves source permissions when staging
|
# resource copier preserves source permissions when staging
|
||||||
# `runtime/jre/**/*` into `target/<profile>/runtime/jre/...`, so the
|
# `runtime/jre/**/*` into `target/<profile>/runtime/jre/...`, so the
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package stirling.software.common.configuration;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
@@ -122,17 +121,17 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "RunningInDocker")
|
@Bean(name = "RunningInDocker")
|
||||||
public boolean runningInDocker() {
|
public boolean runningInDocker() {
|
||||||
return Files.exists(Paths.get("/.dockerenv"));
|
return Files.exists(Path.of("/.dockerenv"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "configDirMounted")
|
@Bean(name = "configDirMounted")
|
||||||
public boolean isRunningInDockerWithConfig() {
|
public boolean isRunningInDockerWithConfig() {
|
||||||
Path dockerEnv = Paths.get("/.dockerenv");
|
Path dockerEnv = Path.of("/.dockerenv");
|
||||||
// default to true if not docker
|
// default to true if not docker
|
||||||
if (!Files.exists(dockerEnv)) {
|
if (!Files.exists(dockerEnv)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Path mountInfo = Paths.get("/proc/1/mountinfo");
|
Path mountInfo = Path.of("/proc/1/mountinfo");
|
||||||
// this should always exist, if not some unknown usecase
|
// this should always exist, if not some unknown usecase
|
||||||
if (!Files.exists(mountInfo)) {
|
if (!Files.exists(mountInfo)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+3
-4
@@ -7,7 +7,6 @@ import java.net.URISyntaxException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ public class ConfigInitializer {
|
|||||||
|
|
||||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||||
// 1) If settings file doesn't exist, create from template
|
// 1) If settings file doesn't exist, create from template
|
||||||
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
Path destPath = Path.of(InstallationPathConfig.getSettingsPath());
|
||||||
|
|
||||||
boolean settingsFileExists = Files.exists(destPath);
|
boolean settingsFileExists = Files.exists(destPath);
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ public class ConfigInitializer {
|
|||||||
if (settingsFileExists) {
|
if (settingsFileExists) {
|
||||||
// move settings.yml to settings.yml.{timestamp}.bak
|
// move settings.yml to settings.yml.{timestamp}.bak
|
||||||
Path backupPath =
|
Path backupPath =
|
||||||
Paths.get(
|
Path.of(
|
||||||
InstallationPathConfig.getSettingsPath()
|
InstallationPathConfig.getSettingsPath()
|
||||||
+ "."
|
+ "."
|
||||||
+ System.currentTimeMillis()
|
+ System.currentTimeMillis()
|
||||||
@@ -96,7 +95,7 @@ public class ConfigInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3) Ensure custom settings file exists
|
// 3) Ensure custom settings file exists
|
||||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
Path customSettingsPath = Path.of(InstallationPathConfig.getCustomSettingsPath());
|
||||||
if (Files.notExists(customSettingsPath)) {
|
if (Files.notExists(customSettingsPath)) {
|
||||||
Files.createFile(customSettingsPath);
|
Files.createFile(customSettingsPath);
|
||||||
log.info("Created custom_settings file: {}", customSettingsPath);
|
log.info("Created custom_settings file: {}", customSettingsPath);
|
||||||
|
|||||||
+5
-6
@@ -3,7 +3,6 @@ package stirling.software.common.configuration;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.InvalidPathException;
|
import java.nio.file.InvalidPathException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -201,7 +200,7 @@ public class RuntimePathConfig {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Normalize to absolute path
|
// Normalize to absolute path
|
||||||
Path path = Paths.get(pathStr.trim()).toAbsolutePath().normalize();
|
Path path = Path.of(pathStr.trim()).toAbsolutePath().normalize();
|
||||||
String normalizedPath = path.toString();
|
String normalizedPath = path.toString();
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
@@ -224,9 +223,9 @@ public class RuntimePathConfig {
|
|||||||
|
|
||||||
private void detectOverlappingPaths(List<String> paths) {
|
private void detectOverlappingPaths(List<String> paths) {
|
||||||
for (int i = 0; i < paths.size(); i++) {
|
for (int i = 0; i < paths.size(); i++) {
|
||||||
Path path1 = Paths.get(paths.get(i));
|
Path path1 = Path.of(paths.get(i));
|
||||||
for (int j = i + 1; j < paths.size(); j++) {
|
for (int j = i + 1; j < paths.size(); j++) {
|
||||||
Path path2 = Paths.get(paths.get(j));
|
Path path2 = Path.of(paths.get(j));
|
||||||
|
|
||||||
// Check if one path is a parent of the other
|
// Check if one path is a parent of the other
|
||||||
if (path1.startsWith(path2)) {
|
if (path1.startsWith(path2)) {
|
||||||
@@ -246,10 +245,10 @@ public class RuntimePathConfig {
|
|||||||
|
|
||||||
private void validatePipelinePaths() {
|
private void validatePipelinePaths() {
|
||||||
try {
|
try {
|
||||||
Path finishedPath = Paths.get(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
|
Path finishedPath = Path.of(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
|
||||||
|
|
||||||
for (String watchedPathStr : pipelineWatchedFoldersPaths) {
|
for (String watchedPathStr : pipelineWatchedFoldersPaths) {
|
||||||
Path watchedPath = Paths.get(watchedPathStr).toAbsolutePath().normalize();
|
Path watchedPath = Path.of(watchedPathStr).toAbsolutePath().normalize();
|
||||||
|
|
||||||
// Check if watched folder is same as finished folder
|
// Check if watched folder is same as finished folder
|
||||||
if (watchedPath.equals(finishedPath)) {
|
if (watchedPath.equals(finishedPath)) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.model;
|
package stirling.software.common.model;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -24,7 +23,7 @@ public class FileInfo {
|
|||||||
|
|
||||||
// Converts the file path string to a Path object.
|
// Converts the file path string to a Path object.
|
||||||
public Path getFilePathAsPath() {
|
public Path getFilePathAsPath() {
|
||||||
return Paths.get(filePath);
|
return Path.of(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formats the file size into a human-readable string.
|
// Formats the file size into a human-readable string.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package stirling.software.common.service;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -35,7 +34,7 @@ public class MobileScannerService {
|
|||||||
public MobileScannerService() throws IOException {
|
public MobileScannerService() throws IOException {
|
||||||
// Create temp directory for mobile scanner uploads
|
// Create temp directory for mobile scanner uploads
|
||||||
this.tempDirectory =
|
this.tempDirectory =
|
||||||
Paths.get(System.getProperty("java.io.tmpdir"), "stirling-mobile-scanner");
|
Path.of(System.getProperty("java.io.tmpdir"), "stirling-mobile-scanner");
|
||||||
Files.createDirectories(tempDirectory);
|
Files.createDirectories(tempDirectory);
|
||||||
log.info("Mobile scanner temp directory: {}", tempDirectory);
|
log.info("Mobile scanner temp directory: {}", tempDirectory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.lang.management.OperatingSystemMXBean;
|
|||||||
import java.lang.management.RuntimeMXBean;
|
import java.lang.management.RuntimeMXBean;
|
||||||
import java.lang.management.ThreadMXBean;
|
import java.lang.management.ThreadMXBean;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Path;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -160,7 +160,7 @@ public class PostHogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isRunningInDocker() {
|
private boolean isRunningInDocker() {
|
||||||
return Files.exists(Paths.get("/.dockerenv"));
|
return Files.exists(Path.of("/.dockerenv"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> getDockerMetrics() {
|
private Map<String, Object> getDockerMetrics() {
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ public class GeneralUtils {
|
|||||||
String pattern = locationPattern;
|
String pattern = locationPattern;
|
||||||
if (pattern.startsWith("file:")) {
|
if (pattern.startsWith("file:")) {
|
||||||
String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", "");
|
String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", "");
|
||||||
Path normalizePath = Paths.get(rawPath).normalize();
|
Path normalizePath = Path.of(rawPath).normalize();
|
||||||
pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
||||||
}
|
}
|
||||||
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||||
@@ -837,7 +837,7 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean createDir(String path) {
|
public boolean createDir(String path) {
|
||||||
Path folder = Paths.get(path);
|
Path folder = Path.of(path);
|
||||||
if (!Files.exists(folder)) {
|
if (!Files.exists(folder)) {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(folder);
|
Files.createDirectories(folder);
|
||||||
@@ -867,7 +867,7 @@ public class GeneralUtils {
|
|||||||
|
|
||||||
public void saveKeyToSettings(String key, Object newValue) throws IOException {
|
public void saveKeyToSettings(String key, Object newValue) throws IOException {
|
||||||
String[] keyArray = key.split("\\.");
|
String[] keyArray = key.split("\\.");
|
||||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
Path settingsPath = Path.of(InstallationPathConfig.getSettingsPath());
|
||||||
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
||||||
settingsYaml.updateValue(Arrays.asList(keyArray), newValue);
|
settingsYaml.updateValue(Arrays.asList(keyArray), newValue);
|
||||||
settingsYaml.saveOverride(settingsPath);
|
settingsYaml.saveOverride(settingsPath);
|
||||||
@@ -888,7 +888,7 @@ public class GeneralUtils {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
Path settingsPath = Path.of(InstallationPathConfig.getSettingsPath());
|
||||||
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
||||||
|
|
||||||
// Apply all updates to the same YamlHelper instance
|
// Apply all updates to the same YamlHelper instance
|
||||||
@@ -974,11 +974,11 @@ public class GeneralUtils {
|
|||||||
*/
|
*/
|
||||||
public void extractPipeline() throws IOException {
|
public void extractPipeline() throws IOException {
|
||||||
Path pipelineDir =
|
Path pipelineDir =
|
||||||
Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
|
Path.of(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
|
||||||
Files.createDirectories(pipelineDir);
|
Files.createDirectories(pipelineDir);
|
||||||
|
|
||||||
for (String name : DEFAULT_VALID_PIPELINE) {
|
for (String name : DEFAULT_VALID_PIPELINE) {
|
||||||
if (!Paths.get(name).getFileName().toString().equals(name)) {
|
if (!Path.of(name).getFileName().toString().equals(name)) {
|
||||||
log.error("Invalid pipeline file name: {}", name);
|
log.error("Invalid pipeline file name: {}", name);
|
||||||
throw new IllegalArgumentException("Invalid pipeline file name: " + name);
|
throw new IllegalArgumentException("Invalid pipeline file name: " + name);
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1014,7 @@ public class GeneralUtils {
|
|||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"scriptName must not contain path traversal characters");
|
"scriptName must not contain path traversal characters");
|
||||||
}
|
}
|
||||||
if (!Paths.get(scriptName).getFileName().toString().equals(scriptName)) {
|
if (!Path.of(scriptName).getFileName().toString().equals(scriptName)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"scriptName must not contain path traversal characters");
|
"scriptName must not contain path traversal characters");
|
||||||
}
|
}
|
||||||
@@ -1024,7 +1024,7 @@ public class GeneralUtils {
|
|||||||
"scriptName must be either 'png_to_webp.py' or 'split_photos.py'");
|
"scriptName must be either 'png_to_webp.py' or 'split_photos.py'");
|
||||||
}
|
}
|
||||||
|
|
||||||
Path scriptsDir = Paths.get(InstallationPathConfig.getScriptsPath(), PYTHON_SCRIPTS_DIR);
|
Path scriptsDir = Path.of(InstallationPathConfig.getScriptsPath(), PYTHON_SCRIPTS_DIR);
|
||||||
Files.createDirectories(scriptsDir);
|
Files.createDirectories(scriptsDir);
|
||||||
|
|
||||||
Path target = scriptsDir.resolve(scriptName);
|
Path target = scriptsDir.resolve(scriptName);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.File;
|
|||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ public class JarPathUtil {
|
|||||||
public static Path currentJar() {
|
public static Path currentJar() {
|
||||||
try {
|
try {
|
||||||
Path jar =
|
Path jar =
|
||||||
Paths.get(
|
Path.of(
|
||||||
JarPathUtil.class
|
JarPathUtil.class
|
||||||
.getProtectionDomain()
|
.getProtectionDomain()
|
||||||
.getCodeSource()
|
.getCodeSource()
|
||||||
@@ -61,14 +60,14 @@ public class JarPathUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Location 2: ./build/libs/ (development build)
|
// Location 2: ./build/libs/ (development build)
|
||||||
possibleLocations[1] = Paths.get("build", "libs", "restart-helper.jar").toAbsolutePath();
|
possibleLocations[1] = Path.of("build", "libs", "restart-helper.jar").toAbsolutePath();
|
||||||
|
|
||||||
// Location 3: app/common/build/libs/ (multi-module build)
|
// Location 3: app/common/build/libs/ (multi-module build)
|
||||||
possibleLocations[2] =
|
possibleLocations[2] =
|
||||||
Paths.get("app", "common", "build", "libs", "restart-helper.jar").toAbsolutePath();
|
Path.of("app", "common", "build", "libs", "restart-helper.jar").toAbsolutePath();
|
||||||
|
|
||||||
// Location 4: Current working directory
|
// Location 4: Current working directory
|
||||||
possibleLocations[3] = Paths.get("restart-helper.jar").toAbsolutePath();
|
possibleLocations[3] = Path.of("restart-helper.jar").toAbsolutePath();
|
||||||
|
|
||||||
// Check each location
|
// Check each location
|
||||||
for (Path location : possibleLocations) {
|
for (Path location : possibleLocations) {
|
||||||
|
|||||||
+4
-5
@@ -2,7 +2,7 @@ package stirling.software.common.model;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -49,16 +49,15 @@ class ApplicationPropertiesLogicTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void tempFileManagement_defaults_and_overrides() {
|
void tempFileManagement_defaults_and_overrides() {
|
||||||
Function<String, String> normalize = s -> Paths.get(s).normalize().toString();
|
Function<String, String> normalize = s -> Path.of(s).normalize().toString();
|
||||||
ApplicationProperties.TempFileManagement tfm =
|
ApplicationProperties.TempFileManagement tfm =
|
||||||
new ApplicationProperties.TempFileManagement();
|
new ApplicationProperties.TempFileManagement();
|
||||||
|
|
||||||
String expectedBase =
|
String expectedBase =
|
||||||
Paths.get(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf")
|
Path.of(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf").toString();
|
||||||
.toString();
|
|
||||||
assertEquals(expectedBase, tfm.getBaseTmpDir());
|
assertEquals(expectedBase, tfm.getBaseTmpDir());
|
||||||
|
|
||||||
String expectedLibre = Paths.get(expectedBase, "libreoffice").toString();
|
String expectedLibre = Path.of(expectedBase, "libreoffice").toString();
|
||||||
assertEquals(expectedLibre, tfm.getLibreofficeDir());
|
assertEquals(expectedLibre, tfm.getLibreofficeDir());
|
||||||
|
|
||||||
tfm.setBaseTmpDir("/custom/base");
|
tfm.setBaseTmpDir("/custom/base");
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -75,7 +74,7 @@ public class SPDFApplication {
|
|||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
// External config files
|
// External config files
|
||||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
Path settingsPath = Path.of(InstallationPathConfig.getSettingsPath());
|
||||||
log.info("Settings file: {}", settingsPath.toString());
|
log.info("Settings file: {}", settingsPath.toString());
|
||||||
if (Files.exists(settingsPath)) {
|
if (Files.exists(settingsPath)) {
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
@@ -84,7 +83,7 @@ public class SPDFApplication {
|
|||||||
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
|
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
Path customSettingsPath = Path.of(InstallationPathConfig.getCustomSettingsPath());
|
||||||
log.info("Custom settings file: {}", customSettingsPath.toString());
|
log.info("Custom settings file: {}", customSettingsPath.toString());
|
||||||
if (Files.exists(customSettingsPath)) {
|
if (Files.exists(customSettingsPath)) {
|
||||||
String existingLocation =
|
String existingLocation =
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup(cmd);
|
checkDependencyAndDisableGroup(cmd);
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
invokeAllWithTimeout(tasks, DEFAULT_TIMEOUT.plusSeconds(3));
|
invokeAllWithTimeout(tasks, DEFAULT_TIMEOUT.plusSeconds(3));
|
||||||
|
|
||||||
// Python / OpenCV special handling
|
// Python / OpenCV special handling
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import java.io.InputStream;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ public class UIDataController {
|
|||||||
|
|
||||||
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||||
try (Stream<Path> paths =
|
try (Stream<Path> paths =
|
||||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
Files.walk(Path.of(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||||
List<Path> jsonFiles =
|
List<Path> jsonFiles =
|
||||||
paths.filter(Files::isRegularFile)
|
paths.filter(Files::isRegularFile)
|
||||||
.filter(p -> p.toString().endsWith(".json"))
|
.filter(p -> p.toString().endsWith(".json"))
|
||||||
|
|||||||
+1
-2
@@ -9,7 +9,6 @@ import java.awt.print.PrinterJob;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -54,7 +53,7 @@ public class PrintFileController {
|
|||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String originalFilename = file.getOriginalFilename();
|
String originalFilename = file.getOriginalFilename();
|
||||||
if (originalFilename != null
|
if (originalFilename != null
|
||||||
&& (originalFilename.contains("..") || Paths.get(originalFilename).isAbsolute())) {
|
&& (originalFilename.contains("..") || Path.of(originalFilename).isAbsolute())) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.invalid.filepath", "Invalid file path detected: " + originalFilename);
|
"error.invalid.filepath", "Invalid file path detected: " + originalFilename);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -7,7 +7,6 @@ import java.nio.file.FileVisitOption;
|
|||||||
import java.nio.file.FileVisitResult;
|
import java.nio.file.FileVisitResult;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.SimpleFileVisitor;
|
import java.nio.file.SimpleFileVisitor;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
@@ -82,7 +81,7 @@ public class PipelineDirectoryProcessor {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for (String watchedFoldersDir : watchedFoldersDirs) {
|
for (String watchedFoldersDir : watchedFoldersDirs) {
|
||||||
scanWatchedFolder(Paths.get(watchedFoldersDir).toAbsolutePath());
|
scanWatchedFolder(Path.of(watchedFoldersDir).toAbsolutePath());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up ThreadLocal to prevent memory leaks
|
// Clean up ThreadLocal to prevent memory leaks
|
||||||
@@ -442,7 +441,7 @@ public class PipelineDirectoryProcessor {
|
|||||||
.replace("{outputFolder}", finishedFoldersDir)
|
.replace("{outputFolder}", finishedFoldersDir)
|
||||||
.replace("{folderName}", dir.toString()))
|
.replace("{folderName}", dir.toString()))
|
||||||
.replaceAll("");
|
.replaceAll("");
|
||||||
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
|
return Path.of(outputDir).isAbsolute() ? Path.of(outputDir) : Path.of(".", outputDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
|
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,6 @@ import java.net.URLDecoder;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -326,12 +325,12 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
Path normalizedPath = Paths.get(file.getName()).normalize();
|
Path normalizedPath = Path.of(file.getName()).normalize();
|
||||||
if (normalizedPath.startsWith("..")) {
|
if (normalizedPath.startsWith("..")) {
|
||||||
throw new SecurityException(
|
throw new SecurityException(
|
||||||
"Potential path traversal attempt in file name: " + file.getName());
|
"Potential path traversal attempt in file name: " + file.getName());
|
||||||
}
|
}
|
||||||
Path path = Paths.get(file.getAbsolutePath());
|
Path path = Path.of(file.getAbsolutePath());
|
||||||
// debug statement
|
// debug statement
|
||||||
log.info("Reading file: {}", path);
|
log.info("Reading file: {}", path);
|
||||||
if (Files.exists(path)) {
|
if (Files.exists(path)) {
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,6 @@ import java.io.InputStream;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -66,7 +65,7 @@ public class ReactRoutingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for external index.html first (customFiles/static/)
|
// Check for external index.html first (customFiles/static/)
|
||||||
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
|
Path externalIndexPath = Path.of(InstallationPathConfig.getStaticPath(), "index.html");
|
||||||
log.debug("Checking for custom index.html at: {}", externalIndexPath);
|
log.debug("Checking for custom index.html at: {}", externalIndexPath);
|
||||||
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
|
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
|
||||||
log.info("Using custom index.html from: {}", externalIndexPath);
|
log.info("Using custom index.html from: {}", externalIndexPath);
|
||||||
@@ -136,7 +135,7 @@ public class ReactRoutingController {
|
|||||||
|
|
||||||
private Resource getIndexHtmlResource() {
|
private Resource getIndexHtmlResource() {
|
||||||
// Check external location first
|
// Check external location first
|
||||||
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
|
Path externalIndexPath = Path.of(InstallationPathConfig.getStaticPath(), "index.html");
|
||||||
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
|
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
|
||||||
return new FileSystemResource(externalIndexPath.toFile());
|
return new FileSystemResource(externalIndexPath.toFile());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ public class PdfJsonConversionService {
|
|||||||
.replaceAll("");
|
.replaceAll("");
|
||||||
return String.format("%s (%s)", cleanName, subtype);
|
return String.format("%s (%s)", cleanName, subtype);
|
||||||
})
|
})
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.toList();
|
||||||
long type3Fonts =
|
long type3Fonts =
|
||||||
responseFonts.stream().filter(f -> "Type3".equals(f.getSubtype())).count();
|
responseFonts.stream().filter(f -> "Type3".equals(f.getSubtype())).count();
|
||||||
|
|
||||||
@@ -1554,7 +1554,7 @@ public class PdfJsonConversionService {
|
|||||||
.glyphName(outline.getGlyphName())
|
.glyphName(outline.getGlyphName())
|
||||||
.unicode(outline.getUnicode())
|
.unicode(outline.getUnicode())
|
||||||
.build())
|
.build())
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@@ -41,8 +40,8 @@ public class SharedSignatureService {
|
|||||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||||
validateFileName(fileName);
|
validateFileName(fileName);
|
||||||
// Check if file exists in user's personal folder or ALL_USERS folder
|
// Check if file exists in user's personal folder or ALL_USERS folder
|
||||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
Path userPath = Path.of(SIGNATURE_BASE_PATH, username, fileName);
|
||||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
Path allUsersPath = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||||
|
|
||||||
return Files.exists(userPath) || Files.exists(allUsersPath);
|
return Files.exists(userPath) || Files.exists(allUsersPath);
|
||||||
}
|
}
|
||||||
@@ -52,7 +51,7 @@ public class SharedSignatureService {
|
|||||||
|
|
||||||
// Get signatures from user's personal folder
|
// Get signatures from user's personal folder
|
||||||
if (StringUtils.hasText(username)) {
|
if (StringUtils.hasText(username)) {
|
||||||
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path userFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
if (Files.exists(userFolder)) {
|
if (Files.exists(userFolder)) {
|
||||||
try {
|
try {
|
||||||
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
|
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
|
||||||
@@ -63,7 +62,7 @@ public class SharedSignatureService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get signatures from ALL_USERS folder
|
// Get signatures from ALL_USERS folder
|
||||||
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
Path allUsersFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||||
if (Files.exists(allUsersFolder)) {
|
if (Files.exists(allUsersFolder)) {
|
||||||
try {
|
try {
|
||||||
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
|
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
|
||||||
@@ -90,7 +89,7 @@ public class SharedSignatureService {
|
|||||||
*/
|
*/
|
||||||
public byte[] getSharedSignatureBytes(String fileName) throws IOException {
|
public byte[] getSharedSignatureBytes(String fileName) throws IOException {
|
||||||
validateFileName(fileName);
|
validateFileName(fileName);
|
||||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
Path allUsersPath = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||||
if (!Files.exists(allUsersPath)) {
|
if (!Files.exists(allUsersPath)) {
|
||||||
throw new FileNotFoundException("Shared signature file not found");
|
throw new FileNotFoundException("Shared signature file not found");
|
||||||
}
|
}
|
||||||
@@ -142,7 +141,7 @@ public class SharedSignatureService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
|
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
|
||||||
Path targetFolder = Paths.get(SIGNATURE_BASE_PATH, folderName);
|
Path targetFolder = Path.of(SIGNATURE_BASE_PATH, folderName);
|
||||||
Files.createDirectories(targetFolder);
|
Files.createDirectories(targetFolder);
|
||||||
|
|
||||||
long timestamp = System.currentTimeMillis();
|
long timestamp = System.currentTimeMillis();
|
||||||
@@ -193,7 +192,7 @@ public class SharedSignatureService {
|
|||||||
List<SavedSignatureResponse> signatures = new ArrayList<>();
|
List<SavedSignatureResponse> signatures = new ArrayList<>();
|
||||||
|
|
||||||
// Load personal signatures
|
// Load personal signatures
|
||||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
if (Files.exists(personalFolder)) {
|
if (Files.exists(personalFolder)) {
|
||||||
try (Stream<Path> stream = Files.list(personalFolder)) {
|
try (Stream<Path> stream = Files.list(personalFolder)) {
|
||||||
stream.filter(this::isImageFile)
|
stream.filter(this::isImageFile)
|
||||||
@@ -224,7 +223,7 @@ public class SharedSignatureService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load shared signatures
|
// Load shared signatures
|
||||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||||
if (Files.exists(sharedFolder)) {
|
if (Files.exists(sharedFolder)) {
|
||||||
try (Stream<Path> stream = Files.list(sharedFolder)) {
|
try (Stream<Path> stream = Files.list(sharedFolder)) {
|
||||||
stream.filter(this::isImageFile)
|
stream.filter(this::isImageFile)
|
||||||
@@ -262,7 +261,7 @@ public class SharedSignatureService {
|
|||||||
validateFileName(signatureId);
|
validateFileName(signatureId);
|
||||||
|
|
||||||
// Try to find and delete image file in personal folder
|
// Try to find and delete image file in personal folder
|
||||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
boolean deleted = false;
|
boolean deleted = false;
|
||||||
|
|
||||||
if (Files.exists(personalFolder)) {
|
if (Files.exists(personalFolder)) {
|
||||||
@@ -283,7 +282,7 @@ public class SharedSignatureService {
|
|||||||
|
|
||||||
// Try shared folder if not found in personal
|
// Try shared folder if not found in personal
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||||
if (Files.exists(sharedFolder)) {
|
if (Files.exists(sharedFolder)) {
|
||||||
try (Stream<Path> stream = Files.list(sharedFolder)) {
|
try (Stream<Path> stream = Files.list(sharedFolder)) {
|
||||||
List<Path> matchingFiles =
|
List<Path> matchingFiles =
|
||||||
|
|||||||
+1
-2
@@ -10,7 +10,6 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
||||||
@@ -268,7 +267,7 @@ public class Type3FontLibrary {
|
|||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.map(String::trim)
|
.map(String::trim)
|
||||||
.filter(s -> !s.isEmpty())
|
.filter(s -> !s.isEmpty())
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeAlias(String alias) {
|
private String normalizeAlias(String alias) {
|
||||||
|
|||||||
+2
-3
@@ -3,7 +3,6 @@ package stirling.software.SPDF.service.pdfjson.type3.tool;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -285,9 +284,9 @@ public final class Type3SignatureTool {
|
|||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
String arg = args[i];
|
String arg = args[i];
|
||||||
if ("--pdf".equals(arg) && i + 1 < args.length) {
|
if ("--pdf".equals(arg) && i + 1 < args.length) {
|
||||||
pdf = Paths.get(args[++i]);
|
pdf = Path.of(args[++i]);
|
||||||
} else if ("--output".equals(arg) && i + 1 < args.length) {
|
} else if ("--output".equals(arg) && i + 1 < args.length) {
|
||||||
output = Paths.get(args[++i]);
|
output = Path.of(args[++i]);
|
||||||
} else if ("--pretty".equals(arg)) {
|
} else if ("--pretty".equals(arg)) {
|
||||||
pretty = true;
|
pretty = true;
|
||||||
} else if ("--help".equals(arg) || "-h".equals(arg)) {
|
} else if ("--help".equals(arg) || "-h".equals(arg)) {
|
||||||
|
|||||||
+2
-3
@@ -8,7 +8,6 @@ import java.net.URISyntaxException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -411,7 +410,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
|
|||||||
|
|
||||||
private Path getInboxFolder(Long chatId) throws IOException {
|
private Path getInboxFolder(Long chatId) throws IOException {
|
||||||
Path baseInbox =
|
Path baseInbox =
|
||||||
Paths.get(
|
Path.of(
|
||||||
runtimePathConfig.getPipelineWatchedFoldersPath(),
|
runtimePathConfig.getPipelineWatchedFoldersPath(),
|
||||||
telegramProperties.getPipelineInboxFolder());
|
telegramProperties.getPipelineInboxFolder());
|
||||||
|
|
||||||
@@ -445,7 +444,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
|
|||||||
|
|
||||||
private List<Path> waitForPipelineOutputs(PipelineFileInfo info) throws IOException {
|
private List<Path> waitForPipelineOutputs(PipelineFileInfo info) throws IOException {
|
||||||
|
|
||||||
Path finishedDir = Paths.get(runtimePathConfig.getPipelineFinishedFoldersPath());
|
Path finishedDir = Path.of(runtimePathConfig.getPipelineFinishedFoldersPath());
|
||||||
Files.createDirectories(finishedDir);
|
Files.createDirectories(finishedDir);
|
||||||
|
|
||||||
Instant start = info.savedAt();
|
Instant start = info.savedAt();
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@ package stirling.software.SPDF.controller.api.misc;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -34,9 +34,9 @@ class PrintFileControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
void printFile_absolutePath_throwsException() {
|
void printFile_absolutePath_throwsException() {
|
||||||
PrintFileRequest request = new PrintFileRequest();
|
PrintFileRequest request = new PrintFileRequest();
|
||||||
String absPath = Paths.get("/etc/passwd").toString();
|
String absPath = Path.of("/etc/passwd").toString();
|
||||||
// Only test on systems where /etc/passwd is absolute
|
// Only test on systems where /etc/passwd is absolute
|
||||||
if (Paths.get(absPath).isAbsolute()) {
|
if (Path.of(absPath).isAbsolute()) {
|
||||||
MockMultipartFile file =
|
MockMultipartFile file =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"fileInput", absPath, "application/pdf", "data".getBytes());
|
"fileInput", absPath, "application/pdf", "data".getBytes());
|
||||||
|
|||||||
+2
-4
@@ -207,16 +207,14 @@ public class AuditDashboardController {
|
|||||||
|
|
||||||
// Include standard enum types in case they're not in the database yet
|
// Include standard enum types in case they're not in the database yet
|
||||||
List<String> enumTypes =
|
List<String> enumTypes =
|
||||||
Arrays.stream(AuditEventType.values())
|
Arrays.stream(AuditEventType.values()).map(AuditEventType::name).toList();
|
||||||
.map(AuditEventType::name)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// Combine both sources, remove duplicates, and sort
|
// Combine both sources, remove duplicates, and sort
|
||||||
Set<String> combinedTypes = new HashSet<>();
|
Set<String> combinedTypes = new HashSet<>();
|
||||||
combinedTypes.addAll(dbTypes);
|
combinedTypes.addAll(dbTypes);
|
||||||
combinedTypes.addAll(enumTypes);
|
combinedTypes.addAll(enumTypes);
|
||||||
|
|
||||||
return combinedTypes.stream().sorted().collect(Collectors.toList());
|
return combinedTypes.stream().sorted().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Export audit data as CSV. */
|
/** Export audit data as CSV. */
|
||||||
|
|||||||
+7
-22
@@ -118,7 +118,7 @@ public class AuditRestController {
|
|||||||
|
|
||||||
// Convert to response format expected by frontend
|
// Convert to response format expected by frontend
|
||||||
List<AuditEventDto> eventDtos =
|
List<AuditEventDto> eventDtos =
|
||||||
events.getContent().stream().map(this::convertToDto).collect(Collectors.toList());
|
events.getContent().stream().map(this::convertToDto).toList();
|
||||||
|
|
||||||
AuditEventsResponse response =
|
AuditEventsResponse response =
|
||||||
AuditEventsResponse.builder()
|
AuditEventsResponse.builder()
|
||||||
@@ -191,19 +191,13 @@ public class AuditRestController {
|
|||||||
ChartData eventsByTypeChart =
|
ChartData eventsByTypeChart =
|
||||||
ChartData.builder()
|
ChartData.builder()
|
||||||
.labels(new ArrayList<>(eventsByType.keySet()))
|
.labels(new ArrayList<>(eventsByType.keySet()))
|
||||||
.values(
|
.values(eventsByType.values().stream().map(Long::intValue).toList())
|
||||||
eventsByType.values().stream()
|
|
||||||
.map(Long::intValue)
|
|
||||||
.collect(Collectors.toList()))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
ChartData eventsByUserChart =
|
ChartData eventsByUserChart =
|
||||||
ChartData.builder()
|
ChartData.builder()
|
||||||
.labels(new ArrayList<>(eventsByUser.keySet()))
|
.labels(new ArrayList<>(eventsByUser.keySet()))
|
||||||
.values(
|
.values(eventsByUser.values().stream().map(Long::intValue).toList())
|
||||||
eventsByUser.values().stream()
|
|
||||||
.map(Long::intValue)
|
|
||||||
.collect(Collectors.toList()))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Sort events by day for time series
|
// Sort events by day for time series
|
||||||
@@ -211,10 +205,7 @@ public class AuditRestController {
|
|||||||
ChartData eventsOverTimeChart =
|
ChartData eventsOverTimeChart =
|
||||||
ChartData.builder()
|
ChartData.builder()
|
||||||
.labels(new ArrayList<>(sortedEventsByDay.keySet()))
|
.labels(new ArrayList<>(sortedEventsByDay.keySet()))
|
||||||
.values(
|
.values(sortedEventsByDay.values().stream().map(Long::intValue).toList())
|
||||||
sortedEventsByDay.values().stream()
|
|
||||||
.map(Long::intValue)
|
|
||||||
.collect(Collectors.toList()))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
AuditChartsData chartsData =
|
AuditChartsData chartsData =
|
||||||
@@ -239,16 +230,14 @@ public class AuditRestController {
|
|||||||
|
|
||||||
// Include standard enum types in case they're not in the database yet
|
// Include standard enum types in case they're not in the database yet
|
||||||
List<String> enumTypes =
|
List<String> enumTypes =
|
||||||
Arrays.stream(AuditEventType.values())
|
Arrays.stream(AuditEventType.values()).map(AuditEventType::name).toList();
|
||||||
.map(AuditEventType::name)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// Combine both sources, remove duplicates, and sort
|
// Combine both sources, remove duplicates, and sort
|
||||||
Set<String> combinedTypes = new HashSet<>();
|
Set<String> combinedTypes = new HashSet<>();
|
||||||
combinedTypes.addAll(dbTypes);
|
combinedTypes.addAll(dbTypes);
|
||||||
combinedTypes.addAll(enumTypes);
|
combinedTypes.addAll(enumTypes);
|
||||||
|
|
||||||
List<String> result = combinedTypes.stream().sorted().collect(Collectors.toList());
|
List<String> result = combinedTypes.stream().sorted().toList();
|
||||||
|
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
@@ -263,11 +252,7 @@ public class AuditRestController {
|
|||||||
// Use the countByPrincipal query to get unique principals
|
// Use the countByPrincipal query to get unique principals
|
||||||
List<Object[]> principalCounts = auditRepository.countByPrincipal();
|
List<Object[]> principalCounts = auditRepository.countByPrincipal();
|
||||||
|
|
||||||
List<String> users =
|
List<String> users = principalCounts.stream().map(arr -> (String) arr[0]).sorted().toList();
|
||||||
principalCounts.stream()
|
|
||||||
.map(arr -> (String) arr[0])
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return ResponseEntity.ok(users);
|
return ResponseEntity.ok(users);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.controller.api;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
@@ -183,7 +182,7 @@ public class SignatureController {
|
|||||||
*/
|
*/
|
||||||
private boolean deleteFromSharedFolder(String signatureId) throws IOException {
|
private boolean deleteFromSharedFolder(String signatureId) throws IOException {
|
||||||
String signatureBasePath = InstallationPathConfig.getSignaturesPath();
|
String signatureBasePath = InstallationPathConfig.getSignaturesPath();
|
||||||
Path sharedFolder = Paths.get(signatureBasePath, ALL_USERS_FOLDER);
|
Path sharedFolder = Path.of(signatureBasePath, ALL_USERS_FOLDER);
|
||||||
boolean deleted = false;
|
boolean deleted = false;
|
||||||
|
|
||||||
if (Files.exists(sharedFolder)) {
|
if (Files.exists(sharedFolder)) {
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.controller.api;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -85,7 +84,7 @@ public class UsageRestController {
|
|||||||
.build();
|
.build();
|
||||||
})
|
})
|
||||||
.sorted(Comparator.comparingInt(EndpointStatistic::getVisits).reversed())
|
.sorted(Comparator.comparingInt(EndpointStatistic::getVisits).reversed())
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
// Apply limit if specified
|
// Apply limit if specified
|
||||||
if (limit != null && limit > 0 && statistics.size() > limit) {
|
if (limit != null && limit > 0 && statistics.size() > limit) {
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.configuration.ee;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -104,7 +103,7 @@ public class LicenseKeyChecker {
|
|||||||
if (keyOrFilePath.startsWith(FILE_PREFIX)) {
|
if (keyOrFilePath.startsWith(FILE_PREFIX)) {
|
||||||
String filePath = keyOrFilePath.substring(FILE_PREFIX.length());
|
String filePath = keyOrFilePath.substring(FILE_PREFIX.length());
|
||||||
try {
|
try {
|
||||||
Path path = Paths.get(filePath);
|
Path path = Path.of(filePath);
|
||||||
if (!Files.exists(path)) {
|
if (!Files.exists(path)) {
|
||||||
log.error("License file does not exist: {}", filePath);
|
log.error("License file does not exist: {}", filePath);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+1
-2
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -329,7 +328,7 @@ public class AdminLicenseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get config directory and target path
|
// Get config directory and target path
|
||||||
Path configPath = Paths.get(InstallationPathConfig.getConfigPath());
|
Path configPath = Path.of(InstallationPathConfig.getConfigPath());
|
||||||
Path configPathAbs = configPath.toAbsolutePath().normalize();
|
Path configPathAbs = configPath.toAbsolutePath().normalize();
|
||||||
Path targetPath = configPathAbs.resolve(filename).normalize();
|
Path targetPath = configPathAbs.resolve(filename).normalize();
|
||||||
log.info(
|
log.info(
|
||||||
|
|||||||
+3
-3
@@ -702,7 +702,7 @@ public class AdminSettingsController {
|
|||||||
if (path != null && !path.trim().isEmpty()) {
|
if (path != null && !path.trim().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
java.nio.file.Path normalized =
|
java.nio.file.Path normalized =
|
||||||
java.nio.file.Paths.get(path.trim()).toAbsolutePath().normalize();
|
java.nio.file.Path.of(path.trim()).toAbsolutePath().normalize();
|
||||||
String normalizedStr = normalized.toString();
|
String normalizedStr = normalized.toString();
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
@@ -719,9 +719,9 @@ public class AdminSettingsController {
|
|||||||
// Check for overlapping paths
|
// Check for overlapping paths
|
||||||
java.util.List<String> pathList = new java.util.ArrayList<>(normalizedPaths);
|
java.util.List<String> pathList = new java.util.ArrayList<>(normalizedPaths);
|
||||||
for (int i = 0; i < pathList.size(); i++) {
|
for (int i = 0; i < pathList.size(); i++) {
|
||||||
java.nio.file.Path path1 = java.nio.file.Paths.get(pathList.get(i));
|
java.nio.file.Path path1 = java.nio.file.Path.of(pathList.get(i));
|
||||||
for (int j = i + 1; j < pathList.size(); j++) {
|
for (int j = i + 1; j < pathList.size(); j++) {
|
||||||
java.nio.file.Path path2 = java.nio.file.Paths.get(pathList.get(j));
|
java.nio.file.Path path2 = java.nio.file.Path.of(pathList.get(j));
|
||||||
if (path1.startsWith(path2) || path2.startsWith(path1)) {
|
if (path1.startsWith(path2) || path2.startsWith(path1)) {
|
||||||
return "Overlapping paths detected: " + path1 + " and " + path2;
|
return "Overlapping paths detected: " + path1 + " and " + path2;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.controller.api;
|
|||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -280,7 +279,7 @@ public class InviteLinkController {
|
|||||||
"expiresAt", invite.getExpiresAt().toString());
|
"expiresAt", invite.getExpiresAt().toString());
|
||||||
return inviteMap;
|
return inviteMap;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("invites", inviteList));
|
return ResponseEntity.ok(Map.of("invites", inviteList));
|
||||||
|
|
||||||
@@ -331,7 +330,7 @@ public class InviteLinkController {
|
|||||||
List<InviteToken> expiredInvites =
|
List<InviteToken> expiredInvites =
|
||||||
inviteTokenRepository.findAll().stream()
|
inviteTokenRepository.findAll().stream()
|
||||||
.filter(invite -> !invite.isValid())
|
.filter(invite -> !invite.isValid())
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
int count = expiredInvites.size();
|
int count = expiredInvites.size();
|
||||||
inviteTokenRepository.deleteAll(expiredInvites);
|
inviteTokenRepository.deleteAll(expiredInvites);
|
||||||
|
|||||||
+2
-3
@@ -6,7 +6,6 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -53,7 +52,7 @@ public class UIDataTessdataController {
|
|||||||
TessdataLanguagesResponse response = new TessdataLanguagesResponse();
|
TessdataLanguagesResponse response = new TessdataLanguagesResponse();
|
||||||
response.setInstalled(getAvailableTesseractLanguages());
|
response.setInstalled(getAvailableTesseractLanguages());
|
||||||
response.setAvailable(getRemoteTessdataLanguages());
|
response.setAvailable(getRemoteTessdataLanguages());
|
||||||
response.setWritable(isWritableDirectory(Paths.get(runtimePathConfig.getTessDataPath())));
|
response.setWritable(isWritableDirectory(Path.of(runtimePathConfig.getTessDataPath())));
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ public class UIDataTessdataController {
|
|||||||
.body(Map.of("message", "No languages provided for download"));
|
.body(Map.of("message", "No languages provided for download"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Path tessdataDir = Paths.get(runtimePathConfig.getTessDataPath());
|
Path tessdataDir = Path.of(runtimePathConfig.getTessDataPath());
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(tessdataDir);
|
Files.createDirectories(tessdataDir);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
+1
-1
@@ -1009,7 +1009,7 @@ public class UserController {
|
|||||||
source.stream()
|
source.stream()
|
||||||
.filter(User::isEnabled)
|
.filter(User::isEnabled)
|
||||||
.map(this::toUserSummaryDTO)
|
.map(this::toUserSummaryDTO)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
return ResponseEntity.ok(users);
|
return ResponseEntity.ok(users);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-11
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@@ -23,7 +22,6 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
ApplicationProperties.Datasource datasourceProps,
|
ApplicationProperties.Datasource datasourceProps,
|
||||||
DataSource dataSource,
|
DataSource dataSource,
|
||||||
DatabaseNotificationServiceInterface backupNotificationService) {
|
DatabaseNotificationServiceInterface backupNotificationService) {
|
||||||
this.BACKUP_DIR = Paths.get(InstallationPathConfig.getBackupPath()).normalize();
|
this.BACKUP_DIR = Path.of(InstallationPathConfig.getBackupPath()).normalize();
|
||||||
this.datasourceProps = datasourceProps;
|
this.datasourceProps = datasourceProps;
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.backupNotificationService = backupNotificationService;
|
this.backupNotificationService = backupNotificationService;
|
||||||
@@ -111,7 +109,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
@Deprecated(since = "2.0.0", forRemoval = true)
|
@Deprecated(since = "2.0.0", forRemoval = true)
|
||||||
private void moveBackupFiles() {
|
private void moveBackupFiles() {
|
||||||
Path sourceDir =
|
Path sourceDir =
|
||||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
Path.of(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
||||||
|
|
||||||
if (!Files.exists(sourceDir)) {
|
if (!Files.exists(sourceDir)) {
|
||||||
log.info("Source directory does not exist: {}", sourceDir);
|
log.info("Source directory does not exist: {}", sourceDir);
|
||||||
@@ -217,7 +215,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
List<FileInfo> backupList = this.getBackupList();
|
List<FileInfo> backupList = this.getBackupList();
|
||||||
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
||||||
|
|
||||||
Path latestExport = Paths.get(backupList.get(0).getFilePath());
|
Path latestExport = Path.of(backupList.get(0).getFilePath());
|
||||||
|
|
||||||
executeDatabaseScript(latestExport);
|
executeDatabaseScript(latestExport);
|
||||||
}
|
}
|
||||||
@@ -258,9 +256,13 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
@Override
|
@Override
|
||||||
public void exportDatabase() {
|
public void exportDatabase() {
|
||||||
List<FileInfo> filteredBackupList =
|
List<FileInfo> filteredBackupList =
|
||||||
this.getBackupList().stream()
|
new ArrayList<>(
|
||||||
.filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
|
this.getBackupList().stream()
|
||||||
.collect(Collectors.toList());
|
.filter(
|
||||||
|
backup ->
|
||||||
|
!backup.getFileName()
|
||||||
|
.startsWith(BACKUP_PREFIX + "user_"))
|
||||||
|
.toList());
|
||||||
|
|
||||||
if (filteredBackupList.size() > 5) {
|
if (filteredBackupList.size() > 5) {
|
||||||
deleteOldestBackup(filteredBackupList);
|
deleteOldestBackup(filteredBackupList);
|
||||||
@@ -341,7 +343,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
|
|
||||||
for (FileInfo backup : backupList) {
|
for (FileInfo backup : backupList) {
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(backup.getFilePath()));
|
Files.deleteIfExists(Path.of(backup.getFilePath()));
|
||||||
deletedFiles.add(Pair.of(backup, true));
|
deletedFiles.add(Pair.of(backup, true));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error deleting backup file: {}", backup.getFileName(), e);
|
log.error("Error deleting backup file: {}", backup.getFileName(), e);
|
||||||
@@ -359,7 +361,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
if (!backupList.isEmpty()) {
|
if (!backupList.isEmpty()) {
|
||||||
FileInfo lastBackup = backupList.get(backupList.size() - 1);
|
FileInfo lastBackup = backupList.get(backupList.size() - 1);
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(lastBackup.getFilePath()));
|
Files.deleteIfExists(Path.of(lastBackup.getFilePath()));
|
||||||
deletedFiles.add(Pair.of(lastBackup, true));
|
deletedFiles.add(Pair.of(lastBackup, true));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error deleting last backup file: {}", lastBackup.getFileName(), e);
|
log.error("Error deleting last backup file: {}", lastBackup.getFileName(), e);
|
||||||
@@ -381,7 +383,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
|||||||
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
||||||
|
|
||||||
FileInfo oldestFile = filteredBackupList.get(0);
|
FileInfo oldestFile = filteredBackupList.get(0);
|
||||||
Files.deleteIfExists(Paths.get(oldestFile.getFilePath()));
|
Files.deleteIfExists(Path.of(oldestFile.getFilePath()));
|
||||||
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
|
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
|
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
|
||||||
|
|||||||
+1
-2
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.service;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -76,7 +75,7 @@ public class KeyPairCleanupService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path privateKeyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
|
Path privateKeyDirectory = Path.of(InstallationPathConfig.getPrivateKeyPath());
|
||||||
Path keyFile = privateKeyDirectory.resolve(keyId + KeyPersistenceService.KEY_SUFFIX);
|
Path keyFile = privateKeyDirectory.resolve(keyId + KeyPersistenceService.KEY_SUFFIX);
|
||||||
|
|
||||||
if (Files.exists(keyFile)) {
|
if (Files.exists(keyFile)) {
|
||||||
|
|||||||
+10
-16
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.service;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
@@ -20,7 +19,6 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cache.Cache;
|
import org.springframework.cache.Cache;
|
||||||
@@ -82,7 +80,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
*/
|
*/
|
||||||
private void loadExistingKeysFromDisk() {
|
private void loadExistingKeysFromDisk() {
|
||||||
try {
|
try {
|
||||||
Path keyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
|
Path keyDirectory = Path.of(InstallationPathConfig.getPrivateKeyPath());
|
||||||
|
|
||||||
if (!Files.exists(keyDirectory)) {
|
if (!Files.exists(keyDirectory)) {
|
||||||
log.info("No existing keys found, generating new keypair");
|
log.info("No existing keys found, generating new keypair");
|
||||||
@@ -99,7 +97,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
b.getFileName().compareTo(a.getFileName())) // Most
|
b.getFileName().compareTo(a.getFileName())) // Most
|
||||||
// recent
|
// recent
|
||||||
// first
|
// first
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyFiles.isEmpty()) {
|
if (keyFiles.isEmpty()) {
|
||||||
@@ -275,7 +273,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
eligible);
|
eligible);
|
||||||
return eligible;
|
return eligible;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateKeyId() {
|
private String generateKeyId() {
|
||||||
@@ -284,20 +282,17 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private KeyPair generateRSAKeypair() {
|
private KeyPair generateRSAKeypair() {
|
||||||
KeyPairGenerator keyPairGenerator = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
|
return keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
log.error("Failed to initialize RSA key pair generator", e);
|
throw new IllegalStateException("RSA key pair generator is not available", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyPairGenerator.generateKeyPair();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensurePrivateKeyDirectoryExists() throws IOException {
|
private void ensurePrivateKeyDirectoryExists() throws IOException {
|
||||||
Path keyPath = Paths.get(InstallationPathConfig.getPrivateKeyPath());
|
Path keyPath = Path.of(InstallationPathConfig.getPrivateKeyPath());
|
||||||
|
|
||||||
if (!Files.exists(keyPath)) {
|
if (!Files.exists(keyPath)) {
|
||||||
Files.createDirectories(keyPath);
|
Files.createDirectories(keyPath);
|
||||||
@@ -312,7 +307,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
* <p>Public key stored as: keyId.pub
|
* <p>Public key stored as: keyId.pub
|
||||||
*/
|
*/
|
||||||
private void storeKeyPair(String keyId, KeyPair keyPair) throws IOException {
|
private void storeKeyPair(String keyId, KeyPair keyPair) throws IOException {
|
||||||
Path keyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
|
Path keyDirectory = Path.of(InstallationPathConfig.getPrivateKeyPath());
|
||||||
|
|
||||||
// Store private key
|
// Store private key
|
||||||
Path privateKeyFile = keyDirectory.resolve(keyId + KEY_SUFFIX);
|
Path privateKeyFile = keyDirectory.resolve(keyId + KEY_SUFFIX);
|
||||||
@@ -346,7 +341,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
private PrivateKey loadPrivateKey(String keyId)
|
private PrivateKey loadPrivateKey(String keyId)
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
Path keyFile =
|
Path keyFile =
|
||||||
Paths.get(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + KEY_SUFFIX);
|
Path.of(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + KEY_SUFFIX);
|
||||||
|
|
||||||
if (!Files.exists(keyFile)) {
|
if (!Files.exists(keyFile)) {
|
||||||
throw new IOException("Private key not found: " + keyFile);
|
throw new IOException("Private key not found: " + keyFile);
|
||||||
@@ -369,8 +364,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
|
|||||||
*/
|
*/
|
||||||
private String loadPublicKey(String keyId) throws IOException {
|
private String loadPublicKey(String keyId) throws IOException {
|
||||||
Path publicKeyFile =
|
Path publicKeyFile =
|
||||||
Paths.get(InstallationPathConfig.getPrivateKeyPath())
|
Path.of(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + PUB_KEY_SUFFIX);
|
||||||
.resolve(keyId + PUB_KEY_SUFFIX);
|
|
||||||
|
|
||||||
if (!Files.exists(publicKeyFile)) {
|
if (!Files.exists(publicKeyFile)) {
|
||||||
throw new IOException("Public key not found: " + publicKeyFile);
|
throw new IOException("Public key not found: " + publicKeyFile);
|
||||||
|
|||||||
+1
-2
@@ -5,7 +5,6 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -101,7 +100,7 @@ public class LoginAttemptService {
|
|||||||
return attemptsCache.entrySet().stream()
|
return attemptsCache.entrySet().stream()
|
||||||
.filter(entry -> entry.getValue().getAttemptCount() >= MAX_ATTEMPT)
|
.filter(entry -> entry.getValue().getAttemptCount() >= MAX_ATTEMPT)
|
||||||
.map(Map.Entry::getKey)
|
.map(Map.Entry::getKey)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRemainingAttempts(String key) {
|
public int getRemainingAttempts(String key) {
|
||||||
|
|||||||
+8
-21
@@ -31,21 +31,14 @@ public class RefreshRateLimitService {
|
|||||||
this.jwtProperties = applicationProperties.getSecurity().getJwt();
|
this.jwtProperties = applicationProperties.getSecurity().getJwt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class RefreshAttempt {
|
private record RefreshAttempt(AtomicInteger count, Instant firstAttempt) {
|
||||||
private final AtomicInteger count = new AtomicInteger(0);
|
RefreshAttempt() {
|
||||||
private final Instant firstAttempt = Instant.now();
|
this(new AtomicInteger(0), Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
int incrementAndGet() {
|
int incrementAndGet() {
|
||||||
return count.incrementAndGet();
|
return count.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
Instant getFirstAttempt() {
|
|
||||||
return firstAttempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getCount() {
|
|
||||||
return count.get();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Map<String, RefreshAttempt> attempts = new ConcurrentHashMap<>();
|
private final Map<String, RefreshAttempt> attempts = new ConcurrentHashMap<>();
|
||||||
@@ -72,7 +65,7 @@ public class RefreshRateLimitService {
|
|||||||
|
|
||||||
// Clean up if outside grace window
|
// Clean up if outside grace window
|
||||||
Instant cutoff = Instant.now().minusMillis(graceWindowMillis);
|
Instant cutoff = Instant.now().minusMillis(graceWindowMillis);
|
||||||
if (attempt.getFirstAttempt().isBefore(cutoff)) {
|
if (attempt.firstAttempt().isBefore(cutoff)) {
|
||||||
attempts.remove(tokenHash);
|
attempts.remove(tokenHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,15 +91,9 @@ public class RefreshRateLimitService {
|
|||||||
? configuredMinutes
|
? configuredMinutes
|
||||||
: JwtConstants.DEFAULT_REFRESH_GRACE_MINUTES;
|
: JwtConstants.DEFAULT_REFRESH_GRACE_MINUTES;
|
||||||
Instant cutoff = Instant.now().minusMillis(graceMinutes * 60000L);
|
Instant cutoff = Instant.now().minusMillis(graceMinutes * 60000L);
|
||||||
int removed =
|
int beforeSize = attempts.size();
|
||||||
attempts.entrySet().stream()
|
attempts.entrySet().removeIf(entry -> entry.getValue().firstAttempt().isBefore(cutoff));
|
||||||
.filter(entry -> entry.getValue().getFirstAttempt().isBefore(cutoff))
|
int removed = beforeSize - attempts.size();
|
||||||
.mapToInt(
|
|
||||||
entry -> {
|
|
||||||
attempts.remove(entry.getKey());
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
if (removed > 0) {
|
if (removed > 0) {
|
||||||
log.debug("Cleaned up {} expired refresh tracking entries", removed);
|
log.debug("Cleaned up {} expired refresh tracking entries", removed);
|
||||||
|
|||||||
+1
-2
@@ -11,7 +11,6 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -413,7 +412,7 @@ public class AuditService {
|
|||||||
|
|
||||||
return m;
|
return m;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
data.put("files", fileInfos);
|
data.put("files", fileInfos);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -4,7 +4,6 @@ import java.io.*;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -64,7 +63,7 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Path getKeystorePath() {
|
private Path getKeystorePath() {
|
||||||
return Paths.get(InstallationPathConfig.getConfigPath(), KEYSTORE_FILENAME);
|
return Path.of(InstallationPathConfig.getConfigPath(), KEYSTORE_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasProOrEnterpriseAccess() {
|
private boolean hasProOrEnterpriseAccess() {
|
||||||
|
|||||||
+9
-10
@@ -5,7 +5,6 @@ import java.io.IOException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@@ -55,7 +54,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
@Override
|
@Override
|
||||||
public byte[] getPersonalSignatureBytes(String username, String fileName) throws IOException {
|
public byte[] getPersonalSignatureBytes(String username, String fileName) throws IOException {
|
||||||
validateFileName(fileName);
|
validateFileName(fileName);
|
||||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
Path userPath = Path.of(SIGNATURE_BASE_PATH, username, fileName);
|
||||||
|
|
||||||
if (!Files.exists(userPath)) {
|
if (!Files.exists(userPath)) {
|
||||||
throw new FileNotFoundException("Personal signature not found");
|
throw new FileNotFoundException("Personal signature not found");
|
||||||
@@ -76,7 +75,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
|
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
|
||||||
Path targetFolder = Paths.get(SIGNATURE_BASE_PATH, folderName);
|
Path targetFolder = Path.of(SIGNATURE_BASE_PATH, folderName);
|
||||||
|
|
||||||
// Only enforce limits for personal signatures (not shared)
|
// Only enforce limits for personal signatures (not shared)
|
||||||
if ("personal".equals(scope)) {
|
if ("personal".equals(scope)) {
|
||||||
@@ -170,13 +169,13 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
List<SavedSignatureResponse> signatures = new ArrayList<>();
|
List<SavedSignatureResponse> signatures = new ArrayList<>();
|
||||||
|
|
||||||
// Load personal signatures
|
// Load personal signatures
|
||||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
if (Files.exists(personalFolder)) {
|
if (Files.exists(personalFolder)) {
|
||||||
signatures.addAll(loadSignaturesFromFolder(personalFolder, "personal", true));
|
signatures.addAll(loadSignaturesFromFolder(personalFolder, "personal", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load shared signatures
|
// Load shared signatures
|
||||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||||
if (Files.exists(sharedFolder)) {
|
if (Files.exists(sharedFolder)) {
|
||||||
signatures.addAll(loadSignaturesFromFolder(sharedFolder, "shared", false));
|
signatures.addAll(loadSignaturesFromFolder(sharedFolder, "shared", false));
|
||||||
}
|
}
|
||||||
@@ -189,7 +188,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
validateFileName(signatureId);
|
validateFileName(signatureId);
|
||||||
|
|
||||||
// Only allow deletion from personal folder
|
// Only allow deletion from personal folder
|
||||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
boolean deleted = false;
|
boolean deleted = false;
|
||||||
|
|
||||||
if (Files.exists(personalFolder)) {
|
if (Files.exists(personalFolder)) {
|
||||||
@@ -227,7 +226,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
validateFileName(signatureId);
|
validateFileName(signatureId);
|
||||||
|
|
||||||
// Try personal folder first
|
// Try personal folder first
|
||||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
Path metadataPath = personalFolder.resolve(signatureId + ".json");
|
Path metadataPath = personalFolder.resolve(signatureId + ".json");
|
||||||
|
|
||||||
if (Files.exists(metadataPath)) {
|
if (Files.exists(metadataPath)) {
|
||||||
@@ -237,7 +236,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If not found in personal, try shared folder
|
// If not found in personal, try shared folder
|
||||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||||
Path sharedMetadataPath = sharedFolder.resolve(signatureId + ".json");
|
Path sharedMetadataPath = sharedFolder.resolve(signatureId + ".json");
|
||||||
|
|
||||||
if (Files.exists(sharedMetadataPath)) {
|
if (Files.exists(sharedMetadataPath)) {
|
||||||
@@ -251,7 +250,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
|
|
||||||
public boolean isSharedSignature(String signatureId) {
|
public boolean isSharedSignature(String signatureId) {
|
||||||
validateFileName(signatureId);
|
validateFileName(signatureId);
|
||||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||||
return Files.exists(sharedFolder.resolve(signatureId + ".json"));
|
return Files.exists(sharedFolder.resolve(signatureId + ".json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +273,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
// Private helper methods
|
// Private helper methods
|
||||||
|
|
||||||
private void enforceStorageLimits(String username, String dataUrlToAdd) throws IOException {
|
private void enforceStorageLimits(String username, String dataUrlToAdd) throws IOException {
|
||||||
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
Path userFolder = Path.of(SIGNATURE_BASE_PATH, username);
|
||||||
|
|
||||||
if (!Files.exists(userFolder)) {
|
if (!Files.exists(userFolder)) {
|
||||||
return; // First signature, no limits to check
|
return; // First signature, no limits to check
|
||||||
|
|||||||
+2
-3
@@ -3,7 +3,6 @@ package stirling.software.proprietary.storage.config;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -58,8 +57,8 @@ public class StorageProviderConfig {
|
|||||||
}
|
}
|
||||||
basePathValue = InstallationPathConfig.getPath() + "storage";
|
basePathValue = InstallationPathConfig.getPath() + "storage";
|
||||||
}
|
}
|
||||||
Path basePath = Paths.get(basePathValue).toAbsolutePath().normalize();
|
Path basePath = Path.of(basePathValue).toAbsolutePath().normalize();
|
||||||
Path installRoot = Paths.get(InstallationPathConfig.getPath()).toAbsolutePath().normalize();
|
Path installRoot = Path.of(InstallationPathConfig.getPath()).toAbsolutePath().normalize();
|
||||||
if (!basePath.startsWith(installRoot)) {
|
if (!basePath.startsWith(installRoot)) {
|
||||||
// Warn rather than hard-fail: admins may legitimately point storage at an external
|
// Warn rather than hard-fail: admins may legitimately point storage at an external
|
||||||
// volume, but an unexpected path could indicate a misconfiguration or traversal
|
// volume, but an unexpected path could indicate a misconfiguration or traversal
|
||||||
|
|||||||
+1
-2
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -80,7 +79,7 @@ public class LocalStorageProvider implements StorageProvider {
|
|||||||
if (filename == null || filename.isBlank()) {
|
if (filename == null || filename.isBlank()) {
|
||||||
return "file";
|
return "file";
|
||||||
}
|
}
|
||||||
String stripped = Paths.get(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
|
String stripped = Path.of(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
|
||||||
return stripped.isBlank() ? "file" : stripped;
|
return stripped.isBlank() ? "file" : stripped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -150,7 +150,7 @@ public class S3StorageProvider implements StorageProvider, AutoCloseable {
|
|||||||
if (originalFilename == null || originalFilename.isBlank()) {
|
if (originalFilename == null || originalFilename.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Strip CR/LF and other control chars before path parsing (Paths.get throws on them on
|
// Strip CR/LF and other control chars before path parsing (Path.of throws on them on
|
||||||
// Windows, and they defeat header parsers).
|
// Windows, and they defeat header parsers).
|
||||||
String stripped = originalFilename.replaceAll("\\p{Cntrl}", "");
|
String stripped = originalFilename.replaceAll("\\p{Cntrl}", "");
|
||||||
// Use only the basename to avoid leaking directory structure into the header.
|
// Use only the basename to avoid leaking directory structure into the header.
|
||||||
@@ -184,7 +184,7 @@ public class S3StorageProvider implements StorageProvider, AutoCloseable {
|
|||||||
if (filename == null || filename.isBlank()) {
|
if (filename == null || filename.isBlank()) {
|
||||||
return "file";
|
return "file";
|
||||||
}
|
}
|
||||||
String stripped = Paths.get(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
|
String stripped = Path.of(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
|
||||||
return stripped.isBlank() ? "file" : stripped;
|
return stripped.isBlank() ? "file" : stripped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-8
@@ -14,7 +14,6 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -361,7 +360,7 @@ public class FileStorageService {
|
|||||||
return files.stream()
|
return files.stream()
|
||||||
.sorted(Comparator.comparing(StoredFile::getCreatedAt).reversed())
|
.sorted(Comparator.comparing(StoredFile::getCreatedAt).reversed())
|
||||||
.map(file -> buildResponse(file, user, roleByFileId.get(file.getId())))
|
.map(file -> buildResponse(file, user, roleByFileId.get(file.getId())))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoredFileResponse getAccessibleFileResponse(User user, Long fileId) {
|
public StoredFileResponse getAccessibleFileResponse(User user, Long fileId) {
|
||||||
@@ -400,7 +399,7 @@ public class FileStorageService {
|
|||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.map(User::getUsername)
|
.map(User::getUsername)
|
||||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||||
.collect(Collectors.toList())
|
.toList()
|
||||||
: List.of();
|
: List.of();
|
||||||
List<ShareLinkResponse> shareLinks =
|
List<ShareLinkResponse> shareLinks =
|
||||||
ownedByCurrentUser && isShareLinksEnabled()
|
ownedByCurrentUser && isShareLinksEnabled()
|
||||||
@@ -419,7 +418,7 @@ public class FileStorageService {
|
|||||||
.expiresAt(share.getExpiresAt())
|
.expiresAt(share.getExpiresAt())
|
||||||
.build())
|
.build())
|
||||||
.sorted(Comparator.comparing(ShareLinkResponse::getCreatedAt))
|
.sorted(Comparator.comparing(ShareLinkResponse::getCreatedAt))
|
||||||
.collect(Collectors.toList())
|
.toList()
|
||||||
: List.of();
|
: List.of();
|
||||||
List<SharedUserResponse> sharedUsers =
|
List<SharedUserResponse> sharedUsers =
|
||||||
ownedByCurrentUser
|
ownedByCurrentUser
|
||||||
@@ -440,7 +439,7 @@ public class FileStorageService {
|
|||||||
Comparator.comparing(
|
Comparator.comparing(
|
||||||
SharedUserResponse::getUsername,
|
SharedUserResponse::getUsername,
|
||||||
String.CASE_INSENSITIVE_ORDER))
|
String.CASE_INSENSITIVE_ORDER))
|
||||||
.collect(Collectors.toList())
|
.toList()
|
||||||
: List.of();
|
: List.of();
|
||||||
return StoredFileResponse.builder()
|
return StoredFileResponse.builder()
|
||||||
.id(file.getId())
|
.id(file.getId())
|
||||||
@@ -762,7 +761,7 @@ public class FileStorageService {
|
|||||||
.accessType(access.getAccessType().name())
|
.accessType(access.getAccessType().name())
|
||||||
.accessedAt(access.getAccessedAt())
|
.accessedAt(access.getAccessedAt())
|
||||||
.build())
|
.build())
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FileShareAccess> listAccessedShareLinks(User user) {
|
public List<FileShareAccess> listAccessedShareLinks(User user) {
|
||||||
@@ -818,7 +817,7 @@ public class FileStorageService {
|
|||||||
.build();
|
.build();
|
||||||
})
|
})
|
||||||
.filter(response -> response.getShareToken() != null)
|
.filter(response -> response.getShareToken() != null)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ensureSharingEnabled() {
|
public void ensureSharingEnabled() {
|
||||||
@@ -1007,7 +1006,7 @@ public class FileStorageService {
|
|||||||
file.getHistoryStorageKey(),
|
file.getHistoryStorageKey(),
|
||||||
file.getAuditLogStorageKey())
|
file.getAuditLogStorageKey())
|
||||||
.filter(value -> value != null && !value.isBlank())
|
.filter(value -> value != null && !value.isBlank())
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanupStoredObject(StoredObject storedObject) {
|
private void cleanupStoredObject(StoredObject storedObject) {
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ public class SigningSessionController {
|
|||||||
.map(
|
.map(
|
||||||
stirling.software.proprietary.workflow.util.WorkflowMapper
|
stirling.software.proprietary.workflow.util.WorkflowMapper
|
||||||
::toResponse)
|
::toResponse)
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.toList();
|
||||||
return ResponseEntity.ok(responses);
|
return ResponseEntity.ok(responses);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error listing sessions for user {}", principal.getName(), e);
|
log.error("Error listing sessions for user {}", principal.getName(), e);
|
||||||
|
|||||||
+31
-66
@@ -98,7 +98,7 @@ public class SigningFinalizationService {
|
|||||||
|
|
||||||
// Step 1.5: Add summary page BEFORE digital signing (if enabled)
|
// Step 1.5: Add summary page BEFORE digital signing (if enabled)
|
||||||
// CRITICAL: Must be done before signing to avoid invalidating signatures
|
// CRITICAL: Must be done before signing to avoid invalidating signatures
|
||||||
if (Boolean.TRUE.equals(settings.includeSummaryPage)) {
|
if (Boolean.TRUE.equals(settings.includeSummaryPage())) {
|
||||||
log.info(
|
log.info(
|
||||||
"Adding summary page before digital signing for session {}",
|
"Adding summary page before digital signing for session {}",
|
||||||
session.getSessionId());
|
session.getSessionId());
|
||||||
@@ -108,11 +108,13 @@ public class SigningFinalizationService {
|
|||||||
// Suppress digital certificate visual block when summary page is enabled
|
// Suppress digital certificate visual block when summary page is enabled
|
||||||
// (wet signatures already applied in Step 1 and will still appear)
|
// (wet signatures already applied in Step 1 and will still appear)
|
||||||
Boolean showVisualSignature =
|
Boolean showVisualSignature =
|
||||||
Boolean.TRUE.equals(settings.includeSummaryPage) ? false : settings.showSignature;
|
Boolean.TRUE.equals(settings.includeSummaryPage())
|
||||||
|
? false
|
||||||
|
: settings.showSignature();
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"Finalization settings: includeSummaryPage={}, showVisualSignature={}",
|
"Finalization settings: includeSummaryPage={}, showVisualSignature={}",
|
||||||
settings.includeSummaryPage,
|
settings.includeSummaryPage(),
|
||||||
showVisualSignature);
|
showVisualSignature);
|
||||||
|
|
||||||
// Step 2: Apply digital certificates per SIGNED participant
|
// Step 2: Apply digital certificates per SIGNED participant
|
||||||
@@ -150,8 +152,8 @@ public class SigningFinalizationService {
|
|||||||
log.info(
|
log.info(
|
||||||
"Applying signature for {} with reason='{}', location='{}'",
|
"Applying signature for {} with reason='{}', location='{}'",
|
||||||
fresh.getEmail(),
|
fresh.getEmail(),
|
||||||
sigMeta.reason,
|
sigMeta.reason(),
|
||||||
sigMeta.location);
|
sigMeta.location());
|
||||||
|
|
||||||
pdf =
|
pdf =
|
||||||
applyDigitalSignature(
|
applyDigitalSignature(
|
||||||
@@ -159,10 +161,10 @@ public class SigningFinalizationService {
|
|||||||
fresh,
|
fresh,
|
||||||
submission,
|
submission,
|
||||||
showVisualSignature,
|
showVisualSignature,
|
||||||
settings.pageNumber,
|
settings.pageNumber(),
|
||||||
sigMeta.reason,
|
sigMeta.reason(),
|
||||||
sigMeta.location,
|
sigMeta.location(),
|
||||||
settings.showLogo);
|
settings.showLogo());
|
||||||
}
|
}
|
||||||
|
|
||||||
return pdf;
|
return pdf;
|
||||||
@@ -682,7 +684,7 @@ public class SigningFinalizationService {
|
|||||||
textDark,
|
textDark,
|
||||||
textMuted,
|
textMuted,
|
||||||
"Subject:",
|
"Subject:",
|
||||||
certInfo.subjectCN);
|
certInfo.subjectCN());
|
||||||
rRowY -= LINE_H;
|
rRowY -= LINE_H;
|
||||||
drawLabelValue(
|
drawLabelValue(
|
||||||
cs,
|
cs,
|
||||||
@@ -693,7 +695,7 @@ public class SigningFinalizationService {
|
|||||||
textDark,
|
textDark,
|
||||||
textMuted,
|
textMuted,
|
||||||
"Issuer:",
|
"Issuer:",
|
||||||
certInfo.issuerCN);
|
certInfo.issuerCN());
|
||||||
rRowY -= LINE_H;
|
rRowY -= LINE_H;
|
||||||
drawLabelValue(
|
drawLabelValue(
|
||||||
cs,
|
cs,
|
||||||
@@ -704,7 +706,7 @@ public class SigningFinalizationService {
|
|||||||
textDark,
|
textDark,
|
||||||
textMuted,
|
textMuted,
|
||||||
"Serial:",
|
"Serial:",
|
||||||
certInfo.serialNumber);
|
certInfo.serialNumber());
|
||||||
rRowY -= LINE_H;
|
rRowY -= LINE_H;
|
||||||
drawLabelValue(
|
drawLabelValue(
|
||||||
cs,
|
cs,
|
||||||
@@ -715,7 +717,7 @@ public class SigningFinalizationService {
|
|||||||
textDark,
|
textDark,
|
||||||
textMuted,
|
textMuted,
|
||||||
"Valid From:",
|
"Valid From:",
|
||||||
certInfo.validFrom);
|
certInfo.validFrom());
|
||||||
rRowY -= LINE_H;
|
rRowY -= LINE_H;
|
||||||
drawLabelValue(
|
drawLabelValue(
|
||||||
cs,
|
cs,
|
||||||
@@ -726,7 +728,7 @@ public class SigningFinalizationService {
|
|||||||
textDark,
|
textDark,
|
||||||
textMuted,
|
textMuted,
|
||||||
"Valid Until:",
|
"Valid Until:",
|
||||||
certInfo.validUntil);
|
certInfo.validUntil());
|
||||||
rRowY -= LINE_H;
|
rRowY -= LINE_H;
|
||||||
drawLabelValue(
|
drawLabelValue(
|
||||||
cs,
|
cs,
|
||||||
@@ -737,7 +739,7 @@ public class SigningFinalizationService {
|
|||||||
textDark,
|
textDark,
|
||||||
textMuted,
|
textMuted,
|
||||||
"Algorithm:",
|
"Algorithm:",
|
||||||
certInfo.algorithm);
|
certInfo.algorithm());
|
||||||
}
|
}
|
||||||
|
|
||||||
yPos -= cardH + 12;
|
yPos -= cardH + 12;
|
||||||
@@ -1075,10 +1077,9 @@ public class SigningFinalizationService {
|
|||||||
}
|
}
|
||||||
String alias = aliases.nextElement();
|
String alias = aliases.nextElement();
|
||||||
Certificate cert = keystore.getCertificate(alias);
|
Certificate cert = keystore.getCertificate(alias);
|
||||||
if (!(cert instanceof X509Certificate)) {
|
if (!(cert instanceof X509Certificate x509)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
X509Certificate x509 = (X509Certificate) cert;
|
|
||||||
|
|
||||||
String subjectCN = extractCN(x509.getSubjectX500Principal().getName());
|
String subjectCN = extractCN(x509.getSubjectX500Principal().getName());
|
||||||
String issuerCN = extractCN(x509.getIssuerX500Principal().getName());
|
String issuerCN = extractCN(x509.getIssuerX500Principal().getName());
|
||||||
@@ -1256,55 +1257,19 @@ public class SigningFinalizationService {
|
|||||||
|
|
||||||
// ===== PRIVATE INNER TYPES =====
|
// ===== PRIVATE INNER TYPES =====
|
||||||
|
|
||||||
private static class SessionSignatureSettings {
|
private record SessionSignatureSettings(
|
||||||
final Boolean showSignature;
|
Boolean showSignature,
|
||||||
final Integer pageNumber;
|
Integer pageNumber,
|
||||||
final Boolean showLogo;
|
Boolean showLogo,
|
||||||
final Boolean includeSummaryPage;
|
Boolean includeSummaryPage) {}
|
||||||
|
|
||||||
SessionSignatureSettings(
|
private record ParticipantSignatureMetadata(String reason, String location) {}
|
||||||
Boolean showSignature,
|
|
||||||
Integer pageNumber,
|
|
||||||
Boolean showLogo,
|
|
||||||
Boolean includeSummaryPage) {
|
|
||||||
this.showSignature = showSignature;
|
|
||||||
this.pageNumber = pageNumber;
|
|
||||||
this.showLogo = showLogo;
|
|
||||||
this.includeSummaryPage = includeSummaryPage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ParticipantSignatureMetadata {
|
private record CertificateInfo(
|
||||||
final String reason;
|
String subjectCN,
|
||||||
final String location;
|
String issuerCN,
|
||||||
|
String serialNumber,
|
||||||
ParticipantSignatureMetadata(String reason, String location) {
|
String validFrom,
|
||||||
this.reason = reason;
|
String validUntil,
|
||||||
this.location = location;
|
String algorithm) {}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CertificateInfo {
|
|
||||||
final String subjectCN;
|
|
||||||
final String issuerCN;
|
|
||||||
final String serialNumber;
|
|
||||||
final String validFrom;
|
|
||||||
final String validUntil;
|
|
||||||
final String algorithm;
|
|
||||||
|
|
||||||
CertificateInfo(
|
|
||||||
String subjectCN,
|
|
||||||
String issuerCN,
|
|
||||||
String serialNumber,
|
|
||||||
String validFrom,
|
|
||||||
String validUntil,
|
|
||||||
String algorithm) {
|
|
||||||
this.subjectCN = subjectCN;
|
|
||||||
this.issuerCN = issuerCN;
|
|
||||||
this.serialNumber = serialNumber;
|
|
||||||
this.validFrom = validFrom;
|
|
||||||
this.validUntil = validUntil;
|
|
||||||
this.algorithm = algorithm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -553,7 +553,7 @@ public class WorkflowSessionService {
|
|||||||
dto.setMyStatus(p.getStatus());
|
dto.setMyStatus(p.getStatus());
|
||||||
return dto;
|
return dto;
|
||||||
})
|
})
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-3
@@ -3,7 +3,6 @@ package stirling.software.proprietary.workflow.util;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@@ -80,12 +79,12 @@ public class WorkflowMapper {
|
|||||||
response.setParticipants(
|
response.setParticipants(
|
||||||
session.getParticipants().stream()
|
session.getParticipants().stream()
|
||||||
.map(p -> toParticipantResponse(p, objectMapper, includeShareTokens))
|
.map(p -> toParticipantResponse(p, objectMapper, includeShareTokens))
|
||||||
.collect(Collectors.toList()));
|
.toList());
|
||||||
} else {
|
} else {
|
||||||
response.setParticipants(
|
response.setParticipants(
|
||||||
session.getParticipants().stream()
|
session.getParticipants().stream()
|
||||||
.map(p -> toParticipantResponse(p, includeShareTokens))
|
.map(p -> toParticipantResponse(p, includeShareTokens))
|
||||||
.collect(Collectors.toList()));
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate participant counts
|
// Calculate participant counts
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ ARG BASE_VERSION=1.0.2
|
|||||||
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
||||||
|
|
||||||
# Stage 1: Build the Java application and frontend
|
# Stage 1: Build the Java application and frontend
|
||||||
FROM gradle:9.3.1-jdk25@sha256:85aec999629f4774a383cb792da4b598bdf5a7e69c4b9570bb70c0f919179183 AS app-build
|
FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS app-build
|
||||||
|
|
||||||
ARG TASK_VERSION=3.49.1
|
ARG TASK_VERSION=3.49.1
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ARG BASE_VERSION=1.0.2
|
|||||||
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
||||||
|
|
||||||
# Stage 1: Build the Java application and frontend
|
# Stage 1: Build the Java application and frontend
|
||||||
FROM gradle:9.3.1-jdk25 AS app-build
|
FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS app-build
|
||||||
|
|
||||||
ARG TASK_VERSION=3.49.1
|
ARG TASK_VERSION=3.49.1
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Single JAR contains both frontend and backend with minimal dependencies
|
# Single JAR contains both frontend and backend with minimal dependencies
|
||||||
|
|
||||||
# Stage 1: Build application with embedded frontend
|
# Stage 1: Build application with embedded frontend
|
||||||
FROM gradle:9.3.1-jdk25 AS build
|
FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS build
|
||||||
|
|
||||||
# Install Node.js and npm for frontend build
|
# Install Node.js and npm for frontend build
|
||||||
ARG TASK_VERSION=3.49.1
|
ARG TASK_VERSION=3.49.1
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import psycopg
|
import psycopg
|
||||||
|
from pgvector import Vector
|
||||||
from pgvector.psycopg import register_vector_async
|
from pgvector.psycopg import register_vector_async
|
||||||
from psycopg_pool import AsyncConnectionPool
|
from psycopg_pool import AsyncConnectionPool
|
||||||
|
|
||||||
@@ -68,8 +69,11 @@ class PgVectorStore(DocumentStore):
|
|||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
async def _bootstrap_schema(self) -> None:
|
async def _bootstrap_schema(self) -> None:
|
||||||
# Uses a dedicated non-pool connection: pooled connections register the
|
# The `vector` type must exist before the pool's configure hook
|
||||||
# vector type on checkout, which fails until the extension exists.
|
# (register_vector_async) can resolve it, and on a fresh database it doesn't
|
||||||
|
# yet. Create the extension + schema on a raw, non-pool connection that hasn't
|
||||||
|
# registered the type; _ensure_ready then opens the pool, whose configure hook
|
||||||
|
# registers the now-existing type per connection.
|
||||||
async with await psycopg.AsyncConnection.connect(self._dsn) as conn:
|
async with await psycopg.AsyncConnection.connect(self._dsn) as conn:
|
||||||
async with conn.cursor() as cur:
|
async with conn.cursor() as cur:
|
||||||
await cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
await cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||||
@@ -230,7 +234,7 @@ class PgVectorStore(DocumentStore):
|
|||||||
metadata = EXCLUDED.metadata,
|
metadata = EXCLUDED.metadata,
|
||||||
embedding = EXCLUDED.embedding
|
embedding = EXCLUDED.embedding
|
||||||
""",
|
""",
|
||||||
(doc.id, collection, owner_id, doc.text, json.dumps(doc.metadata), emb),
|
(doc.id, collection, owner_id, doc.text, json.dumps(doc.metadata), Vector(emb)),
|
||||||
)
|
)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
@@ -307,6 +311,7 @@ class PgVectorStore(DocumentStore):
|
|||||||
owner_id = await self._readable_owner_for(cur, collection, principals)
|
owner_id = await self._readable_owner_for(cur, collection, principals)
|
||||||
if owner_id is None:
|
if owner_id is None:
|
||||||
return []
|
return []
|
||||||
|
query_vec = Vector(query_embedding)
|
||||||
await cur.execute(
|
await cur.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, text, metadata, 1 - (embedding <=> %s) AS score
|
SELECT id, text, metadata, 1 - (embedding <=> %s) AS score
|
||||||
@@ -315,7 +320,7 @@ class PgVectorStore(DocumentStore):
|
|||||||
ORDER BY embedding <=> %s
|
ORDER BY embedding <=> %s
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
""",
|
""",
|
||||||
(query_embedding, collection, owner_id, query_embedding, top_k),
|
(query_vec, collection, owner_id, query_vec, top_k),
|
||||||
)
|
)
|
||||||
rows = await cur.fetchall()
|
rows = await cur.fetchall()
|
||||||
|
|
||||||
|
|||||||
@@ -48,11 +48,18 @@ X64_JRE="$WORK_DIR/jre-x86_64"
|
|||||||
run_jlink() {
|
run_jlink() {
|
||||||
local java_home="$1"
|
local java_home="$1"
|
||||||
local out="$2"
|
local out="$2"
|
||||||
|
local compress
|
||||||
|
if "$java_home/bin/jlink" --help 2>&1 | grep -q 'zip-\[0-9\]'; then
|
||||||
|
compress="zip-6"
|
||||||
|
else
|
||||||
|
compress="2"
|
||||||
|
fi
|
||||||
|
|
||||||
"$java_home/bin/jlink" \
|
"$java_home/bin/jlink" \
|
||||||
--module-path "$java_home/jmods" \
|
--module-path "$java_home/jmods" \
|
||||||
--add-modules "$JLINK_MODULES" \
|
--add-modules "$JLINK_MODULES" \
|
||||||
--strip-debug \
|
--strip-debug \
|
||||||
--compress=zip-6 \
|
--compress="$compress" \
|
||||||
--no-header-files \
|
--no-header-files \
|
||||||
--no-man-pages \
|
--no-man-pages \
|
||||||
--output "$out"
|
--output "$out"
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { defineConfig, loadEnv, type PluginOption } from "vite";
|
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
|
||||||
import { compression, defineAlgorithm } from "vite-plugin-compression2";
|
import { compression, defineAlgorithm } from "vite-plugin-compression2";
|
||||||
import { constants, gzip, brotliCompress } from "node:zlib";
|
import fs from "node:fs/promises";
|
||||||
|
import path, { resolve } from "node:path";
|
||||||
|
import { constants, brotliCompress, gzip } from "node:zlib";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import fs from "node:fs/promises";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import path from "node:path";
|
import type { PluginOption } from "vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||||
|
|
||||||
const gzipPromise = promisify(gzip);
|
const gzipPromise = promisify(gzip);
|
||||||
const brotliPromise = promisify(brotliCompress);
|
const brotliPromise = promisify(brotliCompress);
|
||||||
@@ -96,6 +97,7 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
// this file lived at frontend/, but after the editor was moved under
|
// this file lived at frontend/, but after the editor was moved under
|
||||||
// frontend/editor/ the cwd-based lookup would miss editor/.env*.
|
// frontend/editor/ the cwd-based lookup would miss editor/.env*.
|
||||||
const env = loadEnv(mode, import.meta.dirname, "");
|
const env = loadEnv(mode, import.meta.dirname, "");
|
||||||
|
const parentEnv = loadEnv(mode, resolve(import.meta.dirname, ".."), "");
|
||||||
|
|
||||||
// Effective mode: --mode > STIRLING_FLAVOR > ENABLE_SAAS > DISABLE_ADDITIONAL_FEATURES > proprietary.
|
// Effective mode: --mode > STIRLING_FLAVOR > ENABLE_SAAS > DISABLE_ADDITIONAL_FEATURES > proprietary.
|
||||||
const explicitMode = (VALID_MODES as readonly string[]).includes(mode)
|
const explicitMode = (VALID_MODES as readonly string[]).includes(mode)
|
||||||
@@ -120,6 +122,17 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
// Backend proxy target: default localhost:8080. Override via BACKEND_URL env var
|
// Backend proxy target: default localhost:8080. Override via BACKEND_URL env var
|
||||||
// so the top-level dev launcher can wire a dynamically-assigned backend port.
|
// so the top-level dev launcher can wire a dynamically-assigned backend port.
|
||||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:8080";
|
const backendUrl = process.env.BACKEND_URL || "http://localhost:8080";
|
||||||
|
// Allow host header checks to be configured via env so LAN/reverse-proxy
|
||||||
|
// dev setups don't require editing this file for each machine.
|
||||||
|
const allowedHostsRaw =
|
||||||
|
process.env.FRONTEND_ALLOWED_HOSTS ||
|
||||||
|
env.FRONTEND_ALLOWED_HOSTS ||
|
||||||
|
parentEnv.FRONTEND_ALLOWED_HOSTS ||
|
||||||
|
"";
|
||||||
|
const allowedHosts = allowedHostsRaw
|
||||||
|
.split(",")
|
||||||
|
.map((host) => host.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const backendProxy = {
|
const backendProxy = {
|
||||||
target: backendUrl,
|
target: backendUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@@ -214,6 +227,7 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
|
allowedHosts: allowedHosts.length > 0 ? allowedHosts : undefined,
|
||||||
// make sure this port matches the devUrl port in tauri.conf.json file
|
// make sure this port matches the devUrl port in tauri.conf.json file
|
||||||
port: 5173,
|
port: 5173,
|
||||||
// Tauri expects a fixed port, fail if that port is not available
|
// Tauri expects a fixed port, fail if that port is not available
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|||||||
Vendored
+10
-21
@@ -23,8 +23,8 @@
|
|||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@@ -65,7 +65,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
@@ -73,21 +73,10 @@ goto fail
|
|||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public class PropSync {
|
|||||||
File folder = new File("C:\\Users\\systo\\git\\Stirling-PDF\\app\\core\\src\\main\\resources");
|
File folder = new File("C:\\Users\\systo\\git\\Stirling-PDF\\app\\core\\src\\main\\resources");
|
||||||
File[] files = folder.listFiles((dir, name) -> name.matches("messages_.*\\.properties"));
|
File[] files = folder.listFiles((dir, name) -> name.matches("messages_.*\\.properties"));
|
||||||
|
|
||||||
List<String> enLines = Files.readAllLines(Paths.get(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
|
List<String> enLines = Files.readAllLines(Path.of(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
|
||||||
Map<String, String> enProps = linesToProps(enLines);
|
Map<String, String> enProps = linesToProps(enLines);
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ public class RestartHelper {
|
|||||||
Map<String, String> cli = parseArgs(args);
|
Map<String, String> cli = parseArgs(args);
|
||||||
|
|
||||||
long pid = Long.parseLong(req(cli, "pid"));
|
long pid = Long.parseLong(req(cli, "pid"));
|
||||||
Path appJar = Paths.get(req(cli, "app")).toAbsolutePath().normalize();
|
Path appJar = Path.of(req(cli, "app")).toAbsolutePath().normalize();
|
||||||
String javaBin = cli.getOrDefault("java", "java");
|
String javaBin = cli.getOrDefault("java", "java");
|
||||||
Path argsFile = cli.containsKey("argsFile") ? Paths.get(cli.get("argsFile")) : null;
|
Path argsFile = cli.containsKey("argsFile") ? Path.of(cli.get("argsFile")) : null;
|
||||||
long backoffMs = Long.parseLong(cli.getOrDefault("backoffMs", "1000"));
|
long backoffMs = Long.parseLong(cli.getOrDefault("backoffMs", "1000"));
|
||||||
|
|
||||||
if (!Files.isRegularFile(appJar)) {
|
if (!Files.isRegularFile(appJar)) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
# Inputs:
|
# Inputs:
|
||||||
# STIRLING_JAR - path to a pre-built Stirling-PDF .jar (defaults to the
|
# STIRLING_JAR - path to a pre-built Stirling-PDF .jar (defaults to the
|
||||||
# :stirling-pdf:bootJar output)
|
# :stirling-pdf:bootJar output)
|
||||||
|
# JAVA_BIN - override the Java executable used to launch the JAR
|
||||||
|
# (defaults to MIGRATION_TEST_JAVA, JAVA_HOME, then PATH)
|
||||||
# FIXTURE_DIR - override the fixture directory (rarely needed)
|
# FIXTURE_DIR - override the fixture directory (rarely needed)
|
||||||
#
|
#
|
||||||
# Exits non-zero on any fixture failure and writes a summary to stderr.
|
# Exits non-zero on any fixture failure and writes a summary to stderr.
|
||||||
@@ -19,6 +21,7 @@ set -euo pipefail
|
|||||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
FIXTURE_DIR="${FIXTURE_DIR:-$REPO_ROOT/app/proprietary/src/test/resources/db-migration-fixtures}"
|
FIXTURE_DIR="${FIXTURE_DIR:-$REPO_ROOT/app/proprietary/src/test/resources/db-migration-fixtures}"
|
||||||
STIRLING_JAR="${STIRLING_JAR:-}"
|
STIRLING_JAR="${STIRLING_JAR:-}"
|
||||||
|
JAVA_BIN="${JAVA_BIN:-${MIGRATION_TEST_JAVA:-}}"
|
||||||
ADMIN_USERNAME="${ADMIN_USERNAME:-admin}"
|
ADMIN_USERNAME="${ADMIN_USERNAME:-admin}"
|
||||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-stirling}"
|
ADMIN_PASSWORD="${ADMIN_PASSWORD:-stirling}"
|
||||||
STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-300}"
|
STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-300}"
|
||||||
@@ -26,6 +29,52 @@ STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-300}"
|
|||||||
log() { printf '[migration-test] %s\n' "$*" >&2; }
|
log() { printf '[migration-test] %s\n' "$*" >&2; }
|
||||||
fail() { printf '[migration-test][FAIL] %s\n' "$*" >&2; exit 1; }
|
fail() { printf '[migration-test][FAIL] %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
java_major_version() {
|
||||||
|
local java_bin="$1"
|
||||||
|
"$java_bin" -XshowSettings:properties -version 2>&1 \
|
||||||
|
| awk -F'= ' '/java.specification.version =/ { print $2; exit }'
|
||||||
|
}
|
||||||
|
|
||||||
|
find_java() {
|
||||||
|
local candidate
|
||||||
|
if [[ -n "$JAVA_BIN" ]]; then
|
||||||
|
if [[ -x "$JAVA_BIN" ]]; then
|
||||||
|
candidate="$JAVA_BIN"
|
||||||
|
elif command -v "$JAVA_BIN" >/dev/null 2>&1; then
|
||||||
|
candidate=$(command -v "$JAVA_BIN")
|
||||||
|
else
|
||||||
|
fail "JAVA_BIN/MIGRATION_TEST_JAVA='$JAVA_BIN' is not executable or on PATH"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${JAVA_HOME:-}" && -x "${JAVA_HOME}/bin/java" ]]; then
|
||||||
|
candidate="${JAVA_HOME}/bin/java"
|
||||||
|
else
|
||||||
|
local java_home_var
|
||||||
|
for java_home_var in JAVA_HOME_25_X64 JAVA_HOME_25_ARM64 JAVA_HOME_25_AARCH64; do
|
||||||
|
local java_home="${!java_home_var:-}"
|
||||||
|
if [[ -n "$java_home" && -x "$java_home/bin/java" ]]; then
|
||||||
|
candidate="$java_home/bin/java"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z "${candidate:-}" ]]; then
|
||||||
|
candidate=$(command -v java || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "${candidate:-}" ]] || fail "No java executable found; install JDK 25 or set JAVA_BIN"
|
||||||
|
realpath "$candidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_supported_java() {
|
||||||
|
local java_bin="$1"
|
||||||
|
local major
|
||||||
|
major=$(java_major_version "$java_bin")
|
||||||
|
[[ -n "$major" ]] || fail "Could not determine Java version from '$java_bin'"
|
||||||
|
if (( major < 25 )); then
|
||||||
|
fail "Migration test requires JDK 25+ to run the built JAR, but '$java_bin' is Java $major. Set JAVA_HOME or JAVA_BIN to a JDK 25 installation."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
find_jar() {
|
find_jar() {
|
||||||
local candidate
|
local candidate
|
||||||
if [[ -n "$STIRLING_JAR" ]]; then
|
if [[ -n "$STIRLING_JAR" ]]; then
|
||||||
@@ -63,7 +112,9 @@ test_fixture() {
|
|||||||
label=$(basename "$fixture_path" .mv.db)
|
label=$(basename "$fixture_path" .mv.db)
|
||||||
log "=== $label ==="
|
log "=== $label ==="
|
||||||
|
|
||||||
local jar; jar=$(find_jar)
|
local jar
|
||||||
|
jar=$(find_jar)
|
||||||
|
local java_bin="$MIGRATION_JAVA_BIN"
|
||||||
local workdir; workdir=$(mktemp -d)
|
local workdir; workdir=$(mktemp -d)
|
||||||
local configsdir="$workdir/configs"
|
local configsdir="$workdir/configs"
|
||||||
mkdir -p "$configsdir"
|
mkdir -p "$configsdir"
|
||||||
@@ -74,6 +125,7 @@ test_fixture() {
|
|||||||
local log_file="$workdir/app.log"
|
local log_file="$workdir/app.log"
|
||||||
|
|
||||||
log " jar=$jar"
|
log " jar=$jar"
|
||||||
|
log " java=$java_bin ($("$java_bin" -version 2>&1 | head -n 1))"
|
||||||
log " workdir=$workdir"
|
log " workdir=$workdir"
|
||||||
log " port=$port"
|
log " port=$port"
|
||||||
|
|
||||||
@@ -86,7 +138,7 @@ test_fixture() {
|
|||||||
# to cwd, and we want to make sure we hit the fixture's configs/ and not
|
# to cwd, and we want to make sure we hit the fixture's configs/ and not
|
||||||
# whatever happens to live at the runner's working directory.
|
# whatever happens to live at the runner's working directory.
|
||||||
pushd "$workdir" >/dev/null
|
pushd "$workdir" >/dev/null
|
||||||
java -Xmx1g -jar "$jar" \
|
"$java_bin" -Xmx1g -jar "$jar" \
|
||||||
"--server.port=$port" \
|
"--server.port=$port" \
|
||||||
"--spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;MODE=PostgreSQL" \
|
"--spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;MODE=PostgreSQL" \
|
||||||
"--spring.jpa.show-sql=false" \
|
"--spring.jpa.show-sql=false" \
|
||||||
@@ -169,6 +221,9 @@ test_fixture() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
[[ -d "$FIXTURE_DIR" ]] || fail "Fixture dir not found: $FIXTURE_DIR"
|
[[ -d "$FIXTURE_DIR" ]] || fail "Fixture dir not found: $FIXTURE_DIR"
|
||||||
|
MIGRATION_JAVA_BIN=$(find_java)
|
||||||
|
assert_supported_java "$MIGRATION_JAVA_BIN"
|
||||||
|
|
||||||
local fixtures
|
local fixtures
|
||||||
mapfile -t fixtures < <(find "$FIXTURE_DIR" -maxdepth 1 -name '*.mv.db' | sort)
|
mapfile -t fixtures < <(find "$FIXTURE_DIR" -maxdepth 1 -name '*.mv.db' | sort)
|
||||||
[[ ${#fixtures[@]} -gt 0 ]] || fail "No fixtures under $FIXTURE_DIR"
|
[[ ${#fixtures[@]} -gt 0 ]] || fail "No fixtures under $FIXTURE_DIR"
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ _ISE_STEMS = [
|
|||||||
"characteri",
|
"characteri",
|
||||||
"stabili",
|
"stabili",
|
||||||
"generali",
|
"generali",
|
||||||
"specifi? ".strip(),
|
"specifi? ".strip(), # codespell:ignore specifi
|
||||||
"sterili",
|
"sterili",
|
||||||
"neutrali",
|
"neutrali",
|
||||||
"saniti",
|
"saniti",
|
||||||
"tokeni",
|
"tokeni",
|
||||||
"serie? ".strip(),
|
"serie? ".strip(), # codespell:ignore serie
|
||||||
"alphabeti",
|
"alphabeti",
|
||||||
"synthesi",
|
"synthesi",
|
||||||
"memori",
|
"memori",
|
||||||
@@ -213,7 +213,16 @@ _ISE_STEMS = [
|
|||||||
"colouri",
|
"colouri",
|
||||||
]
|
]
|
||||||
_ISE_STEMS = [s for s in _ISE_STEMS if s and not s.endswith("?")]
|
_ISE_STEMS = [s for s in _ISE_STEMS if s and not s.endswith("?")]
|
||||||
_ISE_SUFFIXES = ["se", "ses", "sed", "sing", "ser", "sers", "sation", "sations"]
|
_ISE_SUFFIXES = [
|
||||||
|
"se",
|
||||||
|
"ses",
|
||||||
|
"sed",
|
||||||
|
"sing",
|
||||||
|
"ser", # codespell:ignore ser
|
||||||
|
"sers",
|
||||||
|
"sation",
|
||||||
|
"sations",
|
||||||
|
]
|
||||||
for stem in _ISE_STEMS:
|
for stem in _ISE_STEMS:
|
||||||
for suf in _ISE_SUFFIXES:
|
for suf in _ISE_SUFFIXES:
|
||||||
add(stem + suf, stem + suf.replace("s", "z", 1))
|
add(stem + suf, stem + suf.replace("s", "z", 1))
|
||||||
|
|||||||
@@ -482,6 +482,46 @@ title = "Add Page Numbers"
|
|||||||
|
|
||||||
Keys use dot notation internally (e.g., `addPageNumbers.selectText.1`).
|
Keys use dot notation internally (e.g., `addPageNumbers.selectText.1`).
|
||||||
|
|
||||||
|
### Pluralization Suffixes
|
||||||
|
|
||||||
|
Pluralized i18n keys use the i18next/Intl plural suffixes:
|
||||||
|
`_zero`, `_one`, `_two`, `_few`, `_many`, and `_other`.
|
||||||
|
|
||||||
|
Define the shared base key plus one entry for each plural category the language
|
||||||
|
needs. Always provide `_other`; i18next uses it as the fallback plural form.
|
||||||
|
Use `{{count}}` as the count placeholder, and call the base key from code with a
|
||||||
|
`count` value:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[common]
|
||||||
|
fileCount_one = "{{count}} file"
|
||||||
|
fileCount_other = "{{count}} files"
|
||||||
|
fileCount_zero = "No files"
|
||||||
|
```
|
||||||
|
|
||||||
|
Languages with more plural categories can add their required forms. For example,
|
||||||
|
Arabic-style plural rules distinguish `0`, `1`, `2`, small counts, large counts,
|
||||||
|
and the fallback form:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[common]
|
||||||
|
uploadCount_zero = "No files uploaded" # count = 0
|
||||||
|
uploadCount_one = "One file uploaded" # count = 1
|
||||||
|
uploadCount_two = "Two files uploaded" # count = 2
|
||||||
|
uploadCount_few = "{{count}} files uploaded" # count = 3-10
|
||||||
|
uploadCount_many = "{{count}} files uploaded" # count = 11-99
|
||||||
|
uploadCount_other = "{{count}} files uploaded" # count = 100, fractions, fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
t("common.uploadCount", { count: files.length });
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not translate the suffix names themselves. They are CLDR plural categories,
|
||||||
|
not English words. Add only the forms required by the target language, but keep
|
||||||
|
the plural key set aligned with `en-US` unless the target language needs
|
||||||
|
additional CLDR forms such as `_few` or `_many`.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### Placeholder Preservation
|
### Placeholder Preservation
|
||||||
|
|||||||
Reference in New Issue
Block a user