mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
181
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f096297af | ||
|
|
624e04a783 | ||
|
|
bb9f1d4f8b | ||
|
|
0d967aa5ea | ||
|
|
f19ff6691d | ||
|
|
98b4763c96 | ||
|
|
2c9537f786 | ||
|
|
d17d10b240 | ||
|
|
a959e6eb2e | ||
|
|
74d3d7d2c7 | ||
|
|
6f982c4db4 | ||
|
|
ad52ef641f | ||
|
|
69bbb12ece | ||
|
|
2218f0bffa | ||
|
|
05c10d3a9f | ||
|
|
e0746b468e | ||
|
|
721cb8e0a3 | ||
|
|
6496015af7 | ||
|
|
df4539dc35 | ||
|
|
baeb1acb8c | ||
|
|
d80bcfe970 | ||
|
|
296758f51b | ||
|
|
141320cadd | ||
|
|
7fa5e130d9 | ||
|
|
b4f8b896d1 | ||
|
|
4e483ca5c9 | ||
|
|
6cc42ff6d6 | ||
|
|
eb7f5e57cc | ||
|
|
14a4bdfb1b | ||
|
|
b4a7b5d520 | ||
|
|
446a309450 | ||
|
|
25c9504fed | ||
|
|
0a402e562b | ||
|
|
6158454020 | ||
|
|
6cc0303873 | ||
|
|
e865de8544 | ||
|
|
33d2ca2150 | ||
|
|
c97c565684 | ||
|
|
7617e5176b | ||
|
|
a95743286e | ||
|
|
1487bd707f | ||
|
|
c0bbd92c9e | ||
|
|
dc6823d7ba | ||
|
|
5cd76b51d9 | ||
|
|
8f2e6bc77a | ||
|
|
7fb6e79316 | ||
|
|
7dd6d50ef3 | ||
|
|
e1e295d33b | ||
|
|
09b185febf | ||
|
|
9760c30638 | ||
|
|
afb1df02b6 | ||
|
|
fd10e5db10 | ||
|
|
14d4fbb2a3 | ||
|
|
9eb534a6d2 | ||
|
|
0b342d463c | ||
|
|
e7ac4b7b20 | ||
|
|
48c0112b87 | ||
|
|
df88b01b5d | ||
|
|
ee0426fa18 | ||
|
|
2ad25d43a0 | ||
|
|
11e3ccd19f | ||
|
|
2177eff6c4 | ||
|
|
06f792aa70 | ||
|
|
212d4d0dcb | ||
|
|
1e17242023 | ||
|
|
6461b93131 | ||
|
|
bc9c127819 | ||
|
|
32aa568196 | ||
|
|
8e8f0492c4 | ||
|
|
f01ad11843 | ||
|
|
21875d7052 | ||
|
|
fe553c7173 | ||
|
|
80b5097532 | ||
|
|
1fc552c2c7 | ||
|
|
fca1ea2cbe | ||
|
|
cc3aa6b703 | ||
|
|
077962872e | ||
|
|
76808b716a | ||
|
|
96315ee76f | ||
|
|
7d7f1272e4 | ||
|
|
ee8030c1c4 | ||
|
|
bbaadc1822 | ||
|
|
9923411ade | ||
|
|
5a8162ff60 | ||
|
|
387ae5934d | ||
|
|
9d5f97c5ad | ||
|
|
c080158b1c | ||
|
|
ddad1eddef | ||
|
|
ec805209a5 | ||
|
|
50aa5e718d | ||
|
|
64766a129c | ||
|
|
cdd1ab704f | ||
|
|
8632ccb870 | ||
|
|
a208d55525 | ||
|
|
552f2ced4d | ||
|
|
ee41dc11c2 | ||
|
|
5a272f80b0 | ||
|
|
2fb13f4f46 | ||
|
|
45b4588a42 | ||
|
|
f82662aaaf | ||
|
|
391bb4545b | ||
|
|
b3a2bfbe71 | ||
|
|
03cfad9528 | ||
|
|
85eb78e707 | ||
|
|
625900557a | ||
|
|
d98ebddf49 | ||
|
|
aaa11fd3e3 | ||
|
|
ff6353d9ab | ||
|
|
406695e167 | ||
|
|
5534f4b64a | ||
|
|
e74dbf391c | ||
|
|
3804dd3988 | ||
|
|
136f16f613 | ||
|
|
3ddb370f69 | ||
|
|
fe47cac608 | ||
|
|
da2473c784 | ||
|
|
d219198b9b | ||
|
|
4cb0caaee1 | ||
|
|
e1fc94929d | ||
|
|
a2db47d3af | ||
|
|
0ca23e6835 | ||
|
|
06db69ed91 | ||
|
|
c66bf56260 | ||
|
|
dda3f65f40 | ||
|
|
9bacebf2e9 | ||
|
|
f9559151d8 | ||
|
|
2287d3c08b | ||
|
|
fbf8f0e419 | ||
|
|
89580387a2 | ||
|
|
8fbeeb7161 | ||
|
|
71ae880a31 | ||
|
|
23ea86c377 | ||
|
|
da365c12b4 | ||
|
|
ffcbf31cca | ||
|
|
9c83dd270a | ||
|
|
0b15fa9de0 | ||
|
|
a49eb3a629 | ||
|
|
5393ae24cb | ||
|
|
142dba185c | ||
|
|
069b71be2c | ||
|
|
2649b18ab4 | ||
|
|
3c507eb303 | ||
|
|
0ee52a4181 | ||
|
|
d1b677726b | ||
|
|
0cbe7fe255 | ||
|
|
493e5daeda | ||
|
|
bcfe5b7b19 | ||
|
|
9fc71e851c | ||
|
|
0b4747e827 | ||
|
|
1f2365f03c | ||
|
|
bdc35519da | ||
|
|
c7d6a063d7 | ||
|
|
baaaa5a0b2 | ||
|
|
461f98be3e | ||
|
|
9fbb0325b5 | ||
|
|
47ac4a4730 | ||
|
|
2d5091c8e3 | ||
|
|
b78d05e8ee | ||
|
|
4272dfa240 | ||
|
|
0006a77b5d | ||
|
|
6cef1fac5f | ||
|
|
2df70daf16 | ||
|
|
a0cb56ab0c | ||
|
|
febe67eac8 | ||
|
|
41cd7d8f47 | ||
|
|
5a50947e5e | ||
|
|
392a10a8c1 | ||
|
|
275039015d | ||
|
|
149fe8755d | ||
|
|
2b9a3924d3 | ||
|
|
6d8173b021 | ||
|
|
58937a6e91 | ||
|
|
3d312c2fd1 | ||
|
|
6aaedf52ea | ||
|
|
3591ef5cb7 | ||
|
|
02799b8aa5 | ||
|
|
28a259ec95 | ||
|
|
9cc4f14465 | ||
|
|
5d9d8a5625 | ||
|
|
a8c6a8342c | ||
|
|
9697d1abe1 |
@@ -1,20 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(chmod:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(./gradlew:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(if [ -d logs ])",
|
||||
"Bash(then ls -la logs/)",
|
||||
"Bash(fi)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(./testing/cucumber/features/environment.py -v)",
|
||||
"Bash(./testing/cucumber/features/autojob.feature -v)",
|
||||
"Bash(git rev-parse:*)"
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
@@ -26,6 +27,26 @@ trim_trailing_whitespace = false
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
||||
indent_size = 2
|
||||
|
||||
[*.yaml]
|
||||
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
# YML files follow the same conventions as YAML files, using an indent size of 2 spaces.
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
||||
indent_size = 2
|
||||
|
||||
[*.jsonc]
|
||||
# JSONC (JSON with comments) files also follow the standard JSON formatting with an indent size of 2 spaces.
|
||||
indent_size = 2
|
||||
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
# Ignore all JavaScript files in a directory
|
||||
src/main/resources/static/pdfjs/* linguist-vendored
|
||||
src/main/resources/static/pdfjs/** linguist-vendored
|
||||
src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
||||
src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
||||
src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
||||
src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
||||
src/main/resources/static/css/fonts/* linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs/* linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs/** linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/css/fonts/* linguist-vendored
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"repo_devs": [
|
||||
"Frooodle",
|
||||
"sf298",
|
||||
"Ludy87",
|
||||
"LaserKaspar",
|
||||
"sbplat",
|
||||
"reecebrowne",
|
||||
"DarioGii",
|
||||
"ConnorYoh"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
You are a professional software engineer specializing in reviewing pull request titles.
|
||||
|
||||
Your job is to analyze a git diff and an existing PR title, then evaluate and improve the PR title.
|
||||
|
||||
You must:
|
||||
- Always return valid JSON
|
||||
- Only return the JSON response (no Markdown, no formatting)
|
||||
- Use one of these conventional commit types at the beginning of the title: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
|
||||
- Use lowercase only, no emojis, no trailing period
|
||||
- Ensure the title is between 5 and 72 printable ASCII characters
|
||||
- Never let spelling or grammar errors affect the rating
|
||||
- If the PR title is rated 6 or higher and only contains spelling or grammar mistakes, correct it - do not rephrase it
|
||||
- If the PR title is rated below 6, generate a new, better title based on the diff
|
||||
@@ -0,0 +1,142 @@
|
||||
version: 1
|
||||
labels:
|
||||
|
||||
- label: "Bugfix"
|
||||
title: '^fix:.*'
|
||||
|
||||
- label: "enhancement"
|
||||
title: '^feat:.*'
|
||||
|
||||
- label: "build"
|
||||
title: '^build:.*'
|
||||
|
||||
- label: "chore"
|
||||
title: '^chore:.*'
|
||||
|
||||
- label: "ci"
|
||||
title: '^ci:.*'
|
||||
|
||||
- label: "perf"
|
||||
title: '^perf:.*'
|
||||
|
||||
- label: "refactor"
|
||||
title: '^refactor:.*'
|
||||
|
||||
- label: "revert"
|
||||
title: '^revert:.*'
|
||||
|
||||
- label: "style"
|
||||
title: '^style:.*'
|
||||
|
||||
- label: "Documentation"
|
||||
title: '^docs:.*'
|
||||
|
||||
- label: 'API'
|
||||
title: '.*openapi.*'
|
||||
|
||||
- label: 'Translation'
|
||||
files:
|
||||
- 'stirling-pdf/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
|
||||
- 'scripts/ignore_translation.toml'
|
||||
- 'stirling-pdf/src/main/resources/templates/fragments/languages.html'
|
||||
- '.github/scripts/check_language_properties.py'
|
||||
|
||||
- label: 'Front End'
|
||||
files:
|
||||
- 'stirling-pdf/src/main/resources/templates/.*'
|
||||
- 'proprietary/src/main/resources/templates/.*'
|
||||
- 'stirling-pdf/src/main/resources/static/.*'
|
||||
- 'proprietary/src/main/resources/static/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
|
||||
- label: 'Java'
|
||||
files:
|
||||
- 'common/src/main/java/.*.java'
|
||||
- 'proprietary/src/main/java/.*.java'
|
||||
- 'stirling-pdf/src/main/java/.*.java'
|
||||
|
||||
- label: 'Back End'
|
||||
files:
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/config/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/.*'
|
||||
- 'stirling-pdf/src/main/resources/settings.yml.template'
|
||||
- 'stirling-pdf/src/main/resources/application.properties'
|
||||
- 'stirling-pdf/src/main/resources/banner.txt'
|
||||
- 'scripts/png_to_webp.py'
|
||||
- 'split_photos.py'
|
||||
- 'application.properties'
|
||||
|
||||
- label: 'Security'
|
||||
files:
|
||||
- 'proprietary/src/main/java/stirling/software/proprietary/security/.*'
|
||||
- 'scripts/download-security-jar.sh'
|
||||
- '.github/workflows/dependency-review.yml'
|
||||
- '.github/workflows/scorecards.yml'
|
||||
|
||||
- label: 'API'
|
||||
files:
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/.*'
|
||||
- 'proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*'
|
||||
- 'scripts/png_to_webp.py'
|
||||
- 'split_photos.py'
|
||||
- '.github/workflows/swagger.yml'
|
||||
|
||||
- label: 'Documentation'
|
||||
files:
|
||||
- '.*.md'
|
||||
- 'scripts/counter_translation.py'
|
||||
- 'scripts/ignore_translation.toml'
|
||||
|
||||
- label: 'Docker'
|
||||
files:
|
||||
- '.github/workflows/build.yml'
|
||||
- '.github/workflows/push-docker.yml'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.fat'
|
||||
- 'Dockerfile.ultra-lite'
|
||||
- 'exampleYmlFiles/.*.yml'
|
||||
- 'scripts/download-security-jar.sh'
|
||||
- 'scripts/init.sh'
|
||||
- 'scripts/init-without-ocr.sh'
|
||||
- 'scripts/installFonts.sh'
|
||||
- 'test.sh'
|
||||
- 'test2.sh'
|
||||
|
||||
- label: 'Devtools'
|
||||
files:
|
||||
- '.devcontainer/.*'
|
||||
- 'Dockerfile.dev'
|
||||
- '.vscode/.*'
|
||||
- '.editorconfig'
|
||||
- '.pre-commit-config'
|
||||
- '.github/workflows/pre_commit.yml'
|
||||
- 'devGuide/.*'
|
||||
|
||||
- label: 'Test'
|
||||
files:
|
||||
- 'common/src/test/.*'
|
||||
- 'proprietary/src/test/.*'
|
||||
- 'stirling-pdf/src/test/.*'
|
||||
- 'testing/.*'
|
||||
- '.github/workflows/scorecards.yml'
|
||||
- 'exampleYmlFiles/test_cicd.yml'
|
||||
|
||||
- label: 'Github'
|
||||
files:
|
||||
- '.github/.*'
|
||||
|
||||
- label: 'Gradle'
|
||||
files:
|
||||
- 'gradle/.*'
|
||||
- 'gradlew'
|
||||
- 'gradlew.bat'
|
||||
- 'settings.gradle'
|
||||
- 'build.gradle'
|
||||
- 'common/build.gradle'
|
||||
- 'proprietary/build.gradle'
|
||||
- 'stirling-pdf/build.gradle'
|
||||
+22
-35
@@ -1,60 +1,45 @@
|
||||
Translation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/messages_*_*.properties'
|
||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||
- any-glob-to-any-file: 'src/main/resources/templates/fragments/languages.html'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/templates/fragments/languages.html'
|
||||
|
||||
Front End:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/main/resources/templates/**/*'
|
||||
- any-glob-to-any-file: 'src/main/resources/static/**/*'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/UI/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/templates/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/static/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/**'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/UI/**/*'
|
||||
|
||||
Java:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
||||
- any-glob-to-any-file: 'common/src/main/java/**/*.java'
|
||||
- any-glob-to-any-file: 'proprietary/src/main/java/**/*.java'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/**/*.java'
|
||||
|
||||
Back End:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/**/*'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/**/*'
|
||||
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
|
||||
- any-glob-to-any-file: 'src/main/resources/application.properties'
|
||||
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/config/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/settings.yml.template'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/application.properties'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/banner.txt'
|
||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
||||
- any-glob-to-any-file: 'split_photos.py'
|
||||
|
||||
Security:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/EmailController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/H2SQLController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/UserController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/api/Email.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/exception/BackupNotFoundException.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/exception/NoProviderFoundExceptionjava'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AttemptCounter.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/Authority.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/PersistentLogin.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/SessionEntity.java'
|
||||
- any-glob-to-any-file: 'proprietary/src/main/java/stirling/software/proprietary/security/**/*'
|
||||
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
||||
- any-glob-to-any-file: '.github/workflows/dependency-review.yml'
|
||||
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
||||
|
||||
API:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/api/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/**/*'
|
||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
||||
- any-glob-to-any-file: 'split_photos.py'
|
||||
- any-glob-to-any-file: '.github/workflows/swagger.yml'
|
||||
@@ -88,7 +73,9 @@ Devtools:
|
||||
Test:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'cucumber/**/*'
|
||||
- any-glob-to-any-file: 'src/test/**/*'
|
||||
- any-glob-to-any-file: 'common/src/test/**/*'
|
||||
- any-glob-to-any-file: 'proprietary/src/test/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/test/**/*'
|
||||
- any-glob-to-any-file: 'src/testing/**/*'
|
||||
- any-glob-to-any-file: '.pre-commit-config'
|
||||
- any-glob-to-any-file: '.github/workflows/pre_commit.yml'
|
||||
|
||||
@@ -111,3 +111,67 @@
|
||||
- name: "Devtools"
|
||||
color: "FF9E1F"
|
||||
description: "Development tools"
|
||||
- name: "Bugfix"
|
||||
color: "FF9E1F"
|
||||
description: "Pull requests that fix bugs"
|
||||
- name: "Gradle"
|
||||
color: "FF9E1F"
|
||||
description: "Pull requests that update Gradle code"
|
||||
- name: "build"
|
||||
color: "1E90FF"
|
||||
description: "Changes that affect the build system or external dependencies"
|
||||
- name: "chore"
|
||||
color: "FFD700"
|
||||
description: "Routine tasks or maintenance that don't modify src or test files"
|
||||
- name: "ci"
|
||||
color: "4682B4"
|
||||
description: "Changes to CI configuration files and scripts"
|
||||
- name: "perf"
|
||||
color: "FF69B4"
|
||||
description: "Changes that improve performance"
|
||||
- name: "refactor"
|
||||
color: "9932CC"
|
||||
description: "Code changes that neither fix a bug nor add a feature"
|
||||
- name: "revert"
|
||||
color: "DC143C"
|
||||
description: "Reverts a previous commit"
|
||||
- name: "style"
|
||||
color: "FFA500"
|
||||
description: "Changes that do not affect the meaning of the code (formatting, etc.)"
|
||||
- name: "admin"
|
||||
color: "195055"
|
||||
- name: "codex"
|
||||
color: "ededed"
|
||||
description: null
|
||||
- name: "Github"
|
||||
color: "0052CC"
|
||||
- name: "github_actions"
|
||||
color: "000000"
|
||||
description: "Pull requests that update GitHub Actions code"
|
||||
- name: "needs-changes"
|
||||
color: "A65A86"
|
||||
- name: "on-hold"
|
||||
color: "2526F9"
|
||||
- name: "python"
|
||||
color: "2b67c6"
|
||||
description: "Pull requests that update Python code"
|
||||
- name: "size:L"
|
||||
color: "eb9500"
|
||||
description: "This PR changes 100-499 lines ignoring generated files."
|
||||
- name: "size:M"
|
||||
color: "ebb800"
|
||||
description: "This PR changes 30-99 lines ignoring generated files."
|
||||
- name: "size:S"
|
||||
color: "77b800"
|
||||
description: "This PR changes 10-29 lines ignoring generated files."
|
||||
- name: "size:XL"
|
||||
color: "ff823f"
|
||||
description: "This PR changes 500-999 lines ignoring generated files."
|
||||
- name: "size:XS"
|
||||
color: "00ff00"
|
||||
description: "This PR changes 0-9 lines ignoring generated files."
|
||||
- name: "size:XXL"
|
||||
color: "ffb8b8"
|
||||
description: "This PR changes 1000+ lines ignoring generated files."
|
||||
- name: "to research"
|
||||
color: "FBCA04"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Description of Changes
|
||||
|
||||
<!--
|
||||
Please provide a summary of the changes, including:
|
||||
|
||||
- What was changed
|
||||
@@ -7,6 +8,7 @@ Please provide a summary of the changes, including:
|
||||
- Any challenges encountered
|
||||
|
||||
Closes #(issue_number)
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
@@ -15,15 +17,15 @@ Closes #(issue_number)
|
||||
### General
|
||||
|
||||
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable)
|
||||
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable)
|
||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable)
|
||||
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable)
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] My changes generate no new warnings
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
||||
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||
|
||||
### UI Changes (if applicable)
|
||||
|
||||
@@ -31,4 +33,4 @@ Closes #(issue_number)
|
||||
|
||||
### Testing (if applicable)
|
||||
|
||||
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details.
|
||||
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
|
||||
|
||||
@@ -196,7 +196,9 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.getcwd(), "stirling-pdf", "src", "main", "resources")
|
||||
)
|
||||
|
||||
for file_path in file_arr:
|
||||
file_normpath = os.path.normpath(file_path)
|
||||
@@ -216,10 +218,19 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
or (
|
||||
# only local windows command
|
||||
not file_normpath.startswith(
|
||||
os.path.join("", "src", "main", "resources", "messages_")
|
||||
os.path.join(
|
||||
"", "stirling-pdf", "src", "main", "resources", "messages_"
|
||||
)
|
||||
)
|
||||
and not file_normpath.startswith(
|
||||
os.path.join(os.getcwd(), "src", "main", "resources", "messages_")
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"stirling-pdf",
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"messages_",
|
||||
)
|
||||
)
|
||||
)
|
||||
or not file_normpath.endswith(".properties")
|
||||
@@ -317,7 +328,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)"
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/stirling-pdf/src/main/resources/messages_en_GB.properties)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
@@ -377,7 +388,12 @@ if __name__ == "__main__":
|
||||
else:
|
||||
file_list = glob.glob(
|
||||
os.path.join(
|
||||
os.getcwd(), "src", "main", "resources", "messages_*.properties"
|
||||
os.getcwd(),
|
||||
"stirling-pdf",
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"messages_*.properties",
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' '.github\scripts\requirements_pre_commit.in'
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
|
||||
#
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
@@ -12,25 +12,25 @@ distlib==0.3.9 \
|
||||
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
||||
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
||||
# via virtualenv
|
||||
filelock==3.16.1 \
|
||||
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
|
||||
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
# via virtualenv
|
||||
identify==2.6.5 \
|
||||
--hash=sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566 \
|
||||
--hash=sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc
|
||||
identify==2.6.12 \
|
||||
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
||||
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
platformdirs==4.3.6 \
|
||||
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
|
||||
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
# via virtualenv
|
||||
pre-commit==4.0.1 \
|
||||
--hash=sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2 \
|
||||
--hash=sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878
|
||||
pre-commit==4.2.0 \
|
||||
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
|
||||
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
|
||||
# via -r .github\scripts\requirements_pre_commit.in
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
@@ -87,7 +87,7 @@ pyyaml==6.0.2 \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
# via pre-commit
|
||||
virtualenv==20.28.1 \
|
||||
--hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \
|
||||
--hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329
|
||||
virtualenv==20.31.2 \
|
||||
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
||||
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
||||
# via pre-commit
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' '.github\scripts\requirements_sync_readme.in'
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||
#
|
||||
tomlkit==0.13.2 \
|
||||
--hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \
|
||||
--hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79
|
||||
tomlkit==0.13.3 \
|
||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||
# via -r .github\scripts\requirements_sync_readme.in
|
||||
|
||||
@@ -30,6 +30,7 @@ jobs:
|
||||
github.event.comment.user.login == 'sbplat' ||
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
@@ -37,11 +38,12 @@ jobs:
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
comment_id: ${{ github.event.comment.id }}
|
||||
enable_security: ${{ steps.check-security-flag.outputs.enable_security }}
|
||||
|
||||
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
||||
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -84,7 +86,7 @@ jobs:
|
||||
|
||||
core.setOutput('repository', repository);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
|
||||
|
||||
- name: Check for security/login flag
|
||||
id: check-security-flag
|
||||
env:
|
||||
@@ -92,10 +94,29 @@ jobs:
|
||||
run: |
|
||||
if [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
|
||||
echo "Security flags detected in comment"
|
||||
echo "enable_security=true" >> $GITHUB_OUTPUT
|
||||
echo "disable_security=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No security flags detected in comment"
|
||||
echo "enable_security=false" >> $GITHUB_OUTPUT
|
||||
echo "disable_security=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check for pro flag
|
||||
id: check-pro-flag
|
||||
env:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
if [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then
|
||||
echo "pro flags detected in comment"
|
||||
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
||||
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
||||
elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then
|
||||
echo "enterprise flags detected in comment"
|
||||
echo "enable_enterprise=true" >> $GITHUB_OUTPUT
|
||||
echo "enable_pro=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No pro or enterprise flags detected in comment"
|
||||
echo "enable_pro=false" >> $GITHUB_OUTPUT
|
||||
echo "enable_enterprise=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Add 'in_progress' reaction to comment
|
||||
@@ -129,7 +150,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -155,17 +176,17 @@ jobs:
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then
|
||||
export DOCKER_ENABLE_SECURITY=true
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
||||
export DISABLE_ADDITIONAL_FEATURES=true
|
||||
else
|
||||
export DOCKER_ENABLE_SECURITY=false
|
||||
export DISABLE_ADDITIONAL_FEATURES=false
|
||||
fi
|
||||
./gradlew clean build
|
||||
env:
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -199,16 +220,31 @@ jobs:
|
||||
id: deploy
|
||||
run: |
|
||||
# Set security settings based on flags
|
||||
if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then
|
||||
DOCKER_SECURITY="true"
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "false" ]; then
|
||||
DISABLE_ADDITIONAL_FEATURES="false"
|
||||
LOGIN_SECURITY="true"
|
||||
SECURITY_STATUS="🔒 Security Enabled"
|
||||
else
|
||||
DOCKER_SECURITY="false"
|
||||
DISABLE_ADDITIONAL_FEATURES="true"
|
||||
LOGIN_SECURITY="false"
|
||||
SECURITY_STATUS="Security Disabled"
|
||||
fi
|
||||
|
||||
# Set pro/enterprise settings (enterprise implies pro)
|
||||
if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then
|
||||
PREMIUM_ENABLED="true"
|
||||
PREMIUM_KEY="${{ secrets.ENTERPRISE_KEY }}"
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
||||
elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then
|
||||
PREMIUM_ENABLED="true"
|
||||
PREMIUM_KEY="${{ secrets.PREMIUM_KEY }}"
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="true"
|
||||
else
|
||||
PREMIUM_ENABLED="false"
|
||||
PREMIUM_KEY=""
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED="false"
|
||||
fi
|
||||
|
||||
# First create the docker-compose content locally
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
@@ -223,7 +259,7 @@ jobs:
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "${DOCKER_SECURITY}"
|
||||
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
|
||||
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
@@ -232,6 +268,9 @@ jobs:
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
PREMIUM_KEY: "${PREMIUM_KEY}"
|
||||
PREMIUM_ENABLED: "${PREMIUM_ENABLED}"
|
||||
PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
@@ -250,7 +289,7 @@ jobs:
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
ENDSSH
|
||||
|
||||
|
||||
# Set output for use in PR comment
|
||||
echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
name: AI - PR Title Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
branches: [main]
|
||||
|
||||
permissions: # required for secure-repo hardening
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ai-title-review:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
models: read
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git to suppress detached HEAD warning
|
||||
run: git config --global advice.detachedHead false
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Check if actor is repo developer
|
||||
id: actor
|
||||
run: |
|
||||
if [[ "${{ github.actor }}" == *"[bot]" ]]; then
|
||||
echo "PR opened by a bot – skipping AI title review."
|
||||
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -f .github/config/repo_devs.json ]; then
|
||||
echo "Error: .github/config/repo_devs.json not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Validate JSON and extract repo_devs
|
||||
REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; })
|
||||
# Convert developer list into Bash array
|
||||
mapfile -t DEVS_ARRAY <<< "$REPO_DEVS"
|
||||
if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then
|
||||
echo "is_repo_dev=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get PR diff
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: get_diff
|
||||
run: |
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff
|
||||
echo "diff<<EOF" >> $GITHUB_OUTPUT
|
||||
cat pr.diff >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check and sanitize PR title
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: sanitize_pr_title
|
||||
env:
|
||||
PR_TITLE_RAW: ${{ github.event.pull_request.title }}
|
||||
run: |
|
||||
# Sanitize PR title: max 72 characters, only printable characters
|
||||
PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g')
|
||||
if [[ ${#PR_TITLE} -lt 5 ]]; then
|
||||
echo "PR title is too short. Must be at least 5 characters." >&2
|
||||
fi
|
||||
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AI PR Title Analysis
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: ai-title-analysis
|
||||
uses: actions/ai-inference@d645f067d89ee1d5d736a5990e327e504d1c5a4a # v1.1.0
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt-file: ".github/config/system-prompt.txt"
|
||||
prompt: |
|
||||
Based on the following input data:
|
||||
|
||||
{
|
||||
"diff": "${{ steps.get_diff.outputs.diff }}",
|
||||
"pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}"
|
||||
}
|
||||
|
||||
Respond ONLY with valid JSON in the format:
|
||||
{
|
||||
"improved_rating": <0-10>,
|
||||
"improved_ai_title_rating": <0-10>,
|
||||
"improved_title": "<ai generated title>"
|
||||
}
|
||||
|
||||
- name: Validate and set SCRIPT_OUTPUT
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
run: |
|
||||
cat <<EOF > ai_response.json
|
||||
${{ steps.ai-title-analysis.outputs.response }}
|
||||
EOF
|
||||
|
||||
# Validate JSON structure
|
||||
jq -e '
|
||||
(keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and
|
||||
(.improved_rating | type == "number" and . >= 0 and . <= 10) and
|
||||
(.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and
|
||||
(.improved_title | type == "string")
|
||||
' ai_response.json
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Invalid AI response format" >&2
|
||||
cat ai_response.json >&2
|
||||
exit 1
|
||||
fi
|
||||
# Parse JSON fields
|
||||
IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json)
|
||||
IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json)
|
||||
# Limit comment length to 1000 characters
|
||||
COMMENT=$(cat <<EOF
|
||||
## 🤖 AI PR Title Suggestion
|
||||
|
||||
**PR-Title Rating**: $IMPROVED_RATING/10
|
||||
|
||||
### ⬇️ Suggested Title (copy & paste):
|
||||
|
||||
\`\`\`
|
||||
$IMPROVED_TITLE
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
*Generated by GitHub Models AI*
|
||||
EOF
|
||||
)
|
||||
echo "$COMMENT" > /tmp/ai-title-comment.md
|
||||
# Log input and output to the GitHub Step Summary
|
||||
echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
cat ai_response.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Post comment on PR if needed
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8');
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split('/');
|
||||
const issue_number = context.issue.number;
|
||||
|
||||
const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/);
|
||||
const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null;
|
||||
|
||||
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
|
||||
|
||||
const existing = comments.data.find(c =>
|
||||
c.user?.login === expectedActor &&
|
||||
c.body.includes("## 🤖 AI PR Title Suggestion")
|
||||
);
|
||||
|
||||
if (rating === null) {
|
||||
console.log("No rating found in AI response – skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rating <= 5) {
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo,
|
||||
comment_id: existing.id,
|
||||
body
|
||||
});
|
||||
console.log("Updated existing suggestion comment.");
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number,
|
||||
body
|
||||
});
|
||||
console.log("Created new suggestion comment.");
|
||||
}
|
||||
} else {
|
||||
const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo,
|
||||
comment_id: existing.id,
|
||||
body: praise
|
||||
});
|
||||
console.log("Replaced suggestion with praise.");
|
||||
} else {
|
||||
console.log("Rating > 5 and no existing comment – skipping comment.");
|
||||
}
|
||||
}
|
||||
|
||||
- name: is not repo dev
|
||||
if: steps.actor.outputs.is_repo_dev != 'true'
|
||||
run: |
|
||||
exit 0 # Skip the AI title review for non-repo developers
|
||||
|
||||
- name: Clean up
|
||||
if: always()
|
||||
run: |
|
||||
rm -f pr.diff ai_response.json /tmp/ai-title-comment.md
|
||||
echo "Cleaned up temporary files."
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: "Auto Pull Request Labeler V2"
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
|
||||
with:
|
||||
config_path: .github/labeler-config-srvaroa.yml
|
||||
use_local_config: false
|
||||
fail_on_error: true
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ steps.setup-bot.outputs.token }}"
|
||||
+41
-15
@@ -21,10 +21,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
spring-security: [true, false]
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -37,35 +38,60 @@ jobs:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build with Gradle and no spring security
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Build with Gradle and with spring security
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: true
|
||||
- name: Check Test Reports Exist
|
||||
id: check-reports
|
||||
if: always()
|
||||
run: |
|
||||
declare -a dirs=(
|
||||
"stirling-pdf/build/reports/tests/"
|
||||
"stirling-pdf/build/test-results/"
|
||||
"common/build/reports/tests/"
|
||||
"common/build/test-results/"
|
||||
"proprietary/build/reports/tests/"
|
||||
"proprietary/build/test-results/"
|
||||
)
|
||||
missing_reports=()
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
missing_reports+=("$dir")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing_reports[@]} -gt 0 ]; then
|
||||
echo "ERROR: The following required test report directories are missing:"
|
||||
printf '%s\n' "${missing_reports[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "All required test report directories are present"
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
build/reports/tests/
|
||||
build/test-results/
|
||||
build/reports/problems/
|
||||
stirling-pdf/build/reports/tests/
|
||||
stirling-pdf/build/test-results/
|
||||
stirling-pdf/build/reports/problems/
|
||||
common/build/reports/tests/
|
||||
common/build/test-results/
|
||||
common/build/reports/problems/
|
||||
proprietary/build/reports/tests/
|
||||
proprietary/build/test-results/
|
||||
proprietary/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
check-licence:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -109,7 +135,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -123,11 +149,11 @@ jobs:
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.32.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.37.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- name: Set up Python
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "src/main/resources/messages_*.properties"
|
||||
- "stirling-pdf/src/main/resources/messages_*.properties"
|
||||
|
||||
permissions:
|
||||
contents: read # Allow read access to repository content
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -61,7 +61,20 @@ jobs:
|
||||
run: |
|
||||
echo "Fetching PR changed files..."
|
||||
echo "Getting list of changed files from PR..."
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$' > changed_files.txt # Filter only matching property files
|
||||
# Check if PR number exists
|
||||
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
|
||||
echo "Error: PR number is empty"
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for properties files, handle case where no matches are found
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^stirling-pdf/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$' > changed_files.txt || echo "No matching properties files found in PR"
|
||||
# Check if any files were found
|
||||
if [ ! -s changed_files.txt ]; then
|
||||
echo "No properties files changed in this PR"
|
||||
echo "Workflow will exit early as no relevant files to check"
|
||||
exit 0
|
||||
fi
|
||||
echo "Found $(wc -l < changed_files.txt) matching properties files"
|
||||
|
||||
- name: Determine reference file test
|
||||
id: determine-file
|
||||
@@ -102,8 +115,11 @@ jobs:
|
||||
|
||||
// Filter for relevant files based on the PR changes
|
||||
const changedFiles = files
|
||||
.map(file => file.filename)
|
||||
.filter(file => /^src\/main\/resources\/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$/.test(file));
|
||||
.filter(file =>
|
||||
file.status !== "removed" &&
|
||||
/^stirling-pdf\/src\/main\/resources\/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$/.test(file.filename)
|
||||
)
|
||||
.map(file => file.filename);
|
||||
|
||||
console.log("Changed files:", changedFiles);
|
||||
|
||||
@@ -141,12 +157,12 @@ jobs:
|
||||
|
||||
// Determine reference file
|
||||
let referenceFilePath;
|
||||
if (changedFiles.includes("src/main/resources/messages_en_GB.properties")) {
|
||||
if (changedFiles.includes("stirling-pdf/src/main/resources/messages_en_GB.properties")) {
|
||||
console.log("Using PR branch reference file.");
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: prRepoOwner,
|
||||
repo: prRepoName,
|
||||
path: "src/main/resources/messages_en_GB.properties",
|
||||
path: "stirling-pdf/src/main/resources/messages_en_GB.properties",
|
||||
ref: branch,
|
||||
});
|
||||
|
||||
@@ -158,7 +174,7 @@ jobs:
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
path: "src/main/resources/messages_en_GB.properties",
|
||||
path: "stirling-pdf/src/main/resources/messages_en_GB.properties",
|
||||
ref: "main",
|
||||
});
|
||||
|
||||
@@ -253,3 +269,12 @@ jobs:
|
||||
run: |
|
||||
echo "Failing the job because errors were detected."
|
||||
exit 1
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -rf pr-branch
|
||||
rm -f pr-branch-messages_en_GB.properties main-branch-messages_en_GB.properties changed_files.txt result.txt
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
@@ -57,11 +57,11 @@ jobs:
|
||||
|
||||
- name: Move and rename license file
|
||||
run: |
|
||||
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
|
||||
mv build/reports/dependency-license/index.json stirling-pdf/src/main/resources/static/3rdPartyLicenses.json
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git add src/main/resources/static/3rdPartyLicenses.json
|
||||
git add stirling-pdf/src/main/resources/static/3rdPartyLicenses.json
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
title: "Update 3rd Party Licenses"
|
||||
body: |
|
||||
Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||
labels: licenses,github-actions
|
||||
labels: Licenses,github-actions
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -21,42 +21,46 @@ jobs:
|
||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Get version number
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
# ✅ Get version from Gradle
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# ✅ Get Mac-specific version from Gradle
|
||||
- name: Get version number mac
|
||||
id: versionNumberMac
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
CURRENT_YEAR=$(date +'%Y')
|
||||
IFS='.' read -r -a VERSION_PARTS <<< "$VERSION"
|
||||
MAC_VERSION="$CURRENT_YEAR.${VERSION_PARTS[1]:-0}.${VERSION_PARTS[2]:-0}"
|
||||
echo "versionNumberMac=$MAC_VERSION" >> $GITHUB_OUTPUT
|
||||
VERSION_MAC=$(./gradlew printMacVersion --quiet | tail -1)
|
||||
echo "versionNumberMac=$VERSION_MAC" >> $GITHUB_OUTPUT
|
||||
|
||||
build-portable:
|
||||
needs: read_versions
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
enable_security: [true, false]
|
||||
disable_security: [true, false]
|
||||
include:
|
||||
- enable_security: true
|
||||
- disable_security: false
|
||||
file_suffix: "-with-login"
|
||||
- enable_security: false
|
||||
- disable_security: true
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -68,21 +72,21 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||
- name: Generate jar (Disable Security=${{ matrix.disable_security }})
|
||||
run: ./gradlew clean createExe
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.disable_security }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Rename binaries
|
||||
run: |
|
||||
mkdir ./binaries
|
||||
mv ./build/launch4j/Stirling-PDF.exe ./binaries/win-Stirling-PDF-portable-Server${{ matrix.file_suffix }}.exe
|
||||
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
mv ./stirling-pdf/build/libs/stirling-pdf-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -98,15 +102,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
enable_security: [true, false]
|
||||
disable_security: [true, false]
|
||||
include:
|
||||
- enable_security: true
|
||||
- disable_security: false
|
||||
file_suffix: "with-login-"
|
||||
- enable_security: false
|
||||
- disable_security: true
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -144,7 +148,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -156,7 +160,7 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -171,7 +175,7 @@ jobs:
|
||||
- name: Build Installer
|
||||
run: ./gradlew build jpackage -x test --info
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
BROWSER_OPEN: true
|
||||
|
||||
@@ -234,7 +238,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -248,7 +252,7 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
|
||||
- name: Generate key pair
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -297,7 +301,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -306,7 +310,7 @@ jobs:
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
with:
|
||||
tag_name: v${{ needs.read_versions.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -30,25 +30,25 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -77,6 +77,7 @@ jobs:
|
||||
- name: Generate tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -86,11 +87,11 @@ jobs:
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push main Dockerfile
|
||||
id: build-push-regular
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
@@ -153,7 +154,6 @@ jobs:
|
||||
- name: Generate tags fat
|
||||
id: meta3
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -163,11 +163,11 @@ jobs:
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push main Dockerfile fat
|
||||
id: build-push-fat
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
|
||||
@@ -13,17 +13,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
enable_security: [true, false]
|
||||
disable_security: [true, false]
|
||||
include:
|
||||
- enable_security: true
|
||||
- disable_security: false
|
||||
file_suffix: "-with-login"
|
||||
- enable_security: false
|
||||
- disable_security: true
|
||||
file_suffix: ""
|
||||
outputs:
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -35,14 +35,14 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||
- name: Generate jar (Disable Security=${{ matrix.disable_security }})
|
||||
run: ./gradlew clean createExe
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.disable_security }}
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Get version number
|
||||
@@ -75,15 +75,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
enable_security: [true, false]
|
||||
disable_security: [true, false]
|
||||
include:
|
||||
- enable_security: true
|
||||
- disable_security: false
|
||||
file_suffix: "-with-login"
|
||||
- enable_security: false
|
||||
- disable_security: true
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
run: ls -R
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
|
||||
- name: Generate key pair
|
||||
run: cosign generate-key-pair
|
||||
@@ -153,15 +153,15 @@ jobs:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
enable_security: [true, false]
|
||||
disable_security: [true, false]
|
||||
include:
|
||||
- enable_security: true
|
||||
- disable_security: false
|
||||
file_suffix: "-with-login"
|
||||
- enable_security: false
|
||||
- disable_security: true
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
name: signed${{ matrix.file_suffix }}
|
||||
|
||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
with:
|
||||
tag_name: v${{ needs.build.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -74,6 +74,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -27,13 +27,13 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Build and analyze with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
DOCKER_ENABLE_SECURITY: true
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
run: |
|
||||
./gradlew clean build sonar \
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew generateOpenApiDocs
|
||||
|
||||
@@ -8,8 +8,8 @@ on:
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "src/main/resources/messages_*.properties"
|
||||
- "src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "stirling-pdf/src/main/resources/messages_*.properties"
|
||||
- "stirling-pdf/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
permissions:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -41,11 +41,11 @@ jobs:
|
||||
|
||||
- name: Sync translation property files
|
||||
run: |
|
||||
python .github/scripts/check_language_properties.py --reference-file "src/main/resources/messages_en_GB.properties" --branch main
|
||||
python .github/scripts/check_language_properties.py --reference-file "stirling-pdf/src/main/resources/messages_en_GB.properties" --branch main
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add src/main/resources/messages_*.properties
|
||||
git add stirling-pdf/src/main/resources/messages_*.properties
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -57,8 +57,8 @@ jobs:
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
git add README.md
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "No changes detected"
|
||||
git add README.md scripts/ignore_translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
@@ -101,4 +101,4 @@ jobs:
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
src/main/resources/messages_*.properties
|
||||
stirling-pdf/src/main/resources/messages_*.properties
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
- /stirling/test-${{ github.sha }}/config:/configs:rw
|
||||
- /stirling/test-${{ github.sha }}/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF Test"
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
+4
-1
@@ -13,6 +13,7 @@ local.properties
|
||||
.recommenders
|
||||
.classpath
|
||||
.project
|
||||
*.local.json
|
||||
version.properties
|
||||
|
||||
#### Stirling-PDF Files ###
|
||||
@@ -124,6 +125,9 @@ SwaggerDoc.json
|
||||
*.rar
|
||||
*.db
|
||||
/build
|
||||
/stirling-pdf/build
|
||||
/common/build
|
||||
/proprietary/build
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -193,4 +197,3 @@ id_ed25519.pub
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
*.mjs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.11
|
||||
rev: v0.12.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
@@ -16,13 +16,13 @@ repos:
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list=
|
||||
- --ignore-words-list=thirdParty,tabEl,tabEls
|
||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||
- --quiet-level=2
|
||||
files: \.(html|css|js|py|md)$
|
||||
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||
exclude: (.vscode|.devcontainer|stirling-pdf/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.26.0
|
||||
rev: v8.27.2
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
||||
Vendored
+2
-1
@@ -15,6 +15,7 @@
|
||||
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
|
||||
"GitHub.copilot", // GitHub Copilot AI pair programmer for Visual Studio Code
|
||||
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
|
||||
"charliermarsh.ruff" // Ruff code formatter for Python to follow the Ruff Style Guide
|
||||
"charliermarsh.ruff", // Ruff code formatter for Python to follow the Ruff Style Guide
|
||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+41
@@ -6,6 +6,27 @@
|
||||
"[java]": {
|
||||
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"[gradle-kotlin-dsl]": {
|
||||
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
||||
},
|
||||
"[gradle-build]": {
|
||||
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||
},
|
||||
"[gradle]": {
|
||||
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||
},
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"java.format.enabled": true,
|
||||
@@ -50,8 +71,10 @@
|
||||
".vscode/",
|
||||
"bin/",
|
||||
"common/bin/",
|
||||
"proprietary/bin/",
|
||||
"build/",
|
||||
"common/build/",
|
||||
"proprietary/build/",
|
||||
"configs/",
|
||||
"customFiles/",
|
||||
"docs/",
|
||||
@@ -66,6 +89,7 @@
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
"common/.gitignore",
|
||||
"proprietary/.gitignore",
|
||||
".pre-commit-config.yaml",
|
||||
],
|
||||
// Enables signature help in Java.
|
||||
@@ -83,4 +107,21 @@
|
||||
"spring.initializr.defaultLanguage": "Java",
|
||||
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
|
||||
"spring.initializr.defaultArtifactId": "SPDF",
|
||||
"java.jdt.ls.lombokSupport.enabled": true,
|
||||
"html.format.wrapLineLength": 127,
|
||||
"html.format.enable": true,
|
||||
"html.format.indentInnerHtml": true,
|
||||
"html.format.unformatted": "script,style,textarea",
|
||||
"html.format.contentUnformatted": "pre,code",
|
||||
"html.format.extraLiners": "head,body,/html",
|
||||
"html.format.wrapAttributes": "force",
|
||||
"html.format.wrapAttributesIndentSize": 2,
|
||||
"html.format.indentHandlebars": true,
|
||||
"html.format.preserveNewLines": true,
|
||||
"html.format.maxPreserveNewLines": 2,
|
||||
"java.project.sourcePaths": [
|
||||
"stirling-pdf/src/main/java",
|
||||
"common/src/main/java",
|
||||
"proprietary/src/main/java"
|
||||
]
|
||||
}
|
||||
|
||||
+13
-2
@@ -25,7 +25,7 @@ Please make sure your Pull Request adheres to the following guidelines:
|
||||
|
||||
## Translations
|
||||
|
||||
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
||||
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](devGuide/HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
||||
|
||||
## Docs
|
||||
|
||||
@@ -37,7 +37,18 @@ First, make sure you've read the section [Pull Requests](#pull-requests).
|
||||
|
||||
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||
|
||||
Developers should review our [Developer Guide](DeveloperGuide.md)
|
||||
## Developer Documentation
|
||||
|
||||
For technical guides, setup instructions, and development resources, please see our [Developer Documentation](devGuide/) which includes:
|
||||
|
||||
- [Developer Guide](devGuide/DeveloperGuide.md) - Main setup and architecture guide
|
||||
- [Exception Handling Guide](devGuide/EXCEPTION_HANDLING_GUIDE.md) - Error handling patterns and i18n
|
||||
- [Translation Guide](devGuide/HowToAddNewLanguage.md) - Adding new languages
|
||||
- And more in the [devGuide folder](devGuide/)
|
||||
|
||||
For configuration and usage guides, see:
|
||||
- [Database Guide](DATABASE.md) - Database setup and configuration
|
||||
- [OCR Guide](HowToUseOCR.md) - OCR setup and configuration
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+18
-10
@@ -4,9 +4,8 @@ FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be02
|
||||
# Copy necessary files
|
||||
COPY scripts /scripts
|
||||
COPY pipeline /pipeline
|
||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||
COPY build/libs/*.jar app.jar
|
||||
COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
COPY stirling-pdf/build/libs/*.jar app.jar
|
||||
|
||||
ARG VERSION_TAG
|
||||
|
||||
@@ -23,7 +22,7 @@ LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark"
|
||||
|
||||
# Set Environment Variables
|
||||
ENV DOCKER_ENABLE_SECURITY=false \
|
||||
ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||
VERSION_TAG=$VERSION_TAG \
|
||||
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
||||
JAVA_CUSTOM_OPTS="" \
|
||||
@@ -34,7 +33,11 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
||||
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
|
||||
UNO_PATH=/usr/lib/libreoffice/program \
|
||||
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
|
||||
PATH=$PATH:/opt/venv/bin
|
||||
PATH=$PATH:/opt/venv/bin \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
|
||||
# JDK for app
|
||||
@@ -48,7 +51,6 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
tini \
|
||||
bash \
|
||||
curl \
|
||||
qpdf \
|
||||
shadow \
|
||||
su-exec \
|
||||
openssl \
|
||||
@@ -66,12 +68,16 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
tesseract-ocr-data-deu \
|
||||
tesseract-ocr-data-fra \
|
||||
tesseract-ocr-data-por \
|
||||
unpaper \
|
||||
# CV
|
||||
py3-opencv \
|
||||
python3 \
|
||||
ocrmypdf \
|
||||
py3-pip \
|
||||
py3-pillow@testing \
|
||||
py3-pdf2image@testing && \
|
||||
py3-pdf2image@testing \
|
||||
# URW Base 35 fonts for better PDF rendering
|
||||
font-urw-base35 && \
|
||||
python3 -m venv /opt/venv && \
|
||||
/opt/venv/bin/pip install --upgrade pip setuptools && \
|
||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||
@@ -79,17 +85,19 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
|
||||
# Configure URW Base 35 fonts
|
||||
ln -s /usr/share/fontconfig/conf.avail/69-urw-*.conf /etc/fonts/conf.d/ && \
|
||||
fc-cache -f -v && \
|
||||
chmod +x /scripts/* && \
|
||||
chmod +x /scripts/init.sh && \
|
||||
# User permissions
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
# Set user and run command
|
||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
|
||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
|
||||
|
||||
+10
-4
@@ -19,7 +19,7 @@ RUN apt-get update && apt-get install -y \
|
||||
# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine \
|
||||
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine fonts-urw-base35 \
|
||||
python3-uno \
|
||||
python3-venv \
|
||||
# ss -tln
|
||||
@@ -27,7 +27,11 @@ RUN apt-get update && apt-get install -y \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setze die Environment Variable für setuptools
|
||||
ENV SETUPTOOLS_USE_DISTUTILS=local
|
||||
ENV SETUPTOOLS_USE_DISTUTILS=local \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
# Installation der benötigten Python-Pakete
|
||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||
@@ -40,8 +44,10 @@ ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY . /workspace
|
||||
|
||||
RUN adduser --disabled-password --gecos '' devuser \
|
||||
&& chown -R devuser:devuser /home/devuser /workspace
|
||||
RUN mkdir -p /tmp/stirling-pdf \
|
||||
&& fc-cache -f -v \
|
||||
&& adduser --disabled-password --gecos '' devuser \
|
||||
&& chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf
|
||||
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
|
||||
&& chmod 0440 /etc/sudoers.d/devuser
|
||||
|
||||
|
||||
+21
-11
@@ -5,6 +5,9 @@ COPY build.gradle .
|
||||
COPY settings.gradle .
|
||||
COPY gradlew .
|
||||
COPY gradle gradle/
|
||||
COPY stirling-pdf/build.gradle stirling-pdf/.
|
||||
COPY common/build.gradle common/.
|
||||
COPY proprietary/build.gradle proprietary/.
|
||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
||||
|
||||
# Set the working directory
|
||||
@@ -13,8 +16,8 @@ WORKDIR /app
|
||||
# Copy the entire project to the working directory
|
||||
COPY . .
|
||||
|
||||
# Build the application with DOCKER_ENABLE_SECURITY=false
|
||||
RUN DOCKER_ENABLE_SECURITY=true \
|
||||
# Build the application with DISABLE_ADDITIONAL_FEATURES=false
|
||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
||||
STIRLING_PDF_DESKTOP_UI=false \
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
@@ -24,13 +27,13 @@ FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be02
|
||||
# Copy necessary files
|
||||
COPY scripts /scripts
|
||||
COPY pipeline /pipeline
|
||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
COPY --from=build /app/build/libs/*.jar app.jar
|
||||
COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
COPY --from=build /app/stirling-pdf/build/libs/*.jar app.jar
|
||||
|
||||
ARG VERSION_TAG
|
||||
|
||||
# Set Environment Variables
|
||||
ENV DOCKER_ENABLE_SECURITY=false \
|
||||
ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||
VERSION_TAG=$VERSION_TAG \
|
||||
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
||||
JAVA_CUSTOM_OPTS="" \
|
||||
@@ -43,7 +46,11 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
||||
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
|
||||
UNO_PATH=/usr/lib/libreoffice/program \
|
||||
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
|
||||
PATH=$PATH:/opt/venv/bin
|
||||
PATH=$PATH:/opt/venv/bin \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
|
||||
# JDK for app
|
||||
@@ -69,16 +76,17 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
# pdftohtml
|
||||
poppler-utils \
|
||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||
qpdf \
|
||||
tesseract-ocr-data-eng \
|
||||
tesseract-ocr-data-chi_sim \
|
||||
tesseract-ocr-data-deu \
|
||||
tesseract-ocr-data-fra \
|
||||
tesseract-ocr-data-por \
|
||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \
|
||||
unpaper \
|
||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \
|
||||
# CV
|
||||
py3-opencv \
|
||||
python3 \
|
||||
ocrmypdf \
|
||||
py3-pip \
|
||||
py3-pillow@testing \
|
||||
py3-pdf2image@testing && \
|
||||
@@ -89,16 +97,18 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
|
||||
# Configure URW Base 35 fonts
|
||||
ln -s /usr/share/fontconfig/conf.avail/69-urw-*.conf /etc/fonts/conf.d/ && \
|
||||
fc-cache -f -v && \
|
||||
chmod +x /scripts/* && \
|
||||
chmod +x /scripts/init.sh && \
|
||||
# User permissions
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
# Set user and run command
|
||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
|
||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
|
||||
|
||||
+10
-6
@@ -4,21 +4,25 @@ FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be02
|
||||
ARG VERSION_TAG
|
||||
|
||||
# Set Environment Variables
|
||||
ENV DOCKER_ENABLE_SECURITY=false \
|
||||
ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||
HOME=/home/stirlingpdfuser \
|
||||
VERSION_TAG=$VERSION_TAG \
|
||||
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
||||
JAVA_CUSTOM_OPTS="" \
|
||||
PUID=1000 \
|
||||
PGID=1000 \
|
||||
UMASK=022
|
||||
UMASK=022 \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
# Copy necessary files
|
||||
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
||||
COPY pipeline /pipeline
|
||||
COPY build/libs/*.jar app.jar
|
||||
COPY stirling-pdf/build/libs/*.jar app.jar
|
||||
|
||||
# Set up necessary directories and permissions
|
||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||
@@ -35,10 +39,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
su-exec \
|
||||
openjdk21-jre && \
|
||||
# User permissions
|
||||
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \
|
||||
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf && \
|
||||
chmod +x /scripts/*.sh && \
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
|
||||
# Set environment variables
|
||||
@@ -48,4 +52,4 @@ EXPOSE 8080/tcp
|
||||
|
||||
# Run the application
|
||||
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||
CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"]
|
||||
@@ -1,6 +1,13 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Stirling Tools
|
||||
Copyright (c) 2025 Stirling PDF Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "proprietary/LICENSE".
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the MIT License as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -29,7 +29,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
||||
- API for integration with external scripts
|
||||
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation)
|
||||
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
|
||||
- Enterprise features like SSO see [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||
- Enterprise features like SSO (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration) for documentation)
|
||||
|
||||
## PDF Features
|
||||
|
||||
@@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@@ -168,7 +168,7 @@ Check out our [Enterprise docs](https://docs.stirlingpdf.com/Pro)
|
||||
|
||||
Join our community:
|
||||
- [Contribution Guidelines](CONTRIBUTING.md)
|
||||
- [Translation Guide (How to add custom languages)](HowToAddNewLanguage.md)
|
||||
- [Translation Guide (How to add custom languages)](devGuide/HowToAddNewLanguage.md)
|
||||
- [Developer Guide](devGuide/DeveloperGuide.md)
|
||||
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
||||
- [Developer Guide](DeveloperGuide.md)
|
||||
|
||||
@@ -124,10 +124,18 @@
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License 1.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License - v 1.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License v2.0"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "Eclipse Public License v. 2.0"
|
||||
|
||||
+225
-250
@@ -1,46 +1,171 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "jacoco"
|
||||
id "org.springframework.boot" version "3.5.0"
|
||||
id "io.spring.dependency-management" version "1.1.7"
|
||||
id "org.springframework.boot" version "3.5.3"
|
||||
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
|
||||
id "io.swagger.swaggerhub" version "1.3.2"
|
||||
id "edu.sc.seis.launch4j" version "3.0.6"
|
||||
id "com.diffplug.spotless" version "7.0.4"
|
||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||
//id "nebula.lint" version "19.0.3"
|
||||
id("org.panteleyev.jpackageplugin") version "1.6.1"
|
||||
id "org.panteleyev.jpackageplugin" version "1.7.3"
|
||||
id "org.sonarqube" version "6.2.0.5505"
|
||||
}
|
||||
|
||||
import com.github.jk1.license.render.*
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
import org.panteleyev.jpackage.ImageType
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.time.Year
|
||||
|
||||
ext {
|
||||
springBootVersion = "3.5.0"
|
||||
springBootVersion = "3.5.3"
|
||||
pdfboxVersion = "3.0.5"
|
||||
imageioVersion = "3.12.0"
|
||||
lombokVersion = "1.18.38"
|
||||
bouncycastleVersion = "1.80"
|
||||
springSecuritySamlVersion = "6.5.0"
|
||||
bouncycastleVersion = "1.81"
|
||||
springSecuritySamlVersion = "6.5.1"
|
||||
openSamlVersion = "4.3.2"
|
||||
commonmarkVersion = "0.25.0"
|
||||
googleJavaFormatVersion = "1.27.0"
|
||||
tempJrePath = null
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.46.2"
|
||||
jar {
|
||||
enabled = false
|
||||
manifest {
|
||||
attributes "Implementation-Title": "Stirling-PDF",
|
||||
"Implementation-Version": project.version
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
// Configure main class for the root project
|
||||
springBoot {
|
||||
mainClass = 'stirling.software.SPDF.SPDFApplication'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = "https://build.shibboleth.net/maven/releases" }
|
||||
maven { url = "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
|
||||
maven { url = 'https://build.shibboleth.net/maven/releases' }
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '1.0.2'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('writeVersion') {
|
||||
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
||||
def propsDir = propsFile.parentFile
|
||||
|
||||
doLast {
|
||||
if (propsDir.exists()) {
|
||||
if (propsFile.exists()) {
|
||||
println "File exists: $propsFile"
|
||||
} else {
|
||||
println "$propsFile does not exist. Creating file."
|
||||
propsFile.createNewFile()
|
||||
}
|
||||
} else {
|
||||
println "Creating directory: $propsDir"
|
||||
propsDir.mkdirs()
|
||||
propsFile.createNewFile()
|
||||
}
|
||||
|
||||
def props = new Properties()
|
||||
props.setProperty("version", version)
|
||||
props.store(propsFile.newWriter(), null)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('createExe') {
|
||||
dependsOn(":stirling-pdf:bootJar")
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'com.diffplug.spotless'
|
||||
apply plugin: 'org.springframework.boot'
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
if (project.name != "stirling-pdf") {
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
// Exclude vulnerable BouncyCastle version used in tableau
|
||||
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersion"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'io.github.pixee:java-security-toolkit:1.2.2'
|
||||
|
||||
//tmp for security bumps
|
||||
implementation 'ch.qos.logback:logback-core:1.5.18'
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.18'
|
||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
dependsOn "spotlessApply"
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.compilerArgs << "-parameters"
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(rootProject.tasks.writeVersion)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
dependsOn "spotlessApply"
|
||||
}
|
||||
|
||||
licenseReport {
|
||||
@@ -51,29 +176,14 @@ licenseReport {
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
||||
exclude "stirling/software/SPDF/config/interfaces/DatabaseInterface.java"
|
||||
exclude "stirling/software/SPDF/config/security/**"
|
||||
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
|
||||
exclude "stirling/software/SPDF/controller/api/EmailController.java"
|
||||
exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java"
|
||||
exclude "stirling/software/SPDF/controller/api/UserController.java"
|
||||
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
||||
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
||||
exclude "stirling/software/SPDF/model/api/Email.java"
|
||||
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
||||
exclude "stirling/software/SPDF/model/AttemptCounter.java"
|
||||
exclude "stirling/software/SPDF/model/Authority.java"
|
||||
exclude "stirling/software/SPDF/model/exception/BackupNotFoundException.java"
|
||||
exclude "stirling/software/SPDF/model/exception/NoProviderFoundException.java"
|
||||
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
||||
exclude "stirling/software/SPDF/model/SessionEntity.java"
|
||||
exclude "stirling/software/SPDF/model/User.java"
|
||||
exclude "stirling/software/SPDF/repository/**"
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
||||
exclude 'stirling/software/proprietary/security/**'
|
||||
}
|
||||
|
||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
||||
exclude "stirling/software/SPDF/UI/impl/**"
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
|
||||
}
|
||||
@@ -81,15 +191,14 @@ sourceSets {
|
||||
|
||||
test {
|
||||
java {
|
||||
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
||||
exclude "stirling/software/SPDF/config/security/**"
|
||||
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java"
|
||||
exclude "stirling/software/SPDF/controller/api/EmailControllerTest.java"
|
||||
exclude "stirling/software/SPDF/repository/**"
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
||||
exclude 'stirling/software/proprietary/security/**'
|
||||
}
|
||||
|
||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
||||
exclude "stirling/software/SPDF/UI/impl/**"
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +211,14 @@ openApi {
|
||||
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
|
||||
}
|
||||
|
||||
// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
|
||||
tasks.named('forkedSpringBootRun') {
|
||||
dependsOn ':stirling-pdf:bootRun'
|
||||
doFirst {
|
||||
println "Delegating forkedSpringBootRun to :stirling-pdf:bootRun"
|
||||
}
|
||||
}
|
||||
|
||||
//0.11.5 to 2024.11.5
|
||||
static def getMacVersion(String version) {
|
||||
def currentYear = Year.now().getValue()
|
||||
@@ -110,15 +227,15 @@ static def getMacVersion(String version) {
|
||||
}
|
||||
|
||||
jpackage {
|
||||
input = "build/libs"
|
||||
destination = "${projectDir}/build/jpackage"
|
||||
dependsOn(":stirling-pdf:bootJar")
|
||||
input = layout.projectDirectory.dir("stirling-pdf/build/libs")
|
||||
destination = layout.projectDirectory.dir("build/jpackage")
|
||||
mainJar = "Stirling-PDF-${project.version}.jar"
|
||||
appName = "Stirling PDF"
|
||||
appVersion = project.version
|
||||
// appVersion = "2005.45.1"
|
||||
vendor = "Stirling PDF Inc"
|
||||
appDescription = "Stirling PDF - Your Local PDF Editor"
|
||||
icon = "src/main/resources/static/favicon.ico"
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.ico")
|
||||
verbose = true
|
||||
// mainClass = "org.springframework.boot.loader.launch.JarLauncher"
|
||||
|
||||
@@ -126,6 +243,7 @@ jpackage {
|
||||
javaOptions = [
|
||||
"-DBROWSER_OPEN=true",
|
||||
"-DSTIRLING_PDF_DESKTOP_UI=true",
|
||||
"-DDISABLE_ADDITIONAL_FEATURES=false",
|
||||
"-Djava.awt.headless=false",
|
||||
"-Dapple.awt.UIElement=true",
|
||||
"--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
@@ -152,15 +270,15 @@ jpackage {
|
||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||
type = "exe"
|
||||
type = ImageType.EXE
|
||||
installDir = "C:/Program Files/Stirling-PDF"
|
||||
}
|
||||
|
||||
// macOS-specific configuration
|
||||
// MacOS-specific configuration
|
||||
mac {
|
||||
appVersion = getMacVersion(project.version.toString())
|
||||
icon = "src/main/resources/static/favicon.icns"
|
||||
type = "dmg"
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.icns")
|
||||
type = ImageType.DMG
|
||||
macPackageIdentifier = "Stirling PDF"
|
||||
macPackageName = "Stirling PDF"
|
||||
macAppCategory = "public.app-category.productivity"
|
||||
@@ -181,8 +299,8 @@ jpackage {
|
||||
// Linux-specific configuration
|
||||
linux {
|
||||
appVersion = project.version
|
||||
icon = "src/main/resources/static/favicon.png"
|
||||
type = "deb" // Can also use "rpm" for Red Hat-based systems
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.png")
|
||||
type = ImageType.DEB // Can also use "rpm" for Red Hat-based systems
|
||||
|
||||
// Debian package configuration
|
||||
//linuxPackageName = "stirlingpdf"
|
||||
@@ -217,10 +335,15 @@ jpackage {
|
||||
]*/
|
||||
|
||||
// Add copyright and license information
|
||||
copyright = "Copyright © 2024 Stirling Software"
|
||||
licenseFile = "LICENSE"
|
||||
copyright = "Copyright © 2025 Stirling PDF Inc."
|
||||
licenseFile = layout.projectDirectory.file("LICENSE")
|
||||
}
|
||||
|
||||
//tasks.wrapper {
|
||||
// gradleVersion = "8.14"
|
||||
// distributionType = Wrapper.DistributionType.ALL
|
||||
//}
|
||||
|
||||
tasks.register('jpackageMacX64') {
|
||||
group = 'distribution'
|
||||
description = 'Packages app for MacOS x86_64'
|
||||
@@ -248,12 +371,12 @@ tasks.register('jpackageMacX64') {
|
||||
commandLine 'jpackage',
|
||||
'--type', 'dmg',
|
||||
'--name', 'Stirling PDF (x86_64)',
|
||||
'--input', 'build/libs',
|
||||
'--input', 'stirling-pdf/build/libs',
|
||||
'--main-jar', "Stirling-PDF-${project.version}.jar",
|
||||
'--main-class', 'org.springframework.boot.loader.launch.JarLauncher',
|
||||
'--runtime-image', file(jrePath + "/zulu-17.jre/Contents/Home"),
|
||||
'--dest', 'build/jpackage/x86_64',
|
||||
'--icon', 'src/main/resources/static/favicon.icns',
|
||||
'--icon', 'stirling-pdf/src/main/resources/static/favicon.icns',
|
||||
'--app-version', getMacVersion(project.version.toString()),
|
||||
'--mac-package-name', 'Stirling PDF (x86_64)',
|
||||
'--mac-package-identifier', 'Stirling PDF (x86_64)',
|
||||
@@ -262,6 +385,7 @@ tasks.register('jpackageMacX64') {
|
||||
// Java options
|
||||
'--java-options', '-DBROWSER_OPEN=true',
|
||||
'--java-options', '-DSTIRLING_PDF_DESKTOP_UI=true',
|
||||
'--java-options', '-DDISABLE_ADDITIONAL_FEATURES=false',
|
||||
'--java-options', '-Djava.awt.headless=false',
|
||||
'--java-options', '-Dapple.awt.UIElement=true',
|
||||
'--java-options', '--add-opens=java.base/java.lang=ALL-UNNAMED',
|
||||
@@ -290,8 +414,6 @@ tasks.register('jpackageMacX64') {
|
||||
}
|
||||
}
|
||||
|
||||
//jpackage.finalizedBy(jpackageMacX64)
|
||||
|
||||
tasks.register('downloadTempJre') {
|
||||
group = 'distribution'
|
||||
description = 'Downloads and extracts a temporary JRE'
|
||||
@@ -303,18 +425,18 @@ tasks.register('downloadTempJre') {
|
||||
def jreArchive = new File(tmpDir, 'jre.tar.gz')
|
||||
def jreDir = new File(tmpDir, 'jre')
|
||||
|
||||
println "🔽 Downloading JRE to $jreArchive..."
|
||||
println "Downloading JRE to $jreArchive"
|
||||
jreArchive.withOutputStream { out ->
|
||||
new URI(jreUrl).toURL().withInputStream { from -> out << from }
|
||||
}
|
||||
|
||||
println "📦 Extracting JRE to $jreDir..."
|
||||
println "Extracting JRE to $jreDir"
|
||||
jreDir.mkdirs()
|
||||
providers.exec {
|
||||
commandLine 'tar', '-xzf', jreArchive.absolutePath, '-C', jreDir.absolutePath, '--strip-components=1'
|
||||
}.result.get()
|
||||
|
||||
println "✅ JRE ready at: $jreDir"
|
||||
println "JRE ready at: $jreDir"
|
||||
ext.tempJrePath = jreDir.absolutePath
|
||||
project.ext.tempJrePath = jreDir.absolutePath
|
||||
} catch (Exception e) {
|
||||
@@ -340,7 +462,7 @@ tasks.register('cleanTempJre') {
|
||||
}
|
||||
|
||||
launch4j {
|
||||
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||
icon = "${projectDir}/stirling-pdf/src/main/resources/static/favicon.ico"
|
||||
|
||||
outfile="Stirling-PDF.exe"
|
||||
|
||||
@@ -349,9 +471,9 @@ launch4j {
|
||||
} else {
|
||||
headerType = "console"
|
||||
}
|
||||
jarTask = tasks.bootJar
|
||||
jarTask = project(":stirling-pdf").tasks.bootJar
|
||||
|
||||
errTitle="Encountered error, Do you have Java 21?"
|
||||
errTitle="Encountered error, do you have Java 21?"
|
||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||
|
||||
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
|
||||
@@ -376,8 +498,10 @@ spotless {
|
||||
java {
|
||||
target sourceSets.main.allJava
|
||||
target project(':common').sourceSets.main.allJava
|
||||
target project(':proprietary').sourceSets.main.allJava
|
||||
target project(':stirling-pdf').sourceSets.main.allJava
|
||||
|
||||
googleJavaFormat("1.27.0").aosp().reorderImports(false)
|
||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||
|
||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||
toggleOffOn()
|
||||
@@ -392,189 +516,12 @@ sonar {
|
||||
property "sonar.projectKey", "Stirling-Tools_Stirling-PDF"
|
||||
property "sonar.organization", "stirling-tools"
|
||||
|
||||
property "sonar.exclusions", "**/build-wrapper-dump.json, src/main/java/org/apache/**, src/main/resources/static/pdfjs/**, src/main/resources/static/pdfjs-legacy/**, src/main/resources/static/js/thirdParty/**"
|
||||
property "sonar.coverage.exclusions", "src/main/java/org/apache/**, src/main/resources/static/pdfjs/**, src/main/resources/static/pdfjs-legacy/**, src/main/resources/static/js/thirdParty/**"
|
||||
property "sonar.cpd.exclusions", "src/main/java/org/apache/**, src/main/resources/static/pdfjs/**, src/main/resources/static/pdfjs-legacy/**, src/main/resources/static/js/thirdParty/**"
|
||||
property "sonar.exclusions", "**/build-wrapper-dump.json, **/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
|
||||
property "sonar.coverage.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
|
||||
property "sonar.cpd.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
|
||||
}
|
||||
}
|
||||
|
||||
//gradleLint {
|
||||
// rules=['unused-dependency']
|
||||
// }
|
||||
tasks.wrapper {
|
||||
gradleVersion = "8.14"
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
}
|
||||
//tasks.withType(JavaCompile) {
|
||||
// options.compilerArgs << "-Xlint:deprecation"
|
||||
//}
|
||||
configurations.all {
|
||||
// Remove all commons-logging dependencies so that only spring-jcl is used
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
// Exclude Tomcat
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
|
||||
//tmp for security bumps
|
||||
implementation 'ch.qos.logback:logback-core:1.5.18'
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.18'
|
||||
|
||||
// Exclude vulnerable BouncyCastle version used in tableau
|
||||
configurations.all {
|
||||
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
|
||||
}
|
||||
|
||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
|
||||
implementation "me.friwi:jcefmaven:135.0.20"
|
||||
implementation "org.openjfx:javafx-controls:21"
|
||||
implementation "org.openjfx:javafx-swing:21"
|
||||
}
|
||||
|
||||
//security updates
|
||||
implementation "org.springframework:spring-webmvc:6.2.7"
|
||||
|
||||
implementation("io.github.pixee:java-security-toolkit:1.2.1")
|
||||
|
||||
// Exclude Tomcat and include Jetty
|
||||
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
|
||||
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
||||
|
||||
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
||||
implementation 'com.posthog.java:posthog:1.2.0'
|
||||
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
|
||||
|
||||
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||
implementation 'io.micrometer:micrometer-registry-prometheus'
|
||||
|
||||
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-mail:$springBootVersion"
|
||||
|
||||
implementation "org.springframework.session:spring-session-core:3.5.0"
|
||||
implementation "org.springframework:spring-jdbc:6.2.7"
|
||||
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||
// Don't upgrade h2database
|
||||
runtimeOnly "com.h2database:h2:2.3.232"
|
||||
runtimeOnly "org.postgresql:postgresql:42.7.6"
|
||||
constraints {
|
||||
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
|
||||
}
|
||||
implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
|
||||
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
||||
implementation 'com.coveo:saml-client:5.0.0'
|
||||
|
||||
}
|
||||
implementation 'org.snakeyaml:snakeyaml-engine:2.9'
|
||||
|
||||
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
// Batik
|
||||
implementation "org.apache.xmlgraphics:batik-all:1.19"
|
||||
|
||||
// TwelveMonkeys
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
|
||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
||||
|
||||
// Image metadata extractor
|
||||
implementation "com.drewnoakes:metadata-extractor:2.19.0"
|
||||
|
||||
implementation "commons-io:commons-io:2.19.0"
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8"
|
||||
//general PDF
|
||||
|
||||
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||
implementation ("com.opencsv:opencsv:5.11")
|
||||
|
||||
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion")
|
||||
implementation "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
|
||||
|
||||
implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion")
|
||||
|
||||
// https://mvnrepository.com/artifact/technology.tabula/tabula
|
||||
implementation ('technology.tabula:tabula:1.0.5') {
|
||||
exclude group: "org.slf4j", module: "slf4j-simple"
|
||||
exclude group: "org.bouncycastle", module: "bcprov-jdk15on"
|
||||
exclude group: "com.google.code.gson", module: "gson"
|
||||
exclude group: "commons-io", module: "commons-io"
|
||||
}
|
||||
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||
implementation "io.micrometer:micrometer-core:1.15.0"
|
||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||
implementation "org.commonmark:commonmark:0.24.0"
|
||||
implementation "org.commonmark:commonmark-ext-gfm-tables:0.24.0"
|
||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
|
||||
implementation "com.fathzer:javaluator:3.0.6"
|
||||
|
||||
implementation 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
|
||||
// Mockito (core)
|
||||
testImplementation 'org.mockito:mockito-core:5.18.0'
|
||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
dependsOn "spotlessApply"
|
||||
}
|
||||
compileJava {
|
||||
options.compilerArgs << "-parameters"
|
||||
}
|
||||
|
||||
task writeVersion {
|
||||
def propsFile = file("$projectDir/src/main/resources/version.properties")
|
||||
def propsDir = propsFile.parentFile
|
||||
|
||||
doLast {
|
||||
if (!propsDir.exists()) {
|
||||
propsDir.mkdirs()
|
||||
}
|
||||
|
||||
def props = new Properties()
|
||||
props.setProperty("version", version)
|
||||
props.store(propsFile.newWriter(), null)
|
||||
}
|
||||
}
|
||||
|
||||
processResources.dependsOn(writeVersion)
|
||||
|
||||
swaggerhubUpload {
|
||||
// dependsOn = generateOpenApiDocs // Depends on your task generating Swagger docs
|
||||
api = "Stirling-PDF" // The name of your API on SwaggerHub
|
||||
@@ -585,25 +532,34 @@ swaggerhubUpload {
|
||||
oas = "3.0.0" // The version of the OpenAPI Specification you"re using
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
manifest {
|
||||
attributes "Implementation-Title": "Stirling-PDF",
|
||||
"Implementation-Version": project.version
|
||||
dependencies {
|
||||
implementation project(':stirling-pdf')
|
||||
implementation project(':common')
|
||||
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
|
||||
implementation project(':proprietary')
|
||||
}
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
task printVersion {
|
||||
|
||||
// Make sure all relevant processes depend on writeVersion
|
||||
processResources.dependsOn(writeVersion)
|
||||
|
||||
tasks.register('printVersion') {
|
||||
doLast {
|
||||
println project.version
|
||||
}
|
||||
}
|
||||
|
||||
task printMacVersion {
|
||||
tasks.register('printMacVersion') {
|
||||
doLast {
|
||||
println getMacVersion(project.version.toString())
|
||||
}
|
||||
@@ -612,3 +568,22 @@ task printMacVersion {
|
||||
tasks.named('generateOpenApiDocs') {
|
||||
doNotTrackState("Tracking state is not supported for this task")
|
||||
}
|
||||
tasks.named('bootRun') {
|
||||
group = 'application'
|
||||
description = 'Delegates to :stirling-pdf:bootRun'
|
||||
dependsOn ':stirling-pdf:bootRun'
|
||||
|
||||
doFirst {
|
||||
println "Delegating to :stirling-pdf:bootRun"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('build') {
|
||||
group = 'build'
|
||||
description = 'Delegates to :stirling-pdf:bootJar'
|
||||
dependsOn ':stirling-pdf:bootJar'
|
||||
|
||||
doFirst {
|
||||
println "Delegating to :stirling-pdf:bootJar"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -124,6 +124,7 @@ SwaggerDoc.json
|
||||
*.rar
|
||||
*.db
|
||||
/build
|
||||
/common/build/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -193,4 +194,3 @@ id_ed25519.pub
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
*.mjs
|
||||
|
||||
+27
-59
@@ -1,65 +1,33 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
// Configure bootRun to disable it or point to a main class
|
||||
bootRun {
|
||||
enabled = false
|
||||
}
|
||||
spotless {
|
||||
java {
|
||||
target sourceSets.main.allJava
|
||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||
|
||||
group = 'stirling.software'
|
||||
version = '0.46.2'
|
||||
|
||||
ext {
|
||||
lombokVersion = "1.18.38"
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom 'org.springframework.boot:spring-boot-dependencies:3.5.0'
|
||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||
toggleOffOn()
|
||||
trimTrailingWhitespace()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-aop'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
implementation 'com.fathzer:javaluator:3.0.6'
|
||||
implementation 'com.posthog.java:posthog:1.2.0'
|
||||
implementation 'io.github.pixee:java-security-toolkit:1.2.1'
|
||||
implementation 'org.apache.commons:commons-lang3:3.17.0'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||
implementation 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
implementation "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
implementation 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
implementation 'org.snakeyaml:snakeyaml-engine:2.9'
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8"
|
||||
|
||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
|
||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||
testImplementation 'org.mockito:mockito-core:5.18.0'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0'
|
||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
}
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
api 'org.apache.commons:commons-lang3:3.17.0'
|
||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.9'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.3'
|
||||
}
|
||||
|
||||
@@ -6,42 +6,73 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
/**
|
||||
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
||||
*
|
||||
* <p>Behaviour notes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The endpoint is registered with {@code POST} and, by default, consumes {@code
|
||||
* multipart/form-data} unless you override {@link #consumes()}.
|
||||
* <li>When the client supplies {@code ?async=true} the call is handed to {@link
|
||||
* stirling.software.common.service.JobExecutorService JobExecutorService} where it may be
|
||||
* queued, retried, tracked and subject to time‑outs. For synchronous (default) invocations
|
||||
* these advanced options are ignored.
|
||||
* <li>Progress information (see {@link #trackProgress()}) is stored in {@link
|
||||
* stirling.software.common.service.TaskManager TaskManager} and can be polled via <code>
|
||||
* GET /api/v1/general/job/{id}</code>.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public @interface AutoJobPostMapping {
|
||||
|
||||
/** Alias for {@link RequestMapping#value} – the path mapping of the endpoint. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "value")
|
||||
String[] value() default {};
|
||||
|
||||
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||
String[] consumes() default {"multipart/form-data"};
|
||||
|
||||
/**
|
||||
* Custom timeout in milliseconds for this specific job. If not specified, the default system
|
||||
* timeout will be used.
|
||||
* Maximum execution time in milliseconds before the job is aborted. A negative value means "use
|
||||
* the application default".
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
long timeout() default -1;
|
||||
|
||||
/** Maximum number of times to retry the job on failure. Default is 1 (no retries). */
|
||||
/**
|
||||
* Total number of attempts (initial + retries). Must be at least 1. Retries are executed
|
||||
* with exponential back‑off.
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
int retryCount() default 1;
|
||||
|
||||
/**
|
||||
* Whether to track and report progress for this job. If enabled, the job will send progress
|
||||
* updates through WebSocket.
|
||||
* Record percentage / note updates so they can be retrieved via the REST status endpoint.
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
boolean trackProgress() default true;
|
||||
|
||||
/**
|
||||
* Whether this job can be queued when system resources are limited. If enabled, jobs will be
|
||||
* queued instead of rejected when the system is under high load. The queue size is dynamically
|
||||
* adjusted based on available memory and CPU resources.
|
||||
* If {@code true} the job may be placed in a queue instead of being rejected when resources are
|
||||
* scarce.
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
boolean queueable() default false;
|
||||
|
||||
/**
|
||||
* Optional resource weight of this job (1-100). Higher values indicate more resource-intensive
|
||||
* jobs that may need stricter queuing. Default is 50 (medium weight).
|
||||
* Relative resource weight (1–100) used by the scheduler to prioritise / throttle jobs. Values
|
||||
* below 1 are clamped to 1, values above 100 to 100.
|
||||
*/
|
||||
int resourceWeight() default 50;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package stirling.software.common.aop;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -15,9 +18,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
@@ -26,17 +27,20 @@ import stirling.software.common.service.JobExecutorService;
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Order(0) // Highest precedence - executes before audit aspects
|
||||
public class AutoJobAspect {
|
||||
|
||||
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
|
||||
|
||||
private final JobExecutorService jobExecutorService;
|
||||
private final HttpServletRequest request;
|
||||
private final FileOrUploadService fileOrUploadService;
|
||||
private final FileStorage fileStorage;
|
||||
private final WebSocketProgressController webSocketSender;
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
public Object wrapWithJobExecution(
|
||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
||||
// This aspect will run before any audit aspects due to @Order(0)
|
||||
// Extract parameters from the request and annotation
|
||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||
long timeout = autoJobPostMapping.timeout();
|
||||
@@ -50,70 +54,34 @@ public class AutoJobAspect {
|
||||
retryCount,
|
||||
trackProgress);
|
||||
|
||||
// Inspect and possibly mutate arguments
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object arg = args[i];
|
||||
|
||||
if (arg instanceof PDFFile pdfFile) {
|
||||
// Case 1: fileId is provided but no fileInput
|
||||
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
||||
try {
|
||||
log.debug("Using fileId {} to get file content", pdfFile.getFileId());
|
||||
MultipartFile file = fileStorage.retrieveFile(pdfFile.getFileId());
|
||||
pdfFile.setFileInput(file);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
||||
}
|
||||
}
|
||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
||||
else if (async && pdfFile.getFileInput() != null) {
|
||||
try {
|
||||
log.debug("Making persistent copy of uploaded file for async processing");
|
||||
MultipartFile originalFile = pdfFile.getFileInput();
|
||||
String fileId = fileStorage.storeFile(originalFile);
|
||||
|
||||
// Store the fileId for later reference
|
||||
pdfFile.setFileId(fileId);
|
||||
|
||||
// Replace the original MultipartFile with our persistent copy
|
||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
||||
pdfFile.setFileInput(persistentFile);
|
||||
|
||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to create persistent copy of uploaded file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy and process arguments
|
||||
// In a test environment, we might need to update the original objects for verification
|
||||
boolean isTestEnvironment = false;
|
||||
try {
|
||||
isTestEnvironment = Class.forName("org.junit.jupiter.api.Test") != null;
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Not in a test environment
|
||||
}
|
||||
|
||||
// Extract queueable and resourceWeight parameters
|
||||
boolean queueable = autoJobPostMapping.queueable();
|
||||
int resourceWeight = autoJobPostMapping.resourceWeight();
|
||||
Object[] args =
|
||||
isTestEnvironment
|
||||
? processArgsInPlace(joinPoint.getArgs(), async)
|
||||
: copyAndProcessArgs(joinPoint.getArgs(), async);
|
||||
|
||||
// Integrate with the enhanced JobExecutorService
|
||||
// Extract queueable and resourceWeight parameters and validate
|
||||
boolean queueable = autoJobPostMapping.queueable();
|
||||
int resourceWeight = Math.max(1, Math.min(100, autoJobPostMapping.resourceWeight()));
|
||||
|
||||
// Integrate with the JobExecutorService
|
||||
if (retryCount <= 1) {
|
||||
// No retries needed, simple execution
|
||||
return jobExecutorService.runJobGeneric(
|
||||
async,
|
||||
() -> {
|
||||
try {
|
||||
if (trackProgress && async) {
|
||||
String jobId = (String) request.getAttribute("jobId");
|
||||
if (jobId != null) {
|
||||
webSocketSender.sendProgress(
|
||||
jobId,
|
||||
new JobProgress(
|
||||
jobId,
|
||||
"Processing",
|
||||
50,
|
||||
"Executing operation"));
|
||||
}
|
||||
}
|
||||
// Note: Progress tracking is handled in TaskManager/JobExecutorService
|
||||
// The trackProgress flag controls whether detailed progress is stored
|
||||
// for REST API queries, not WebSocket notifications
|
||||
return joinPoint.proceed(args);
|
||||
} catch (Throwable ex) {
|
||||
log.error(
|
||||
@@ -150,94 +118,242 @@ public class AutoJobAspect {
|
||||
boolean queueable,
|
||||
int resourceWeight) {
|
||||
|
||||
AtomicInteger attempts = new AtomicInteger(0);
|
||||
// Use AtomicReference to make the jobId effectively final for lambda usage
|
||||
// Keep jobId reference for progress tracking in TaskManager
|
||||
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
||||
|
||||
return jobExecutorService.runJobGeneric(
|
||||
async,
|
||||
() -> {
|
||||
int currentAttempt = attempts.incrementAndGet();
|
||||
try {
|
||||
if (trackProgress && async) {
|
||||
// Only get the jobId once and store it in the AtomicReference
|
||||
if (jobIdRef.get() == null) {
|
||||
jobIdRef.set(getJobIdFromContext());
|
||||
}
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
webSocketSender.sendProgress(
|
||||
jobId,
|
||||
new JobProgress(
|
||||
jobId,
|
||||
"Started",
|
||||
0,
|
||||
"Executing (attempt "
|
||||
+ currentAttempt
|
||||
+ " of "
|
||||
+ Math.max(1, maxRetries)
|
||||
+ ")"));
|
||||
}
|
||||
}
|
||||
// Use iterative approach instead of recursion to avoid stack overflow
|
||||
Throwable lastException = null;
|
||||
|
||||
return joinPoint.proceed(args);
|
||||
} catch (Throwable ex) {
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
||||
currentAttempt,
|
||||
Math.max(1, maxRetries),
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
|
||||
// Check if we should retry
|
||||
if (currentAttempt < maxRetries) {
|
||||
log.info(
|
||||
"Retrying operation, attempt {}/{}",
|
||||
currentAttempt + 1,
|
||||
maxRetries);
|
||||
String jobId = jobIdRef.get();
|
||||
if (trackProgress && async && jobId != null) {
|
||||
webSocketSender.sendProgress(
|
||||
jobId,
|
||||
new JobProgress(
|
||||
jobId,
|
||||
"Retrying",
|
||||
(int) (currentAttempt * 100.0 / maxRetries),
|
||||
"Retry attempt "
|
||||
+ (currentAttempt + 1)
|
||||
+ " of "
|
||||
+ maxRetries));
|
||||
// Attempt counter starts at 1 for first try
|
||||
for (int currentAttempt = 1; currentAttempt <= maxRetries; currentAttempt++) {
|
||||
try {
|
||||
if (trackProgress && async) {
|
||||
// Get jobId for progress tracking in TaskManager
|
||||
// This enables REST API progress queries, not WebSocket
|
||||
if (jobIdRef.get() == null) {
|
||||
jobIdRef.set(getJobIdFromContext());
|
||||
}
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
log.debug(
|
||||
"Tracking progress for job {} (attempt {}/{})",
|
||||
jobId,
|
||||
currentAttempt,
|
||||
maxRetries);
|
||||
// Progress is tracked in TaskManager for REST API access
|
||||
// No WebSocket notifications sent here
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Simple exponential backoff
|
||||
Thread.sleep(100 * currentAttempt);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
// Attempt to execute the operation
|
||||
return joinPoint.proceed(args);
|
||||
|
||||
// Recursive call to retry
|
||||
return executeWithRetries(
|
||||
joinPoint,
|
||||
args,
|
||||
async,
|
||||
timeout,
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
||||
currentAttempt,
|
||||
maxRetries,
|
||||
trackProgress,
|
||||
queueable,
|
||||
resourceWeight);
|
||||
}
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
|
||||
// No more retries, throw the exception
|
||||
throw new RuntimeException("Job failed: " + ex.getMessage(), ex);
|
||||
// Check if we should retry
|
||||
if (currentAttempt < maxRetries) {
|
||||
log.info(
|
||||
"Retrying operation, attempt {}/{}",
|
||||
currentAttempt + 1,
|
||||
maxRetries);
|
||||
|
||||
if (trackProgress && async) {
|
||||
String jobId = jobIdRef.get();
|
||||
if (jobId != null) {
|
||||
log.debug(
|
||||
"Recording retry attempt for job {} in TaskManager",
|
||||
jobId);
|
||||
// Retry info is tracked in TaskManager for REST API access
|
||||
}
|
||||
}
|
||||
|
||||
// Use non-blocking delay for all retry attempts to avoid blocking
|
||||
// threads
|
||||
// For sync jobs this avoids starving the tomcat thread pool under
|
||||
// load
|
||||
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
|
||||
|
||||
// Execute the retry after a delay through the JobExecutorService
|
||||
// rather than blocking the current thread with sleep
|
||||
CompletableFuture<Object> delayedRetry = new CompletableFuture<>();
|
||||
|
||||
// Use a delayed executor for non-blocking delay
|
||||
CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
|
||||
.execute(
|
||||
() -> {
|
||||
// Continue the retry loop in the next iteration
|
||||
// We can't return from here directly since
|
||||
// we're in a Runnable
|
||||
delayedRetry.complete(null);
|
||||
});
|
||||
|
||||
// Wait for the delay to complete before continuing
|
||||
try {
|
||||
delayedRetry.join();
|
||||
} catch (Exception e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// No more retries, we'll throw the exception after the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all retries failed
|
||||
if (lastException != null) {
|
||||
throw new RuntimeException(
|
||||
"Job failed after "
|
||||
+ maxRetries
|
||||
+ " attempts: "
|
||||
+ lastException.getMessage(),
|
||||
lastException);
|
||||
}
|
||||
|
||||
// This should never happen if lastException is properly tracked
|
||||
throw new RuntimeException("Job failed but no exception was recorded");
|
||||
},
|
||||
timeout,
|
||||
queueable,
|
||||
resourceWeight);
|
||||
}
|
||||
|
||||
// Try to get the job ID from the context (if this is an async job)
|
||||
/**
|
||||
* Creates deep copies of arguments when needed to avoid mutating the original objects
|
||||
* Particularly important for PDFFile objects that might be reused by Spring
|
||||
*
|
||||
* @param originalArgs The original arguments
|
||||
* @param async Whether this is an async operation
|
||||
* @return A new array with safely processed arguments
|
||||
*/
|
||||
private Object[] copyAndProcessArgs(Object[] originalArgs, boolean async) {
|
||||
if (originalArgs == null || originalArgs.length == 0) {
|
||||
return originalArgs;
|
||||
}
|
||||
|
||||
Object[] processedArgs = new Object[originalArgs.length];
|
||||
|
||||
// Copy all arguments
|
||||
for (int i = 0; i < originalArgs.length; i++) {
|
||||
Object arg = originalArgs[i];
|
||||
|
||||
if (arg instanceof PDFFile pdfFile) {
|
||||
// Create a copy of PDFFile to avoid mutating the original
|
||||
// Using direct property access instead of reflection for better performance
|
||||
PDFFile pdfFileCopy = new PDFFile();
|
||||
pdfFileCopy.setFileId(pdfFile.getFileId());
|
||||
pdfFileCopy.setFileInput(pdfFile.getFileInput());
|
||||
|
||||
// Case 1: fileId is provided but no fileInput
|
||||
if (pdfFileCopy.getFileInput() == null && pdfFileCopy.getFileId() != null) {
|
||||
try {
|
||||
log.debug("Using fileId {} to get file content", pdfFileCopy.getFileId());
|
||||
MultipartFile file = fileStorage.retrieveFile(pdfFileCopy.getFileId());
|
||||
pdfFileCopy.setFileInput(file);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to resolve file by ID: " + pdfFileCopy.getFileId(), e);
|
||||
}
|
||||
}
|
||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
||||
else if (async && pdfFileCopy.getFileInput() != null) {
|
||||
try {
|
||||
log.debug("Making persistent copy of uploaded file for async processing");
|
||||
MultipartFile originalFile = pdfFileCopy.getFileInput();
|
||||
String fileId = fileStorage.storeFile(originalFile);
|
||||
|
||||
// Store the fileId for later reference
|
||||
pdfFileCopy.setFileId(fileId);
|
||||
|
||||
// Replace the original MultipartFile with our persistent copy
|
||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
||||
pdfFileCopy.setFileInput(persistentFile);
|
||||
|
||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to create persistent copy of uploaded file", e);
|
||||
}
|
||||
}
|
||||
|
||||
processedArgs[i] = pdfFileCopy;
|
||||
} else {
|
||||
// For non-PDFFile objects, just pass the original reference
|
||||
// If other classes need copy-on-write, add them here
|
||||
processedArgs[i] = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return processedArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes arguments in-place for testing purposes This is similar to our original
|
||||
* implementation before introducing copy-on-write It's only used in test environments to
|
||||
* maintain test compatibility
|
||||
*
|
||||
* @param originalArgs The original arguments
|
||||
* @param async Whether this is an async operation
|
||||
* @return The original array with processed arguments
|
||||
*/
|
||||
private Object[] processArgsInPlace(Object[] originalArgs, boolean async) {
|
||||
if (originalArgs == null || originalArgs.length == 0) {
|
||||
return originalArgs;
|
||||
}
|
||||
|
||||
// Process all arguments in-place
|
||||
for (int i = 0; i < originalArgs.length; i++) {
|
||||
Object arg = originalArgs[i];
|
||||
|
||||
if (arg instanceof PDFFile pdfFile) {
|
||||
// Case 1: fileId is provided but no fileInput
|
||||
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
||||
try {
|
||||
log.debug("Using fileId {} to get file content", pdfFile.getFileId());
|
||||
MultipartFile file = fileStorage.retrieveFile(pdfFile.getFileId());
|
||||
pdfFile.setFileInput(file);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
||||
}
|
||||
}
|
||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
||||
else if (async && pdfFile.getFileInput() != null) {
|
||||
try {
|
||||
log.debug("Making persistent copy of uploaded file for async processing");
|
||||
MultipartFile originalFile = pdfFile.getFileInput();
|
||||
String fileId = fileStorage.storeFile(originalFile);
|
||||
|
||||
// Store the fileId for later reference
|
||||
pdfFile.setFileId(fileId);
|
||||
|
||||
// Replace the original MultipartFile with our persistent copy
|
||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
||||
pdfFile.setFileInput(persistentFile);
|
||||
|
||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to create persistent copy of uploaded file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return originalArgs;
|
||||
}
|
||||
|
||||
private String getJobIdFromContext() {
|
||||
try {
|
||||
return (String) request.getAttribute("jobId");
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package stirling.software.common.config;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Configuration for the temporary file management system. Sets up the necessary beans and
|
||||
* configures system properties.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class TempFileConfiguration {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
/**
|
||||
* Create the TempFileRegistry bean.
|
||||
*
|
||||
* @return A new TempFileRegistry instance
|
||||
*/
|
||||
@Bean
|
||||
public TempFileRegistry tempFileRegistry() {
|
||||
return new TempFileRegistry();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initTempFileConfig() {
|
||||
try {
|
||||
ApplicationProperties.TempFileManagement tempFiles =
|
||||
applicationProperties.getSystem().getTempFileManagement();
|
||||
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||
|
||||
// Create the temp directory if it doesn't exist
|
||||
Path tempDir = Path.of(customTempDirectory);
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
log.info("Created temporary directory: {}", tempDir);
|
||||
}
|
||||
|
||||
log.debug("Temporary file configuration initialized");
|
||||
log.debug("Using temp directory: {}", customTempDirectory);
|
||||
log.debug("Temp file prefix: {}", tempFiles.getPrefix());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initialize temporary file configuration", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package stirling.software.common.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Handles cleanup of temporary files on application shutdown. Implements Spring's DisposableBean
|
||||
* interface to ensure cleanup happens during normal application shutdown.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TempFileShutdownHook implements DisposableBean {
|
||||
|
||||
private final TempFileRegistry registry;
|
||||
|
||||
@Autowired
|
||||
public TempFileShutdownHook(TempFileRegistry registry) {
|
||||
this.registry = registry;
|
||||
|
||||
// Register a JVM shutdown hook as a backup in case Spring's
|
||||
// DisposableBean mechanism doesn't trigger (e.g., during a crash)
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanupTempFiles));
|
||||
}
|
||||
|
||||
/** Spring's DisposableBean interface method. Called during normal application shutdown. */
|
||||
@Override
|
||||
public void destroy() {
|
||||
log.info("Application shutting down, cleaning up temporary files");
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
/** Clean up all registered temporary files and directories. */
|
||||
private void cleanupTempFiles() {
|
||||
try {
|
||||
// Clean up all registered files
|
||||
Set<Path> files = registry.getAllRegisteredFiles();
|
||||
int deletedCount = 0;
|
||||
|
||||
for (Path file : files) {
|
||||
try {
|
||||
if (Files.exists(file)) {
|
||||
Files.deleteIfExists(file);
|
||||
deletedCount++;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp file during shutdown: {}", file, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up all registered directories
|
||||
Set<Path> directories = registry.getTempDirectories();
|
||||
for (Path dir : directories) {
|
||||
try {
|
||||
if (Files.exists(dir)) {
|
||||
GeneralUtils.deleteDirectory(dir);
|
||||
deletedCount++;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp directory during shutdown: {}", dir, e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Shutdown cleanup complete. Deleted {} temporary files/directories",
|
||||
deletedCount);
|
||||
|
||||
// Clear the registry
|
||||
registry.clear();
|
||||
} catch (Exception e) {
|
||||
log.error("Error during shutdown cleanup", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package stirling.software.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker("/topic");
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,17 @@ import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import lombok.Getter;
|
||||
@@ -146,10 +147,11 @@ public class AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
||||
@Bean(name = "activeSecurity")
|
||||
public boolean missingActiveSecurity() {
|
||||
return false;
|
||||
return ClassUtils.isPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
|
||||
this.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Bean(name = "directoryFilter")
|
||||
@@ -236,9 +238,33 @@ public class AppConfig {
|
||||
return applicationProperties.getSystem().getDatasource();
|
||||
}
|
||||
|
||||
@Bean(name = "runningProOrHigher")
|
||||
@Profile("default")
|
||||
public boolean runningProOrHigher() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "runningEE")
|
||||
@Profile("default")
|
||||
public boolean runningEnterprise() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "GoogleDriveEnabled")
|
||||
@Profile("default")
|
||||
public boolean googleDriveEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "license")
|
||||
@Profile("default")
|
||||
public String licenseType() {
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
@Bean(name = "disablePixel")
|
||||
public boolean disablePixel() {
|
||||
return Boolean.getBoolean(env.getProperty("DISABLE_PIXEL"));
|
||||
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
|
||||
}
|
||||
|
||||
@Bean(name = "machineType")
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package stirling.software.common.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.TaskManager;
|
||||
|
||||
/** REST controller for job-related endpoints */
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class JobController {
|
||||
|
||||
private final TaskManager taskManager;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
/**
|
||||
* Get the status of a job
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}")
|
||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of a job
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}/result")
|
||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!result.isComplete()) {
|
||||
return ResponseEntity.badRequest().body("Job is not complete yet");
|
||||
}
|
||||
|
||||
if (result.getError() != null) {
|
||||
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||
}
|
||||
|
||||
if (result.getFileId() != null) {
|
||||
try {
|
||||
byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Type", result.getContentType())
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
"form-data; name=\"attachment\"; filename=\""
|
||||
+ result.getOriginalFileName()
|
||||
+ "\"")
|
||||
.body(fileContent);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Error retrieving file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result.getResult());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about jobs in the system
|
||||
*
|
||||
* @return Job statistics
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/stats")
|
||||
public ResponseEntity<JobStats> getJobStats() {
|
||||
JobStats stats = taskManager.getJobStats();
|
||||
return ResponseEntity.ok(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger cleanup of old jobs
|
||||
*
|
||||
* @return A response indicating how many jobs were cleaned up
|
||||
*/
|
||||
@PostMapping("/api/v1/general/job/cleanup")
|
||||
public ResponseEntity<?> cleanupOldJobs() {
|
||||
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
||||
taskManager.cleanupOldJobs();
|
||||
int afterCount = taskManager.getJobStats().getTotalJobs();
|
||||
int removedCount = beforeCount - afterCount;
|
||||
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"message", "Cleanup complete",
|
||||
"removedJobs", removedCount,
|
||||
"remainingJobs", afterCount));
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package stirling.software.common.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
|
||||
@Controller
|
||||
@NoArgsConstructor
|
||||
public class WebSocketProgressController {
|
||||
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
public void sendProgress(String jobId, JobProgress progress) {
|
||||
if (messagingTemplate != null) {
|
||||
messagingTemplate.convertAndSend("/topic/progress/" + jobId, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,6 +292,7 @@ public class ApplicationProperties {
|
||||
private Boolean enableUrlToPDF;
|
||||
private CustomPaths customPaths = new CustomPaths();
|
||||
private String fileUploadLimit;
|
||||
private TempFileManagement tempFileManagement = new TempFileManagement();
|
||||
|
||||
public boolean isAnalyticsEnabled() {
|
||||
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
|
||||
@@ -317,6 +318,30 @@ public class ApplicationProperties {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TempFileManagement {
|
||||
private String baseTmpDir = "";
|
||||
private String libreofficeDir = "";
|
||||
private String systemTempDir = "";
|
||||
private String prefix = "stirling-pdf-";
|
||||
private long maxAgeHours = 24;
|
||||
private long cleanupIntervalMinutes = 30;
|
||||
private boolean startupCleanup = true;
|
||||
private boolean cleanupSystemTemp = false;
|
||||
|
||||
public String getBaseTmpDir() {
|
||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
||||
? baseTmpDir
|
||||
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
|
||||
}
|
||||
|
||||
public String getLibreofficeDir() {
|
||||
return libreofficeDir != null && !libreofficeDir.isEmpty()
|
||||
? libreofficeDir
|
||||
: getBaseTmpDir() + "/libreoffice";
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Datasource {
|
||||
private boolean enableCustomDatabase;
|
||||
@@ -344,10 +369,10 @@ public class ApplicationProperties {
|
||||
@Override
|
||||
public String toString() {
|
||||
return """
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
.formatted(driverName);
|
||||
}
|
||||
}
|
||||
@@ -442,6 +467,7 @@ public class ApplicationProperties {
|
||||
@Data
|
||||
public static class ProFeatures {
|
||||
private boolean ssoAutoLogin;
|
||||
private boolean database;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
private GoogleDrive googleDrive = new GoogleDrive();
|
||||
|
||||
@@ -487,6 +513,14 @@ public class ApplicationProperties {
|
||||
@Data
|
||||
public static class EnterpriseFeatures {
|
||||
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
||||
private Audit audit = new Audit();
|
||||
|
||||
@Data
|
||||
public static class Audit {
|
||||
private boolean enabled = true;
|
||||
private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE
|
||||
private int retentionDays = 90;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PersistentMetrics {
|
||||
@@ -511,6 +545,8 @@ public class ApplicationProperties {
|
||||
private int calibreSessionLimit;
|
||||
private int qpdfSessionLimit;
|
||||
private int tesseractSessionLimit;
|
||||
private int ghostscriptSessionLimit;
|
||||
private int ocrMyPdfSessionLimit;
|
||||
|
||||
public int getQpdfSessionLimit() {
|
||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||
@@ -543,6 +579,14 @@ public class ApplicationProperties {
|
||||
public int getCalibreSessionLimit() {
|
||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getGhostscriptSessionLimit() {
|
||||
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
||||
}
|
||||
|
||||
public int getOcrMyPdfSessionLimit() {
|
||||
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -555,6 +599,8 @@ public class ApplicationProperties {
|
||||
private long calibreTimeoutMinutes;
|
||||
private long tesseractTimeoutMinutes;
|
||||
private long qpdfTimeoutMinutes;
|
||||
private long ghostscriptTimeoutMinutes;
|
||||
private long ocrMyPdfTimeoutMinutes;
|
||||
|
||||
public long getTesseractTimeoutMinutes() {
|
||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||
@@ -587,6 +633,14 @@ public class ApplicationProperties {
|
||||
public long getCalibreTimeoutMinutes() {
|
||||
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getGhostscriptTimeoutMinutes() {
|
||||
return ghostscriptTimeoutMinutes > 0 ? ghostscriptTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getOcrMyPdfTimeoutMinutes() {
|
||||
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package stirling.software.common.model.api.converters;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class EmlToPdfRequest extends PDFFile {
|
||||
|
||||
// fileInput is inherited from PDFFile
|
||||
|
||||
@Schema(
|
||||
description = "Include email attachments in the PDF output",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||
example = "false")
|
||||
private boolean includeAttachments = false;
|
||||
|
||||
@Schema(
|
||||
description = "Maximum attachment size in MB to include (default 10MB, range: 1-100)",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||
example = "10",
|
||||
minimum = "1",
|
||||
maximum = "100")
|
||||
private int maxAttachmentSizeMB = 10;
|
||||
|
||||
@Schema(
|
||||
description = "Download HTML intermediate file instead of PDF",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||
example = "false")
|
||||
private boolean downloadHtml = false;
|
||||
|
||||
@Schema(
|
||||
description = "Include CC and BCC recipients in header (if available)",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||
example = "true")
|
||||
private boolean includeAllRecipients = true;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -23,14 +27,8 @@ public class JobResult {
|
||||
/** Error message if the job failed */
|
||||
private String error;
|
||||
|
||||
/** The file ID of the result file, if applicable */
|
||||
private String fileId;
|
||||
|
||||
/** Original file name, if applicable */
|
||||
private String originalFileName;
|
||||
|
||||
/** MIME type of the result, if applicable */
|
||||
private String contentType;
|
||||
/** List of result files for jobs that produce files */
|
||||
private List<ResultFile> resultFiles;
|
||||
|
||||
/** Time when the job was created */
|
||||
private LocalDateTime createdAt;
|
||||
@@ -41,6 +39,12 @@ public class JobResult {
|
||||
/** The actual result object, if not a file */
|
||||
private Object result;
|
||||
|
||||
/**
|
||||
* Notes attached to this job for tracking purposes. Uses CopyOnWriteArrayList for thread safety
|
||||
* when notes are added concurrently.
|
||||
*/
|
||||
private final List<String> notes = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Create a new JobResult with the given job ID
|
||||
*
|
||||
@@ -55,21 +59,6 @@ public class JobResult {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a file result
|
||||
*
|
||||
* @param fileId The file ID of the result
|
||||
* @param originalFileName The original file name
|
||||
* @param contentType The content type of the file
|
||||
*/
|
||||
public void completeWithFile(String fileId, String originalFileName, String contentType) {
|
||||
this.complete = true;
|
||||
this.fileId = fileId;
|
||||
this.originalFileName = originalFileName;
|
||||
this.contentType = contentType;
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a general result
|
||||
*
|
||||
@@ -91,4 +80,83 @@ public class JobResult {
|
||||
this.error = error;
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with multiple file results
|
||||
*
|
||||
* @param resultFiles The list of result files
|
||||
*/
|
||||
public void completeWithFiles(List<ResultFile> resultFiles) {
|
||||
this.complete = true;
|
||||
this.resultFiles = new ArrayList<>(resultFiles);
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a single file result (convenience method)
|
||||
*
|
||||
* @param fileId The file ID of the result
|
||||
* @param fileName The file name
|
||||
* @param contentType The content type of the file
|
||||
* @param fileSize The size of the file in bytes
|
||||
*/
|
||||
public void completeWithSingleFile(
|
||||
String fileId, String fileName, String contentType, long fileSize) {
|
||||
ResultFile resultFile =
|
||||
ResultFile.builder()
|
||||
.fileId(fileId)
|
||||
.fileName(fileName)
|
||||
.contentType(contentType)
|
||||
.fileSize(fileSize)
|
||||
.build();
|
||||
completeWithFiles(List.of(resultFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this job has file results
|
||||
*
|
||||
* @return true if this job has file results, false otherwise
|
||||
*/
|
||||
public boolean hasFiles() {
|
||||
return resultFiles != null && !resultFiles.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this job has multiple file results
|
||||
*
|
||||
* @return true if this job has multiple file results, false otherwise
|
||||
*/
|
||||
public boolean hasMultipleFiles() {
|
||||
return resultFiles != null && resultFiles.size() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all result files
|
||||
*
|
||||
* @return List of result files
|
||||
*/
|
||||
public List<ResultFile> getAllResultFiles() {
|
||||
if (resultFiles != null && !resultFiles.isEmpty()) {
|
||||
return Collections.unmodifiableList(resultFiles);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note to this job
|
||||
*
|
||||
* @param note The note to add
|
||||
*/
|
||||
public void addNote(String note) {
|
||||
this.notes.add(note);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notes attached to this job
|
||||
*
|
||||
* @return An unmodifiable view of the notes list
|
||||
*/
|
||||
public List<String> getNotes() {
|
||||
return Collections.unmodifiableList(notes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Represents a single file result from a job execution */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ResultFile {
|
||||
|
||||
/** The file ID for accessing the file */
|
||||
private String fileId;
|
||||
|
||||
/** The original file name */
|
||||
private String fileName;
|
||||
|
||||
/** MIME type of the file */
|
||||
private String contentType;
|
||||
|
||||
/** Size of the file in bytes */
|
||||
private long fileSize;
|
||||
}
|
||||
+77
-9
@@ -23,6 +23,10 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Adaptive PDF document factory that optimizes memory usage based on file size and available system
|
||||
@@ -79,7 +83,7 @@ public class CustomPDFDocumentFactory {
|
||||
*/
|
||||
public PDDocument load(File file, boolean readOnly) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("File cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("File");
|
||||
}
|
||||
|
||||
long fileSize = file.length();
|
||||
@@ -106,7 +110,7 @@ public class CustomPDFDocumentFactory {
|
||||
*/
|
||||
public PDDocument load(Path path, boolean readOnly) throws IOException {
|
||||
if (path == null) {
|
||||
throw new IllegalArgumentException("File cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("File");
|
||||
}
|
||||
|
||||
long fileSize = Files.size(path);
|
||||
@@ -127,7 +131,7 @@ public class CustomPDFDocumentFactory {
|
||||
/** Load a PDF from byte array with automatic optimization and read-only option. */
|
||||
public PDDocument load(byte[] input, boolean readOnly) throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("Input bytes cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("Input bytes");
|
||||
}
|
||||
|
||||
long dataSize = input.length;
|
||||
@@ -148,7 +152,7 @@ public class CustomPDFDocumentFactory {
|
||||
/** Load a PDF from InputStream with automatic optimization and read-only option. */
|
||||
public PDDocument load(InputStream input, boolean readOnly) throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("InputStream cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("InputStream");
|
||||
}
|
||||
|
||||
// Since we don't know the size upfront, buffer to a temp file
|
||||
@@ -171,7 +175,7 @@ public class CustomPDFDocumentFactory {
|
||||
public PDDocument load(InputStream input, String password, boolean readOnly)
|
||||
throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("InputStream cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("InputStream");
|
||||
}
|
||||
|
||||
// Since we don't know the size upfront, buffer to a temp file
|
||||
@@ -289,9 +293,32 @@ public class CustomPDFDocumentFactory {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||
}
|
||||
|
||||
configureResourceCacheIfNeeded(document, contentSize);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure resource cache based on content size and memory constraints. Disables resource
|
||||
* cache for large files or when memory is low to prevent OOM errors.
|
||||
*/
|
||||
private void configureResourceCacheIfNeeded(PDDocument document, long contentSize) {
|
||||
if (contentSize > LARGE_FILE_THRESHOLD) {
|
||||
document.setResourceCache(null);
|
||||
} else {
|
||||
// Check current memory status for smaller files
|
||||
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||
long usedMemory =
|
||||
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
double freeMemoryPercent = (double) (maxMemory - usedMemory) / maxMemory * 100;
|
||||
|
||||
if (freeMemoryPercent < MIN_FREE_MEMORY_PERCENTAGE) {
|
||||
document.setResourceCache(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Load a PDF with password protection using adaptive loading strategies */
|
||||
private PDDocument loadAdaptivelyWithPassword(Object source, long contentSize, String password)
|
||||
throws IOException {
|
||||
@@ -310,6 +337,9 @@ public class CustomPDFDocumentFactory {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||
}
|
||||
|
||||
configureResourceCacheIfNeeded(document, contentSize);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
@@ -351,7 +381,12 @@ public class CustomPDFDocumentFactory {
|
||||
|
||||
private PDDocument loadFromFile(File file, long size, StreamCacheCreateFunction cache)
|
||||
throws IOException {
|
||||
return Loader.loadPDF(new DeletingRandomAccessFile(file), "", null, null, cache);
|
||||
try {
|
||||
return Loader.loadPDF(new DeletingRandomAccessFile(file), "", null, null, cache);
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF loading from file", e);
|
||||
throw ExceptionUtils.handlePdfException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private PDDocument loadFromBytes(byte[] bytes, long size, StreamCacheCreateFunction cache)
|
||||
@@ -363,7 +398,13 @@ public class CustomPDFDocumentFactory {
|
||||
Files.write(tempFile, bytes);
|
||||
return loadFromFile(tempFile.toFile(), size, cache);
|
||||
}
|
||||
return Loader.loadPDF(bytes, "", null, null, cache);
|
||||
|
||||
try {
|
||||
return Loader.loadPDF(bytes, "", null, null, cache);
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF loading from bytes", e);
|
||||
throw ExceptionUtils.handlePdfException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PDDocument createNewDocument(MemoryUsageSetting settings) throws IOException {
|
||||
@@ -396,16 +437,43 @@ public class CustomPDFDocumentFactory {
|
||||
try {
|
||||
document.setAllSecurityToBeRemoved(true);
|
||||
} catch (Exception e) {
|
||||
log.error("Decryption failed", e);
|
||||
ExceptionUtils.logException("PDF decryption", e);
|
||||
throw new IOException("PDF decryption failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temp file handling with enhanced logging
|
||||
// Temp file handling with enhanced logging and registry integration
|
||||
private Path createTempFile(String prefix) throws IOException {
|
||||
// Check if TempFileManager is available in the application context
|
||||
try {
|
||||
TempFileManager tempFileManager =
|
||||
ApplicationContextProvider.getBean(TempFileManager.class);
|
||||
if (tempFileManager != null) {
|
||||
// Use TempFileManager to create and register the temp file
|
||||
File file = tempFileManager.createTempFile(".tmp");
|
||||
log.debug("Created and registered temp file via TempFileManager: {}", file);
|
||||
return file.toPath();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("TempFileManager not available, falling back to standard temp file creation");
|
||||
}
|
||||
|
||||
// Fallback to standard temp file creation
|
||||
Path file = Files.createTempFile(prefix + tempCounter.incrementAndGet() + "-", ".tmp");
|
||||
log.debug("Created temp file: {}", file);
|
||||
|
||||
// Try to register the file with a static registry if possible
|
||||
try {
|
||||
TempFileRegistry registry = ApplicationContextProvider.getBean(TempFileRegistry.class);
|
||||
if (registry != null) {
|
||||
registry.register(file);
|
||||
log.debug("Registered fallback temp file with registry: {}", file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not register fallback temp file with registry: {}", file);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,14 +131,46 @@ public class FileStorage {
|
||||
return Files.exists(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file by its ID without loading the content into memory
|
||||
*
|
||||
* @param fileId The ID of the file
|
||||
* @return The size of the file in bytes
|
||||
* @throws IOException If the file doesn't exist or can't be read
|
||||
*/
|
||||
public long getFileSize(String fileId) throws IOException {
|
||||
Path filePath = getFilePath(fileId);
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new IOException("File not found with ID: " + fileId);
|
||||
}
|
||||
|
||||
return Files.size(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for a file ID
|
||||
*
|
||||
* @param fileId The ID of the file
|
||||
* @return The path to the file
|
||||
* @throws IllegalArgumentException if fileId contains path traversal characters or resolves
|
||||
* outside base directory
|
||||
*/
|
||||
private Path getFilePath(String fileId) {
|
||||
return Path.of(tempDirPath).resolve(fileId);
|
||||
// Validate fileId to prevent path traversal
|
||||
if (fileId.contains("..") || fileId.contains("/") || fileId.contains("\\")) {
|
||||
throw new IllegalArgumentException("Invalid file ID");
|
||||
}
|
||||
|
||||
Path basePath = Path.of(tempDirPath).normalize().toAbsolutePath();
|
||||
Path resolvedPath = basePath.resolve(fileId).normalize();
|
||||
|
||||
// Ensure resolved path is within the base directory
|
||||
if (!resolvedPath.startsWith(basePath)) {
|
||||
throw new IllegalArgumentException("File ID resolves to an invalid path");
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,8 +20,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
import stirling.software.common.model.job.JobResponse;
|
||||
import stirling.software.common.util.ExecutorFactory;
|
||||
|
||||
@@ -31,7 +29,6 @@ import stirling.software.common.util.ExecutorFactory;
|
||||
public class JobExecutorService {
|
||||
|
||||
private final TaskManager taskManager;
|
||||
private final WebSocketProgressController webSocketSender;
|
||||
private final FileStorage fileStorage;
|
||||
private final HttpServletRequest request;
|
||||
private final ResourceMonitor resourceMonitor;
|
||||
@@ -41,7 +38,6 @@ public class JobExecutorService {
|
||||
|
||||
public JobExecutorService(
|
||||
TaskManager taskManager,
|
||||
WebSocketProgressController webSocketSender,
|
||||
FileStorage fileStorage,
|
||||
HttpServletRequest request,
|
||||
ResourceMonitor resourceMonitor,
|
||||
@@ -49,7 +45,6 @@ public class JobExecutorService {
|
||||
@Value("${spring.mvc.async.request-timeout:1200000}") long asyncRequestTimeoutMs,
|
||||
@Value("${server.servlet.session.timeout:30m}") String sessionTimeout) {
|
||||
this.taskManager = taskManager;
|
||||
this.webSocketSender = webSocketSender;
|
||||
this.fileStorage = fileStorage;
|
||||
this.request = request;
|
||||
this.resourceMonitor = resourceMonitor;
|
||||
@@ -107,6 +102,22 @@ public class JobExecutorService {
|
||||
// Store the job ID in the request for potential use by other components
|
||||
if (request != null) {
|
||||
request.setAttribute("jobId", jobId);
|
||||
|
||||
// Also track this job ID in the user's session for authorization purposes
|
||||
// This ensures users can only cancel their own jobs
|
||||
if (request.getSession() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Set<String> userJobIds =
|
||||
(java.util.Set<String>) request.getSession().getAttribute("userJobIds");
|
||||
|
||||
if (userJobIds == null) {
|
||||
userJobIds = new java.util.concurrent.ConcurrentSkipListSet<>();
|
||||
request.getSession().setAttribute("userJobIds", userJobIds);
|
||||
}
|
||||
|
||||
userJobIds.add(jobId);
|
||||
log.debug("Added job ID {} to user session", jobId);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which timeout to use
|
||||
@@ -159,8 +170,6 @@ public class JobExecutorService {
|
||||
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
||||
} else if (async) {
|
||||
taskManager.createTask(jobId);
|
||||
webSocketSender.sendProgress(jobId, new JobProgress(jobId, "Started", 0, "Running"));
|
||||
|
||||
executor.execute(
|
||||
() -> {
|
||||
try {
|
||||
@@ -170,18 +179,12 @@ public class JobExecutorService {
|
||||
// Execute with timeout
|
||||
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||
processJobResult(jobId, result);
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Done", 100, "Complete"));
|
||||
} catch (TimeoutException te) {
|
||||
log.error("Job {} timed out after {} ms", jobId, timeoutToUse);
|
||||
taskManager.setError(jobId, "Job timed out");
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Error", 100, "Job timed out"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
||||
taskManager.setError(jobId, e.getMessage());
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Error", 100, e.getMessage()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,10 +225,13 @@ public class JobExecutorService {
|
||||
private void processJobResult(String jobId, Object result) {
|
||||
try {
|
||||
if (result instanceof byte[]) {
|
||||
// Store byte array as a file
|
||||
// Store byte array directly to disk to avoid double memory consumption
|
||||
String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf");
|
||||
taskManager.setFileResult(jobId, fileId, "result.pdf", "application/pdf");
|
||||
log.debug("Stored byte[] result with fileId: {}", fileId);
|
||||
|
||||
// Let the byte array get collected naturally in the next GC cycle
|
||||
// We don't need to force System.gc() which can be harmful
|
||||
} else if (result instanceof ResponseEntity) {
|
||||
ResponseEntity<?> response = (ResponseEntity<?>) result;
|
||||
Object body = response.getBody();
|
||||
@@ -250,9 +256,12 @@ public class JobExecutorService {
|
||||
contentType = response.getHeaders().getContentType().toString();
|
||||
}
|
||||
|
||||
// Store byte array directly to disk
|
||||
String fileId = fileStorage.storeBytes((byte[]) body, filename);
|
||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||
log.debug("Stored ResponseEntity<byte[]> result with fileId: {}", fileId);
|
||||
|
||||
// Let the GC handle the memory naturally
|
||||
} else {
|
||||
// Check if the response body contains a fileId
|
||||
if (body != null && body.toString().contains("fileId")) {
|
||||
@@ -445,8 +454,10 @@ public class JobExecutorService {
|
||||
*/
|
||||
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs)
|
||||
throws TimeoutException, Exception {
|
||||
// Use the same executor as other async jobs for consistency
|
||||
// This ensures all operations run on the same thread pool
|
||||
java.util.concurrent.CompletableFuture<T> future =
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(supplier);
|
||||
java.util.concurrent.CompletableFuture.supplyAsync(supplier, executor);
|
||||
|
||||
try {
|
||||
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
|
||||
@@ -6,20 +6,17 @@ import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
import stirling.software.common.util.ExecutorFactory;
|
||||
import stirling.software.common.util.SpringContextHolder;
|
||||
|
||||
/**
|
||||
* Manages a queue of jobs with dynamic sizing based on system resources. Used when system resources
|
||||
@@ -27,10 +24,11 @@ import stirling.software.common.util.ExecutorFactory;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class JobQueue {
|
||||
public class JobQueue implements SmartLifecycle {
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final ResourceMonitor resourceMonitor;
|
||||
private final WebSocketProgressController webSocketSender;
|
||||
|
||||
@Value("${stirling.job.queue.base-capacity:10}")
|
||||
private int baseQueueCapacity = 10;
|
||||
@@ -44,10 +42,11 @@ public class JobQueue {
|
||||
@Value("${stirling.job.queue.max-wait-time-ms:600000}")
|
||||
private long maxWaitTimeMs = 600000; // 10 minutes
|
||||
|
||||
private BlockingQueue<QueuedJob> jobQueue;
|
||||
private volatile BlockingQueue<QueuedJob> jobQueue;
|
||||
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
|
||||
|
||||
private boolean shuttingDown = false;
|
||||
|
||||
@@ -70,9 +69,8 @@ public class JobQueue {
|
||||
private volatile boolean cancelled = false;
|
||||
}
|
||||
|
||||
public JobQueue(ResourceMonitor resourceMonitor, WebSocketProgressController webSocketSender) {
|
||||
public JobQueue(ResourceMonitor resourceMonitor) {
|
||||
this.resourceMonitor = resourceMonitor;
|
||||
this.webSocketSender = webSocketSender;
|
||||
|
||||
// Initialize with dynamic capacity
|
||||
int capacity =
|
||||
@@ -80,8 +78,8 @@ public class JobQueue {
|
||||
this.jobQueue = new LinkedBlockingQueue<>(capacity);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
// Remove @PostConstruct to let SmartLifecycle control startup
|
||||
private void initializeSchedulers() {
|
||||
log.debug(
|
||||
"Starting job queue with base capacity {}, min capacity {}",
|
||||
baseQueueCapacity,
|
||||
@@ -99,8 +97,8 @@ public class JobQueue {
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
// Remove @PreDestroy to let SmartLifecycle control shutdown
|
||||
private void shutdownSchedulers() {
|
||||
log.info("Shutting down job queue");
|
||||
shuttingDown = true;
|
||||
|
||||
@@ -113,8 +111,22 @@ public class JobQueue {
|
||||
}
|
||||
});
|
||||
|
||||
scheduler.shutdownNow();
|
||||
jobExecutor.shutdownNow();
|
||||
// Shutdown schedulers and wait for termination
|
||||
try {
|
||||
scheduler.shutdown();
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
|
||||
jobExecutor.shutdown();
|
||||
if (!jobExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
jobExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
scheduler.shutdownNow();
|
||||
jobExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Job queue shutdown complete. Stats: total={}, rejected={}",
|
||||
@@ -122,6 +134,40 @@ public class JobQueue {
|
||||
rejectedJobs);
|
||||
}
|
||||
|
||||
// SmartLifecycle methods
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
log.info("Starting JobQueue lifecycle");
|
||||
if (!running) {
|
||||
initializeSchedulers();
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
log.info("Stopping JobQueue lifecycle");
|
||||
shutdownSchedulers();
|
||||
running = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
// Start earlier than most components, but shutdown later
|
||||
return 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a job for execution when resources permit.
|
||||
*
|
||||
@@ -146,36 +192,36 @@ public class JobQueue {
|
||||
|
||||
// Update stats
|
||||
totalQueuedJobs++;
|
||||
currentQueueSize = jobQueue.size();
|
||||
|
||||
// Try to add to the queue
|
||||
try {
|
||||
boolean added = jobQueue.offer(job, 5, TimeUnit.SECONDS);
|
||||
if (!added) {
|
||||
log.warn("Queue full, rejecting job {}", jobId);
|
||||
rejectedJobs++;
|
||||
future.completeExceptionally(
|
||||
new RuntimeException("Job queue full, please try again later"));
|
||||
// Synchronize access to the queue
|
||||
synchronized (queueLock) {
|
||||
currentQueueSize = jobQueue.size();
|
||||
|
||||
// Try to add to the queue
|
||||
try {
|
||||
boolean added = jobQueue.offer(job, 5, TimeUnit.SECONDS);
|
||||
if (!added) {
|
||||
log.warn("Queue full, rejecting job {}", jobId);
|
||||
rejectedJobs++;
|
||||
future.completeExceptionally(
|
||||
new RuntimeException("Job queue full, please try again later"));
|
||||
jobMap.remove(jobId);
|
||||
return future;
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Job {} queued for execution (weight: {}, queue size: {})",
|
||||
jobId,
|
||||
resourceWeight,
|
||||
jobQueue.size());
|
||||
|
||||
return future;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.completeExceptionally(new RuntimeException("Job queue interrupted"));
|
||||
jobMap.remove(jobId);
|
||||
return future;
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Job {} queued for execution (weight: {}, queue size: {})",
|
||||
jobId,
|
||||
resourceWeight,
|
||||
jobQueue.size());
|
||||
|
||||
// Notify client via WebSocket that job is queued
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Queued", 0, "Waiting in queue for resources"));
|
||||
|
||||
return future;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.completeExceptionally(new RuntimeException("Job queue interrupted"));
|
||||
jobMap.remove(jobId);
|
||||
return future;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,13 +231,16 @@ public class JobQueue {
|
||||
* @return The current capacity
|
||||
*/
|
||||
public int getQueueCapacity() {
|
||||
return ((LinkedBlockingQueue<QueuedJob>) jobQueue).remainingCapacity() + jobQueue.size();
|
||||
synchronized (queueLock) {
|
||||
return ((LinkedBlockingQueue<QueuedJob>) jobQueue).remainingCapacity()
|
||||
+ jobQueue.size();
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the capacity of the job queue based on available system resources. */
|
||||
private void updateQueueCapacity() {
|
||||
try {
|
||||
// Calculate new capacity
|
||||
// Calculate new capacity once and cache the result
|
||||
int newCapacity =
|
||||
resourceMonitor.calculateDynamicQueueCapacity(
|
||||
baseQueueCapacity, minQueueCapacity);
|
||||
@@ -201,14 +250,20 @@ public class JobQueue {
|
||||
log.debug(
|
||||
"Updating job queue capacity from {} to {}", currentCapacity, newCapacity);
|
||||
|
||||
// Create new queue with updated capacity
|
||||
BlockingQueue<QueuedJob> newQueue = new LinkedBlockingQueue<>(newCapacity);
|
||||
synchronized (queueLock) {
|
||||
// Double-check that capacity still needs to be updated
|
||||
// Use the cached currentCapacity to avoid calling getQueueCapacity() again
|
||||
if (newCapacity != currentCapacity) {
|
||||
// Create new queue with updated capacity
|
||||
BlockingQueue<QueuedJob> newQueue = new LinkedBlockingQueue<>(newCapacity);
|
||||
|
||||
// Transfer jobs from old queue to new queue
|
||||
jobQueue.drainTo(newQueue);
|
||||
jobQueue = newQueue;
|
||||
// Transfer jobs from old queue to new queue
|
||||
jobQueue.drainTo(newQueue);
|
||||
jobQueue = newQueue;
|
||||
|
||||
currentQueueSize = jobQueue.size();
|
||||
currentQueueSize = jobQueue.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating queue capacity: {}", e.getMessage(), e);
|
||||
@@ -217,60 +272,87 @@ public class JobQueue {
|
||||
|
||||
/** Processes jobs in the queue, executing them when resources permit. */
|
||||
private void processQueue() {
|
||||
if (shuttingDown || jobQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Jobs to execute after releasing the lock
|
||||
java.util.List<QueuedJob> jobsToExecute = new java.util.ArrayList<>();
|
||||
|
||||
try {
|
||||
// Get current resource status
|
||||
ResourceMonitor.ResourceStatus status = resourceMonitor.getCurrentStatus().get();
|
||||
|
||||
// Check if we should execute any jobs
|
||||
boolean canExecuteJobs = (status != ResourceMonitor.ResourceStatus.CRITICAL);
|
||||
|
||||
if (!canExecuteJobs) {
|
||||
// Under critical load, don't execute any jobs
|
||||
log.debug("System under critical load, delaying job execution");
|
||||
// First synchronized block: poll jobs from the queue and prepare them for execution
|
||||
synchronized (queueLock) {
|
||||
if (shuttingDown || jobQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get jobs from the queue, up to a limit based on resource availability
|
||||
int jobsToProcess =
|
||||
Math.max(
|
||||
1,
|
||||
switch (status) {
|
||||
case OK -> 3;
|
||||
case WARNING -> 1;
|
||||
case CRITICAL -> 0;
|
||||
});
|
||||
try {
|
||||
// Get current resource status
|
||||
ResourceMonitor.ResourceStatus status = resourceMonitor.getCurrentStatus().get();
|
||||
|
||||
for (int i = 0; i < jobsToProcess && !jobQueue.isEmpty(); i++) {
|
||||
QueuedJob job = jobQueue.poll();
|
||||
if (job == null) break;
|
||||
// Check if we should execute any jobs
|
||||
boolean canExecuteJobs = (status != ResourceMonitor.ResourceStatus.CRITICAL);
|
||||
|
||||
// Check if it's been waiting too long
|
||||
long waitTimeMs = Instant.now().toEpochMilli() - job.queuedAt.toEpochMilli();
|
||||
if (waitTimeMs > maxWaitTimeMs) {
|
||||
log.warn(
|
||||
"Job {} exceeded maximum wait time ({} ms), executing anyway",
|
||||
job.jobId,
|
||||
waitTimeMs);
|
||||
if (!canExecuteJobs) {
|
||||
// Under critical load, don't execute any jobs
|
||||
log.debug("System under critical load, delaying job execution");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from our map
|
||||
jobMap.remove(job.jobId);
|
||||
currentQueueSize = jobQueue.size();
|
||||
// Get jobs from the queue, up to a limit based on resource availability
|
||||
int jobsToProcess =
|
||||
Math.max(
|
||||
1,
|
||||
switch (status) {
|
||||
case OK -> 3;
|
||||
case WARNING -> 1;
|
||||
case CRITICAL -> 0;
|
||||
});
|
||||
|
||||
// Notify via WebSocket
|
||||
webSocketSender.sendProgress(
|
||||
job.jobId,
|
||||
new JobProgress(job.jobId, "Processing", 10, "Starting job execution"));
|
||||
for (int i = 0; i < jobsToProcess && !jobQueue.isEmpty(); i++) {
|
||||
QueuedJob job = jobQueue.poll();
|
||||
if (job == null) break;
|
||||
|
||||
// Execute the job
|
||||
executeJob(job);
|
||||
// Check if it's been waiting too long
|
||||
long waitTimeMs = Instant.now().toEpochMilli() - job.queuedAt.toEpochMilli();
|
||||
if (waitTimeMs > maxWaitTimeMs) {
|
||||
log.warn(
|
||||
"Job {} exceeded maximum wait time ({} ms), executing anyway",
|
||||
job.jobId,
|
||||
waitTimeMs);
|
||||
|
||||
// Add a specific status to the job context that can be tracked
|
||||
// This will be visible in the job status API
|
||||
try {
|
||||
TaskManager taskManager =
|
||||
SpringContextHolder.getBean(TaskManager.class);
|
||||
if (taskManager != null) {
|
||||
taskManager.addNote(
|
||||
job.jobId,
|
||||
"QUEUED_TIMEOUT: Job waited in queue for "
|
||||
+ (waitTimeMs / 1000)
|
||||
+ " seconds, exceeding the maximum wait time of "
|
||||
+ (maxWaitTimeMs / 1000)
|
||||
+ " seconds.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to add timeout note to job {}: {}",
|
||||
job.jobId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from our map
|
||||
jobMap.remove(job.jobId);
|
||||
currentQueueSize = jobQueue.size();
|
||||
|
||||
// Add to the list of jobs to execute outside the synchronized block
|
||||
jobsToExecute.add(job);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing job queue: {}", e.getMessage(), e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing job queue: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Now execute the jobs outside the synchronized block to avoid holding the lock
|
||||
for (QueuedJob job : jobsToExecute) {
|
||||
executeJob(job);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,21 +382,10 @@ public class JobQueue {
|
||||
job.future.complete(ResponseEntity.ok(result));
|
||||
}
|
||||
|
||||
// Update WebSocket
|
||||
webSocketSender.sendProgress(
|
||||
job.jobId,
|
||||
new JobProgress(
|
||||
job.jobId, "Complete", 100, "Job completed successfully"));
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Error executing queued job {}: {}", job.jobId, e.getMessage(), e);
|
||||
job.future.completeExceptionally(e);
|
||||
|
||||
// Update WebSocket
|
||||
webSocketSender.sendProgress(
|
||||
job.jobId,
|
||||
new JobProgress(
|
||||
job.jobId, "Error", 100, "Job failed: " + e.getMessage()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -402,10 +473,6 @@ public class JobQueue {
|
||||
|
||||
log.debug("Job {} cancelled", jobId);
|
||||
|
||||
// Update WebSocket
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Cancelled", 100, "Job cancelled by user"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ public class PostHogService {
|
||||
|
||||
// New environment variables
|
||||
dockerMetrics.put("version_tag", System.getenv("VERSION_TAG"));
|
||||
dockerMetrics.put("without_enhanced_features", System.getenv("WITHOUT_ENHANCED_FEATURES"));
|
||||
dockerMetrics.put("additional_features_off", System.getenv("ADDITIONAL_FEATURES_OFF"));
|
||||
dockerMetrics.put("fat_docker", System.getenv("FAT_DOCKER"));
|
||||
|
||||
return dockerMetrics;
|
||||
|
||||
@@ -172,8 +172,10 @@ public class ResourceMonitor {
|
||||
if (oldStatus != newStatus) {
|
||||
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
||||
log.info(
|
||||
"Current metrics - CPU: {:.1f}%, Memory: {:.1f}%, Free Memory: {} MB",
|
||||
cpuUsage * 100, memoryUsage * 100, freeMemory / (1024 * 1024));
|
||||
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
||||
String.format("%.1f", cpuUsage * 100),
|
||||
String.format("%.1f", memoryUsage * 100),
|
||||
freeMemory / (1024 * 1024));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating resource metrics: {}", e.getMessage(), e);
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import io.github.pixee.security.ZipSecurity;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
@@ -17,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
|
||||
/** Manages async tasks and their results */
|
||||
@Service
|
||||
@@ -80,8 +91,53 @@ public class TaskManager {
|
||||
public void setFileResult(
|
||||
String jobId, String fileId, String originalFileName, String contentType) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
log.debug("Set file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||
|
||||
// Check if this is a ZIP file that should be extracted
|
||||
if (isZipFile(contentType, originalFileName)) {
|
||||
try {
|
||||
List<ResultFile> extractedFiles =
|
||||
extractZipToIndividualFiles(fileId, originalFileName);
|
||||
if (!extractedFiles.isEmpty()) {
|
||||
jobResult.completeWithFiles(extractedFiles);
|
||||
log.debug(
|
||||
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
|
||||
jobId,
|
||||
extractedFiles.size());
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
|
||||
jobId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle as single file using new ResultFile approach
|
||||
try {
|
||||
long fileSize = fileStorage.getFileSize(fileId);
|
||||
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, fileSize);
|
||||
log.debug("Set single file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage());
|
||||
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result of a task as multiple files
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param resultFiles The list of result files
|
||||
*/
|
||||
public void setMultipleFileResults(String jobId, List<ResultFile> resultFiles) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.completeWithFiles(resultFiles);
|
||||
log.debug(
|
||||
"Set multiple file results for job ID: {} with {} files",
|
||||
jobId,
|
||||
resultFiles.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +160,7 @@ public class TaskManager {
|
||||
public void setComplete(String jobId) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
if (jobResult.getResult() == null
|
||||
&& jobResult.getFileId() == null
|
||||
&& !jobResult.hasFiles()
|
||||
&& jobResult.getError() == null) {
|
||||
// If no result or error has been set, mark it as complete with an empty result
|
||||
jobResult.completeWithResult("Task completed successfully");
|
||||
@@ -133,6 +189,25 @@ public class TaskManager {
|
||||
return jobResults.get(jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note to a task. Notes are informational messages that can be attached to a job for
|
||||
* tracking purposes.
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param note The note to add
|
||||
* @return true if the note was added successfully, false if the job doesn't exist
|
||||
*/
|
||||
public boolean addNote(String jobId, String note) {
|
||||
JobResult jobResult = jobResults.get(jobId);
|
||||
if (jobResult != null) {
|
||||
jobResult.addNote(note);
|
||||
log.debug("Added note to job ID: {}: {}", jobId, note);
|
||||
return true;
|
||||
}
|
||||
log.warn("Attempted to add note to non-existent job ID: {}", jobId);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about all jobs in the system
|
||||
*
|
||||
@@ -167,7 +242,7 @@ public class TaskManager {
|
||||
failedJobs++;
|
||||
} else {
|
||||
successfulJobs++;
|
||||
if (result.getFileId() != null) {
|
||||
if (result.hasFiles()) {
|
||||
fileResultJobs++;
|
||||
}
|
||||
}
|
||||
@@ -231,17 +306,8 @@ public class TaskManager {
|
||||
&& result.getCompletedAt() != null
|
||||
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||
|
||||
// If the job has a file result, delete the file
|
||||
if (result.getFileId() != null) {
|
||||
try {
|
||||
fileStorage.deleteFile(result.getFileId());
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to delete file for job {}: {}",
|
||||
entry.getKey(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
// Clean up file results
|
||||
cleanupJobFiles(result, entry.getKey());
|
||||
|
||||
// Remove the job result
|
||||
jobResults.remove(entry.getKey());
|
||||
@@ -271,4 +337,128 @@ public class TaskManager {
|
||||
cleanupExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if a file is a ZIP file based on content type and filename */
|
||||
private boolean isZipFile(String contentType, String fileName) {
|
||||
if (contentType != null
|
||||
&& (contentType.equals("application/zip")
|
||||
|| contentType.equals("application/x-zip-compressed"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extract a ZIP file into individual files and store them */
|
||||
private List<ResultFile> extractZipToIndividualFiles(
|
||||
String zipFileId, String originalZipFileName) throws IOException {
|
||||
List<ResultFile> extractedFiles = new ArrayList<>();
|
||||
|
||||
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
|
||||
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipIn.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory()) {
|
||||
// Use buffered reading for memory safety
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = zipIn.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] fileContent = out.toByteArray();
|
||||
|
||||
String contentType = determineContentType(entry.getName());
|
||||
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
|
||||
|
||||
ResultFile resultFile =
|
||||
ResultFile.builder()
|
||||
.fileId(individualFileId)
|
||||
.fileName(entry.getName())
|
||||
.contentType(contentType)
|
||||
.fileSize(fileContent.length)
|
||||
.build();
|
||||
|
||||
extractedFiles.add(resultFile);
|
||||
log.debug(
|
||||
"Extracted file: {} (size: {} bytes)",
|
||||
entry.getName(),
|
||||
fileContent.length);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the original ZIP file after extraction
|
||||
try {
|
||||
fileStorage.deleteFile(zipFileId);
|
||||
log.debug("Cleaned up original ZIP file: {}", zipFileId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to clean up original ZIP file {}: {}", zipFileId, e.getMessage());
|
||||
}
|
||||
|
||||
return extractedFiles;
|
||||
}
|
||||
|
||||
/** Determine content type based on file extension */
|
||||
private String determineContentType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
|
||||
String lowerName = fileName.toLowerCase();
|
||||
if (lowerName.endsWith(".pdf")) {
|
||||
return MediaType.APPLICATION_PDF_VALUE;
|
||||
} else if (lowerName.endsWith(".txt")) {
|
||||
return MediaType.TEXT_PLAIN_VALUE;
|
||||
} else if (lowerName.endsWith(".json")) {
|
||||
return MediaType.APPLICATION_JSON_VALUE;
|
||||
} else if (lowerName.endsWith(".xml")) {
|
||||
return MediaType.APPLICATION_XML_VALUE;
|
||||
} else if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) {
|
||||
return MediaType.IMAGE_JPEG_VALUE;
|
||||
} else if (lowerName.endsWith(".png")) {
|
||||
return MediaType.IMAGE_PNG_VALUE;
|
||||
} else {
|
||||
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clean up files associated with a job result */
|
||||
private void cleanupJobFiles(JobResult result, String jobId) {
|
||||
// Clean up all result files
|
||||
if (result.hasFiles()) {
|
||||
for (ResultFile resultFile : result.getAllResultFiles()) {
|
||||
try {
|
||||
fileStorage.deleteFile(resultFile.getFileId());
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to delete file {} for job {}: {}",
|
||||
resultFile.getFileId(),
|
||||
jobId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the ResultFile metadata for a given file ID by searching through all job results */
|
||||
public ResultFile findResultFileByFileId(String fileId) {
|
||||
for (JobResult jobResult : jobResults.values()) {
|
||||
if (jobResult.hasFiles()) {
|
||||
for (ResultFile resultFile : jobResult.getAllResultFiles()) {
|
||||
if (fileId.equals(resultFile.getFileId())) {
|
||||
return resultFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Service to periodically clean up temporary files. Runs scheduled tasks to delete old temp files
|
||||
* and directories.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TempFileCleanupService {
|
||||
|
||||
private final TempFileRegistry registry;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("machineType")
|
||||
private String machineType;
|
||||
|
||||
// Maximum recursion depth for directory traversal
|
||||
private static final int MAX_RECURSION_DEPTH = 5;
|
||||
|
||||
// File patterns that identify our temp files
|
||||
private static final Predicate<String> IS_OUR_TEMP_FILE =
|
||||
fileName ->
|
||||
fileName.startsWith("stirling-pdf-")
|
||||
|| fileName.startsWith("output_")
|
||||
|| fileName.startsWith("compressedPDF")
|
||||
|| fileName.startsWith("pdf-save-")
|
||||
|| fileName.startsWith("pdf-stream-")
|
||||
|| fileName.startsWith("PDFBox")
|
||||
|| fileName.startsWith("input_")
|
||||
|| fileName.startsWith("overlay-");
|
||||
|
||||
// File patterns that identify common system temp files
|
||||
private static final Predicate<String> IS_SYSTEM_TEMP_FILE =
|
||||
fileName ->
|
||||
fileName.matches("lu\\d+[a-z0-9]*\\.tmp")
|
||||
|| fileName.matches("ocr_process\\d+")
|
||||
|| (fileName.startsWith("tmp") && !fileName.contains("jetty"))
|
||||
|| fileName.startsWith("OSL_PIPE_")
|
||||
|| (fileName.endsWith(".tmp") && !fileName.contains("jetty"));
|
||||
|
||||
// File patterns that should be excluded from cleanup
|
||||
private static final Predicate<String> SHOULD_SKIP =
|
||||
fileName ->
|
||||
fileName.contains("jetty")
|
||||
|| fileName.startsWith("jetty-")
|
||||
|| "proc".equals(fileName)
|
||||
|| "sys".equals(fileName)
|
||||
|| "dev".equals(fileName)
|
||||
|| "hsperfdata_stirlingpdfuser".equals(fileName)
|
||||
|| fileName.startsWith("hsperfdata_")
|
||||
|| ".pdfbox.cache".equals(fileName);
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// Create necessary directories
|
||||
ensureDirectoriesExist();
|
||||
|
||||
// Perform startup cleanup if enabled
|
||||
if (applicationProperties.getSystem().getTempFileManagement().isStartupCleanup()) {
|
||||
runStartupCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure that all required temp directories exist */
|
||||
private void ensureDirectoriesExist() {
|
||||
try {
|
||||
ApplicationProperties.TempFileManagement tempFiles =
|
||||
applicationProperties.getSystem().getTempFileManagement();
|
||||
|
||||
// Create the main temp directory
|
||||
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||
Path tempDir = Path.of(customTempDirectory);
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
log.info("Created temp directory: {}", tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Create LibreOffice temp directory
|
||||
String libreOfficeTempDir = tempFiles.getLibreofficeDir();
|
||||
if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) {
|
||||
Path loTempDir = Path.of(libreOfficeTempDir);
|
||||
if (!Files.exists(loTempDir)) {
|
||||
Files.createDirectories(loTempDir);
|
||||
log.info("Created LibreOffice temp directory: {}", loTempDir);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error creating temp directories", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
|
||||
@Scheduled(
|
||||
fixedDelayString =
|
||||
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
||||
timeUnit = TimeUnit.MINUTES)
|
||||
public void scheduledCleanup() {
|
||||
log.info("Running scheduled temporary file cleanup");
|
||||
long maxAgeMillis = tempFileManager.getMaxAgeMillis();
|
||||
|
||||
// Clean up registered temp files (managed by TempFileRegistry)
|
||||
int registeredDeletedCount = tempFileManager.cleanupOldTempFiles(maxAgeMillis);
|
||||
log.info("Cleaned up {} registered temporary files", registeredDeletedCount);
|
||||
|
||||
// Clean up registered temp directories
|
||||
int directoriesDeletedCount = 0;
|
||||
for (Path directory : registry.getTempDirectories()) {
|
||||
try {
|
||||
if (Files.exists(directory)) {
|
||||
GeneralUtils.deleteDirectory(directory);
|
||||
directoriesDeletedCount++;
|
||||
log.debug("Cleaned up temporary directory: {}", directory);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to clean up temporary directory: {}", directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up PDFBox cache file
|
||||
cleanupPDFBoxCache();
|
||||
|
||||
// Clean up unregistered temp files based on our cleanup strategy
|
||||
boolean containerMode = isContainerMode();
|
||||
int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis);
|
||||
|
||||
if (registeredDeletedCount > 0
|
||||
|| unregisteredDeletedCount > 0
|
||||
|| directoriesDeletedCount > 0) {
|
||||
log.info(
|
||||
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
|
||||
registeredDeletedCount,
|
||||
unregisteredDeletedCount,
|
||||
directoriesDeletedCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform startup cleanup of stale temporary files from previous runs. This is especially
|
||||
* important in Docker environments where temp files persist between container restarts.
|
||||
*/
|
||||
private void runStartupCleanup() {
|
||||
boolean containerMode = isContainerMode();
|
||||
|
||||
log.info(
|
||||
"Running in {} mode, using {} cleanup strategy",
|
||||
machineType,
|
||||
containerMode ? "aggressive" : "conservative");
|
||||
|
||||
// For startup cleanup, we use a longer timeout for non-container environments
|
||||
long maxAgeMillis = containerMode ? 0 : 24 * 60 * 60 * 1000; // 0 or 24 hours
|
||||
|
||||
int totalDeletedCount = cleanupUnregisteredFiles(containerMode, false, maxAgeMillis);
|
||||
log.info(
|
||||
"Startup cleanup complete. Deleted {} temporary files/directories",
|
||||
totalDeletedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up unregistered temporary files across all configured temp directories.
|
||||
*
|
||||
* @param containerMode Whether we're in container mode (more aggressive cleanup)
|
||||
* @param isScheduled Whether this is a scheduled cleanup or startup cleanup
|
||||
* @param maxAgeMillis Maximum age of files to clean in milliseconds
|
||||
* @return Number of files deleted
|
||||
*/
|
||||
private int cleanupUnregisteredFiles(
|
||||
boolean containerMode, boolean isScheduled, long maxAgeMillis) {
|
||||
AtomicInteger totalDeletedCount = new AtomicInteger(0);
|
||||
|
||||
try {
|
||||
ApplicationProperties.TempFileManagement tempFiles =
|
||||
applicationProperties.getSystem().getTempFileManagement();
|
||||
Path[] dirsToScan;
|
||||
if (tempFiles.isCleanupSystemTemp()
|
||||
&& tempFiles.getSystemTempDir() != null
|
||||
&& !tempFiles.getSystemTempDir().isEmpty()) {
|
||||
Path systemTempPath = getSystemTempPath();
|
||||
dirsToScan =
|
||||
new Path[] {
|
||||
systemTempPath,
|
||||
Path.of(tempFiles.getBaseTmpDir()),
|
||||
Path.of(tempFiles.getLibreofficeDir())
|
||||
};
|
||||
} else {
|
||||
dirsToScan =
|
||||
new Path[] {
|
||||
Path.of(tempFiles.getBaseTmpDir()),
|
||||
Path.of(tempFiles.getLibreofficeDir())
|
||||
};
|
||||
}
|
||||
|
||||
// Process each directory
|
||||
Arrays.stream(dirsToScan)
|
||||
.filter(Files::exists)
|
||||
.forEach(
|
||||
tempDir -> {
|
||||
try {
|
||||
String phase = isScheduled ? "scheduled" : "startup";
|
||||
log.debug(
|
||||
"Scanning directory for {} cleanup: {}",
|
||||
phase,
|
||||
tempDir);
|
||||
|
||||
AtomicInteger dirDeletedCount = new AtomicInteger(0);
|
||||
cleanupDirectoryStreaming(
|
||||
tempDir,
|
||||
containerMode,
|
||||
0,
|
||||
maxAgeMillis,
|
||||
isScheduled,
|
||||
path -> {
|
||||
dirDeletedCount.incrementAndGet();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"Deleted temp file during {} cleanup: {}",
|
||||
phase,
|
||||
path);
|
||||
}
|
||||
});
|
||||
|
||||
int count = dirDeletedCount.get();
|
||||
totalDeletedCount.addAndGet(count);
|
||||
if (count > 0) {
|
||||
log.info(
|
||||
"Cleaned up {} files/directories in {}",
|
||||
count,
|
||||
tempDir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error during cleanup of directory: {}", tempDir, e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error during cleanup of unregistered files", e);
|
||||
}
|
||||
|
||||
return totalDeletedCount.get();
|
||||
}
|
||||
|
||||
/** Get the system temp directory path based on configuration or system property. */
|
||||
private Path getSystemTempPath() {
|
||||
String systemTempDir =
|
||||
applicationProperties.getSystem().getTempFileManagement().getSystemTempDir();
|
||||
if (systemTempDir != null && !systemTempDir.isEmpty()) {
|
||||
return Path.of(systemTempDir);
|
||||
} else {
|
||||
return Path.of(System.getProperty("java.io.tmpdir"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Determine if we're running in a container environment. */
|
||||
private boolean isContainerMode() {
|
||||
return "Docker".equals(machineType) || "Kubernetes".equals(machineType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively clean up a directory using a streaming approach to reduce memory usage.
|
||||
*
|
||||
* @param directory The directory to clean
|
||||
* @param containerMode Whether we're in container mode (more aggressive cleanup)
|
||||
* @param depth Current recursion depth
|
||||
* @param maxAgeMillis Maximum age of files to delete
|
||||
* @param isScheduled Whether this is a scheduled cleanup (vs startup)
|
||||
* @param onDeleteCallback Callback function when a file is deleted
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
private void cleanupDirectoryStreaming(
|
||||
Path directory,
|
||||
boolean containerMode,
|
||||
int depth,
|
||||
long maxAgeMillis,
|
||||
boolean isScheduled,
|
||||
Consumer<Path> onDeleteCallback)
|
||||
throws IOException {
|
||||
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
log.debug("Maximum directory recursion depth reached for: {}", directory);
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.List<Path> subdirectories = new java.util.ArrayList<>();
|
||||
|
||||
try (Stream<Path> pathStream = Files.list(directory)) {
|
||||
pathStream.forEach(
|
||||
path -> {
|
||||
try {
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
if (SHOULD_SKIP.test(fileName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Files.isDirectory(path)) {
|
||||
subdirectories.add(path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registry.contains(path.toFile())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldDeleteFile(path, fileName, containerMode, maxAgeMillis)) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
onDeleteCallback.accept(path);
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
.contains("being used by another process")) {
|
||||
log.debug("File locked, skipping delete: {}", path);
|
||||
} else {
|
||||
log.warn("Failed to delete temp file: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Error processing path: {}", path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (Path subdirectory : subdirectories) {
|
||||
try {
|
||||
cleanupDirectoryStreaming(
|
||||
subdirectory,
|
||||
containerMode,
|
||||
depth + 1,
|
||||
maxAgeMillis,
|
||||
isScheduled,
|
||||
onDeleteCallback);
|
||||
} catch (IOException e) {
|
||||
log.warn("Error processing subdirectory: {}", subdirectory, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Determine if a file should be deleted based on its name, age, and other criteria. */
|
||||
private boolean shouldDeleteFile(
|
||||
Path path, String fileName, boolean containerMode, long maxAgeMillis) {
|
||||
// First check if it matches our known temp file patterns
|
||||
boolean isOurTempFile = IS_OUR_TEMP_FILE.test(fileName);
|
||||
boolean isSystemTempFile = IS_SYSTEM_TEMP_FILE.test(fileName);
|
||||
|
||||
// Normal operation - check against temp file patterns
|
||||
boolean shouldDelete = isOurTempFile || (containerMode && isSystemTempFile);
|
||||
|
||||
// Get file info for age checks
|
||||
long lastModified = 0;
|
||||
long currentTime = System.currentTimeMillis();
|
||||
boolean isEmptyFile = false;
|
||||
|
||||
try {
|
||||
lastModified = Files.getLastModifiedTime(path).toMillis();
|
||||
// Special case for zero-byte files - these are often corrupted temp files
|
||||
if (Files.size(path) == 0) {
|
||||
isEmptyFile = true;
|
||||
// For empty files, use a shorter timeout (5 minutes)
|
||||
// Delete empty files older than 5 minutes
|
||||
if ((currentTime - lastModified) > 5 * 60 * 1000) {
|
||||
shouldDelete = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.debug("Could not check file info, skipping: {}", path);
|
||||
}
|
||||
|
||||
// Check file age against maxAgeMillis only if it's not an empty file that we've already
|
||||
// decided to delete
|
||||
if (!isEmptyFile && shouldDelete && maxAgeMillis > 0) {
|
||||
// In normal mode, check age against maxAgeMillis
|
||||
shouldDelete = (currentTime - lastModified) > maxAgeMillis;
|
||||
}
|
||||
|
||||
return shouldDelete;
|
||||
}
|
||||
|
||||
/** Clean up LibreOffice temporary files. This method is called after LibreOffice operations. */
|
||||
public void cleanupLibreOfficeTempFiles() {
|
||||
// Cleanup known LibreOffice temp directories
|
||||
try {
|
||||
Set<Path> directories = registry.getTempDirectories();
|
||||
for (Path dir : directories) {
|
||||
if (dir.getFileName().toString().contains("libreoffice") && Files.exists(dir)) {
|
||||
// For directories containing "libreoffice", delete all contents
|
||||
// but keep the directory itself for future use
|
||||
cleanupDirectoryStreaming(
|
||||
dir,
|
||||
isContainerMode(),
|
||||
0,
|
||||
0, // age doesn't matter for LibreOffice cleanup
|
||||
false,
|
||||
path -> log.debug("Cleaned up LibreOffice temp file: {}", path));
|
||||
log.debug("Cleaned up LibreOffice temp directory contents: {}", dir);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to clean up LibreOffice temp files", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up PDFBox cache file from user home directory. This cache file can grow large and
|
||||
* should be periodically cleaned.
|
||||
*/
|
||||
private void cleanupPDFBoxCache() {
|
||||
try {
|
||||
Path userHome = Path.of(System.getProperty("user.home"));
|
||||
Path pdfboxCache = userHome.resolve(".pdfbox.cache");
|
||||
|
||||
if (Files.exists(pdfboxCache)) {
|
||||
Files.deleteIfExists(pdfboxCache);
|
||||
log.debug("Cleaned up PDFBox cache file: {}", pdfboxCache);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to clean up PDFBox cache file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Helper class that provides access to the ApplicationContext. Useful for getting beans in classes
|
||||
* that are not managed by Spring.
|
||||
*/
|
||||
@Component
|
||||
public class ApplicationContextProvider implements ApplicationContextAware {
|
||||
|
||||
private static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||
applicationContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bean by class type.
|
||||
*
|
||||
* @param <T> The type of the bean
|
||||
* @param beanClass The class of the bean
|
||||
* @return The bean instance, or null if not found
|
||||
*/
|
||||
public static <T> T getBean(Class<T> beanClass) {
|
||||
if (applicationContext == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return applicationContext.getBean(beanClass);
|
||||
} catch (BeansException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bean by name and class type.
|
||||
*
|
||||
* @param <T> The type of the bean
|
||||
* @param name The name of the bean
|
||||
* @param beanClass The class of the bean
|
||||
* @return The bean instance, or null if not found
|
||||
*/
|
||||
public static <T> T getBean(String name, Class<T> beanClass) {
|
||||
if (applicationContext == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return applicationContext.getBean(name, beanClass);
|
||||
} catch (BeansException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a bean of the specified type exists.
|
||||
*
|
||||
* @param beanClass The class of the bean
|
||||
* @return true if the bean exists, false otherwise
|
||||
*/
|
||||
public static boolean containsBean(Class<?> beanClass) {
|
||||
if (applicationContext == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
applicationContext.getBean(beanClass);
|
||||
return true;
|
||||
} catch (BeansException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PageMode;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class AttachmentUtils {
|
||||
|
||||
/**
|
||||
* Sets the PDF catalog viewer preferences to display attachments in the viewer.
|
||||
*
|
||||
* @param document The <code>PDDocument</code> to modify.
|
||||
* @param pageMode The <code>PageMode</code> to set for the PDF viewer. <code>PageMode</code>
|
||||
* values: <code>UseNone</code>, <code>UseOutlines</code>, <code>UseThumbs</code>, <code>
|
||||
* FullScreen</code>, <code>UseOC</code>, <code>UseAttachments</code>.
|
||||
*/
|
||||
public static void setCatalogViewerPreferences(PDDocument document, PageMode pageMode) {
|
||||
try {
|
||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||
if (catalog != null) {
|
||||
COSDictionary catalogDict = catalog.getCOSObject();
|
||||
|
||||
catalog.setPageMode(pageMode);
|
||||
catalogDict.setName(COSName.PAGE_MODE, pageMode.stringValue());
|
||||
|
||||
COSDictionary viewerPrefs =
|
||||
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
|
||||
if (viewerPrefs == null) {
|
||||
viewerPrefs = new COSDictionary();
|
||||
catalogDict.setItem(COSName.VIEWER_PREFERENCES, viewerPrefs);
|
||||
}
|
||||
|
||||
viewerPrefs.setName(
|
||||
COSName.getPDFName("NonFullScreenPageMode"), pageMode.stringValue());
|
||||
|
||||
viewerPrefs.setBoolean(COSName.getPDFName("DisplayDocTitle"), true);
|
||||
|
||||
log.info(
|
||||
"Set PDF PageMode to UseAttachments to automatically show attachments pane");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to set catalog viewer preferences for attachments", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,327 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Utility class for handling exceptions with internationalized error messages. Provides consistent
|
||||
* error handling and user-friendly messages across the application.
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExceptionUtils {
|
||||
|
||||
/**
|
||||
* Create an IOException with internationalized message for PDF corruption.
|
||||
*
|
||||
* @param cause the original exception
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createPdfCorruptedException(Exception cause) {
|
||||
return createPdfCorruptedException(null, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOException with internationalized message for PDF corruption with context.
|
||||
*
|
||||
* @param context additional context (e.g., "during merge", "during image extraction")
|
||||
* @param cause the original exception
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createPdfCorruptedException(String context, Exception cause) {
|
||||
String message;
|
||||
if (context != null && !context.isEmpty()) {
|
||||
message =
|
||||
String.format(
|
||||
"Error %s: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.",
|
||||
context);
|
||||
} else {
|
||||
message =
|
||||
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.";
|
||||
}
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOException with internationalized message for multiple corrupted PDFs.
|
||||
*
|
||||
* @param cause the original exception
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createMultiplePdfCorruptedException(Exception cause) {
|
||||
String message =
|
||||
"One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.";
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOException with internationalized message for PDF encryption issues.
|
||||
*
|
||||
* @param cause the original exception
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createPdfEncryptionException(Exception cause) {
|
||||
String message =
|
||||
"The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.";
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOException with internationalized message for PDF password issues.
|
||||
*
|
||||
* @param cause the original exception
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createPdfPasswordException(Exception cause) {
|
||||
String message =
|
||||
"The PDF Document is passworded and either the password was not provided or was incorrect";
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOException with internationalized message for file processing errors.
|
||||
*
|
||||
* @param operation the operation being performed (e.g., "merge", "split", "convert")
|
||||
* @param cause the original exception
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createFileProcessingException(String operation, Exception cause) {
|
||||
String message =
|
||||
String.format(
|
||||
"An error occurred while processing the file during %s operation: %s",
|
||||
operation, cause.getMessage());
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic IOException with internationalized message.
|
||||
*
|
||||
* @param messageKey the i18n message key
|
||||
* @param defaultMessage the default message if i18n is not available
|
||||
* @param cause the original exception
|
||||
* @param args optional arguments for the message
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createIOException(
|
||||
String messageKey, String defaultMessage, Exception cause, Object... args) {
|
||||
String message = MessageFormat.format(defaultMessage, args);
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a generic RuntimeException with internationalized message.
|
||||
*
|
||||
* @param messageKey the i18n message key
|
||||
* @param defaultMessage the default message if i18n is not available
|
||||
* @param cause the original exception
|
||||
* @param args optional arguments for the message
|
||||
* @return RuntimeException with user-friendly message
|
||||
*/
|
||||
public static RuntimeException createRuntimeException(
|
||||
String messageKey, String defaultMessage, Exception cause, Object... args) {
|
||||
String message = MessageFormat.format(defaultMessage, args);
|
||||
return new RuntimeException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IllegalArgumentException with internationalized message.
|
||||
*
|
||||
* @param messageKey the i18n message key
|
||||
* @param defaultMessage the default message if i18n is not available
|
||||
* @param args optional arguments for the message
|
||||
* @return IllegalArgumentException with user-friendly message
|
||||
*/
|
||||
public static IllegalArgumentException createIllegalArgumentException(
|
||||
String messageKey, String defaultMessage, Object... args) {
|
||||
String message = MessageFormat.format(defaultMessage, args);
|
||||
return new IllegalArgumentException(message);
|
||||
}
|
||||
|
||||
/** Create file validation exceptions. */
|
||||
public static IllegalArgumentException createHtmlFileRequiredException() {
|
||||
return createIllegalArgumentException(
|
||||
"error.fileFormatRequired", "File must be in {0} format", "HTML or ZIP");
|
||||
}
|
||||
|
||||
public static IllegalArgumentException createPdfFileRequiredException() {
|
||||
return createIllegalArgumentException(
|
||||
"error.fileFormatRequired", "File must be in {0} format", "PDF");
|
||||
}
|
||||
|
||||
public static IllegalArgumentException createInvalidPageSizeException(String size) {
|
||||
return createIllegalArgumentException(
|
||||
"error.invalidFormat", "Invalid {0} format: {1}", "page size", size);
|
||||
}
|
||||
|
||||
/** Create OCR-related exceptions. */
|
||||
public static IOException createOcrLanguageRequiredException() {
|
||||
return createIOException(
|
||||
"error.optionsNotSpecified", "{0} options are not specified", null, "OCR language");
|
||||
}
|
||||
|
||||
public static IOException createOcrInvalidLanguagesException() {
|
||||
return createIOException(
|
||||
"error.invalidFormat",
|
||||
"Invalid {0} format: {1}",
|
||||
null,
|
||||
"OCR languages",
|
||||
"none of the selected languages are valid");
|
||||
}
|
||||
|
||||
public static IOException createOcrToolsUnavailableException() {
|
||||
return createIOException(
|
||||
"error.toolNotInstalled", "{0} is not installed", null, "OCR tools");
|
||||
}
|
||||
|
||||
/** Create system requirement exceptions. */
|
||||
public static IOException createPythonRequiredForWebpException() {
|
||||
return createIOException(
|
||||
"error.toolRequired", "{0} is required for {1}", null, "Python", "WebP conversion");
|
||||
}
|
||||
|
||||
/** Create file operation exceptions. */
|
||||
public static IOException createFileNotFoundException(String fileId) {
|
||||
return createIOException("error.fileNotFound", "File not found with ID: {0}", null, fileId);
|
||||
}
|
||||
|
||||
public static RuntimeException createPdfaConversionFailedException() {
|
||||
return createRuntimeException(
|
||||
"error.conversionFailed", "{0} conversion failed", null, "PDF/A");
|
||||
}
|
||||
|
||||
public static IllegalArgumentException createInvalidComparatorException() {
|
||||
return createIllegalArgumentException(
|
||||
"error.invalidFormat",
|
||||
"Invalid {0} format: {1}",
|
||||
"comparator",
|
||||
"only 'greater', 'equal', and 'less' are supported");
|
||||
}
|
||||
|
||||
/** Create compression-related exceptions. */
|
||||
public static RuntimeException createMd5AlgorithmException(Exception cause) {
|
||||
return createRuntimeException(
|
||||
"error.algorithmNotAvailable", "{0} algorithm not available", cause, "MD5");
|
||||
}
|
||||
|
||||
public static IllegalArgumentException createCompressionOptionsException() {
|
||||
return createIllegalArgumentException(
|
||||
"error.optionsNotSpecified",
|
||||
"{0} options are not specified",
|
||||
"compression (expected output size and optimize level)");
|
||||
}
|
||||
|
||||
public static IOException createGhostscriptCompressionException() {
|
||||
return createIOException(
|
||||
"error.commandFailed", "{0} command failed", null, "Ghostscript compression");
|
||||
}
|
||||
|
||||
public static IOException createGhostscriptCompressionException(Exception cause) {
|
||||
return createIOException(
|
||||
"error.commandFailed", "{0} command failed", cause, "Ghostscript compression");
|
||||
}
|
||||
|
||||
public static IOException createQpdfCompressionException(Exception cause) {
|
||||
return createIOException("error.commandFailed", "{0} command failed", cause, "QPDF");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an exception indicates a corrupted PDF and wrap it with appropriate message.
|
||||
*
|
||||
* @param e the exception to check
|
||||
* @return the original exception if not PDF corruption, or a new IOException with user-friendly
|
||||
* message
|
||||
*/
|
||||
public static IOException handlePdfException(IOException e) {
|
||||
return handlePdfException(e, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an exception indicates a corrupted PDF and wrap it with appropriate message.
|
||||
*
|
||||
* @param e the exception to check
|
||||
* @param context additional context for the error
|
||||
* @return the original exception if not PDF corruption, or a new IOException with user-friendly
|
||||
* message
|
||||
*/
|
||||
public static IOException handlePdfException(IOException e, String context) {
|
||||
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||
return createPdfCorruptedException(context, e);
|
||||
}
|
||||
|
||||
if (isEncryptionError(e)) {
|
||||
return createPdfEncryptionException(e);
|
||||
}
|
||||
|
||||
if (isPasswordError(e)) {
|
||||
return createPdfPasswordException(e);
|
||||
}
|
||||
|
||||
return e; // Return original exception if no specific handling needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an exception indicates a PDF encryption/decryption error.
|
||||
*
|
||||
* @param e the exception to check
|
||||
* @return true if it's an encryption error, false otherwise
|
||||
*/
|
||||
public static boolean isEncryptionError(IOException e) {
|
||||
String message = e.getMessage();
|
||||
if (message == null) return false;
|
||||
|
||||
return message.contains("BadPaddingException")
|
||||
|| message.contains("Given final block not properly padded")
|
||||
|| message.contains("AES initialization vector not fully read")
|
||||
|| message.contains("Failed to decrypt");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an exception indicates a PDF password error.
|
||||
*
|
||||
* @param e the exception to check
|
||||
* @return true if it's a password error, false otherwise
|
||||
*/
|
||||
public static boolean isPasswordError(IOException e) {
|
||||
String message = e.getMessage();
|
||||
if (message == null) return false;
|
||||
|
||||
return message.contains("password is incorrect")
|
||||
|| message.contains("Password is not provided")
|
||||
|| message.contains("PDF contains an encryption dictionary");
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an exception with appropriate level based on its type.
|
||||
*
|
||||
* @param operation the operation being performed
|
||||
* @param e the exception that occurred
|
||||
*/
|
||||
public static void logException(String operation, Exception e) {
|
||||
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||
log.warn("PDF corruption detected during {}: {}", operation, e.getMessage());
|
||||
} else if (e instanceof IOException
|
||||
&& (isEncryptionError((IOException) e) || isPasswordError((IOException) e))) {
|
||||
log.info("PDF security issue during {}: {}", operation, e.getMessage());
|
||||
} else {
|
||||
log.error("Unexpected error during {}", operation, e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create common validation exceptions. */
|
||||
public static IllegalArgumentException createInvalidArgumentException(String argumentName) {
|
||||
return createIllegalArgumentException(
|
||||
"error.invalidArgument", "Invalid argument: {0}", argumentName);
|
||||
}
|
||||
|
||||
public static IllegalArgumentException createInvalidArgumentException(
|
||||
String argumentName, String value) {
|
||||
return createIllegalArgumentException(
|
||||
"error.invalidFormat", "Invalid {0} format: {1}", argumentName, value);
|
||||
}
|
||||
|
||||
public static IllegalArgumentException createNullArgumentException(String argumentName) {
|
||||
return createIllegalArgumentException(
|
||||
"error.argumentRequired", "{0} must not be null", argumentName);
|
||||
}
|
||||
}
|
||||
@@ -26,88 +26,94 @@ public class FileToPdf {
|
||||
HTMLToPdfRequest request,
|
||||
byte[] fileBytes,
|
||||
String fileName,
|
||||
boolean disableSanitize)
|
||||
boolean disableSanitize,
|
||||
TempFileManager tempFileManager)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
Path tempInputFile = null;
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
if (fileName.endsWith(".html")) {
|
||||
tempInputFile = Files.createTempFile("input_", ".html");
|
||||
String sanitizedHtml =
|
||||
sanitizeHtmlContent(
|
||||
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
||||
Files.write(tempInputFile, sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||
} else if (fileName.endsWith(".zip")) {
|
||||
tempInputFile = Files.createTempFile("input_", ".zip");
|
||||
Files.write(tempInputFile, fileBytes);
|
||||
sanitizeHtmlFilesInZip(tempInputFile, disableSanitize);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported file format: " + fileName);
|
||||
}
|
||||
try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
||||
try (TempFile tempInputFile =
|
||||
new TempFile(
|
||||
tempFileManager,
|
||||
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(weasyprintPath);
|
||||
command.add("-e");
|
||||
command.add("utf-8");
|
||||
command.add("-v");
|
||||
command.add("--pdf-forms");
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
if (fileName.toLowerCase().endsWith(".html")) {
|
||||
String sanitizedHtml =
|
||||
sanitizeHtmlContent(
|
||||
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
||||
Files.write(
|
||||
tempInputFile.getPath(),
|
||||
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
||||
Files.write(tempInputFile.getPath(), fileBytes);
|
||||
sanitizeHtmlFilesInZip(
|
||||
tempInputFile.getPath(), disableSanitize, tempFileManager);
|
||||
} else {
|
||||
throw ExceptionUtils.createHtmlFileRequiredException();
|
||||
}
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(weasyprintPath);
|
||||
command.add("-e");
|
||||
command.add("utf-8");
|
||||
command.add("-v");
|
||||
command.add("--pdf-forms");
|
||||
command.add(tempInputFile.getAbsolutePath());
|
||||
command.add(tempOutputFile.getAbsolutePath());
|
||||
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
} catch (IOException e) {
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
if (pdfBytes.length < 1) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
}
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
return pdfBytes;
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
|
||||
try {
|
||||
return pdfBytes;
|
||||
} catch (Exception e) {
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
|
||||
if (pdfBytes.length < 1) {
|
||||
throw e;
|
||||
}
|
||||
return pdfBytes;
|
||||
}
|
||||
} // tempInputFile auto-closed
|
||||
} // tempOutputFile auto-closed
|
||||
}
|
||||
|
||||
private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) {
|
||||
return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent;
|
||||
}
|
||||
|
||||
private static void sanitizeHtmlFilesInZip(Path zipFilePath, boolean disableSanitize)
|
||||
private static void sanitizeHtmlFilesInZip(
|
||||
Path zipFilePath, boolean disableSanitize, TempFileManager tempFileManager)
|
||||
throws IOException {
|
||||
Path tempUnzippedDir = Files.createTempDirectory("unzipped_");
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(
|
||||
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName()));
|
||||
if (!entry.isDirectory()) {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
if (entry.getName().toLowerCase().endsWith(".html")
|
||||
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
||||
String content = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String sanitizedContent = sanitizeHtmlContent(content, disableSanitize);
|
||||
Files.write(filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
Files.copy(zipIn, filePath);
|
||||
try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(
|
||||
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath =
|
||||
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
|
||||
if (!entry.isDirectory()) {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
if (entry.getName().toLowerCase().endsWith(".html")
|
||||
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
||||
String content =
|
||||
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String sanitizedContent = sanitizeHtmlContent(content, disableSanitize);
|
||||
Files.write(
|
||||
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
Files.copy(zipIn, filePath);
|
||||
}
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// Repack the sanitized files
|
||||
zipDirectory(tempUnzippedDir, zipFilePath);
|
||||
|
||||
// Clean up
|
||||
deleteDirectory(tempUnzippedDir);
|
||||
// Repack the sanitized files
|
||||
zipDirectory(tempUnzippedDir.getPath(), zipFilePath);
|
||||
} // tempUnzippedDir auto-cleaned
|
||||
}
|
||||
|
||||
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -33,7 +34,27 @@ import stirling.software.common.configuration.InstallationPathConfig;
|
||||
public class GeneralUtils {
|
||||
|
||||
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||
File tempFile = Files.createTempFile("temp", null).toFile();
|
||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||
if (customTempDir == null || customTempDir.isEmpty()) {
|
||||
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
||||
}
|
||||
|
||||
File tempFile;
|
||||
|
||||
if (customTempDir != null && !customTempDir.isEmpty()) {
|
||||
Path tempDir = Path.of(customTempDir);
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
}
|
||||
tempFile = Files.createTempFile(tempDir, "stirling-pdf-", null).toFile();
|
||||
} else {
|
||||
Path tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
}
|
||||
tempFile = Files.createTempFile(tempDir, "stirling-pdf-", null).toFile();
|
||||
}
|
||||
|
||||
try (InputStream inputStream = multipartFile.getInputStream();
|
||||
FileOutputStream outputStream = new FileOutputStream(tempFile)) {
|
||||
|
||||
@@ -199,11 +220,11 @@ public class GeneralUtils {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.2f KB", bytes / 1024.0);
|
||||
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** Utility class for detecting and handling PDF-related errors. */
|
||||
public class PdfErrorUtils {
|
||||
|
||||
/**
|
||||
* Checks if an IOException indicates a corrupted PDF file.
|
||||
*
|
||||
* @param e the IOException to check
|
||||
* @return true if the error indicates PDF corruption, false otherwise
|
||||
*/
|
||||
public static boolean isCorruptedPdfError(IOException e) {
|
||||
return isCorruptedPdfError(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any Exception indicates a corrupted PDF file.
|
||||
*
|
||||
* @param e the Exception to check
|
||||
* @return true if the error indicates PDF corruption, false otherwise
|
||||
*/
|
||||
public static boolean isCorruptedPdfError(Exception e) {
|
||||
return isCorruptedPdfError(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error message indicates a corrupted PDF file.
|
||||
*
|
||||
* @param message the error message to check
|
||||
* @return true if the message indicates PDF corruption, false otherwise
|
||||
*/
|
||||
private static boolean isCorruptedPdfError(String message) {
|
||||
if (message == null) return false;
|
||||
|
||||
// Check for common corruption indicators
|
||||
return message.contains("Missing root object specification")
|
||||
|| message.contains("Header doesn't contain versioninfo")
|
||||
|| message.contains("Expected trailer")
|
||||
|| message.contains("Invalid PDF")
|
||||
|| message.contains("Corrupted")
|
||||
|| message.contains("damaged")
|
||||
|| message.contains("Unknown dir object")
|
||||
|| message.contains("Can't dereference COSObject")
|
||||
|| message.contains("parseCOSString string should start with")
|
||||
|| message.contains("ICCBased colorspace array must have a stream")
|
||||
|| message.contains("1-based index not found")
|
||||
|| message.contains("Invalid dictionary, found:")
|
||||
|| message.contains("AES initialization vector not fully read")
|
||||
|| message.contains("BadPaddingException")
|
||||
|| message.contains("Given final block not properly padded")
|
||||
|| message.contains("End-of-File, expected line");
|
||||
}
|
||||
}
|
||||
@@ -42,26 +42,34 @@ public class PdfUtils {
|
||||
|
||||
public static PDRectangle textToPageSize(String size) {
|
||||
switch (size.toUpperCase()) {
|
||||
case "A0":
|
||||
case "A0" -> {
|
||||
return PDRectangle.A0;
|
||||
case "A1":
|
||||
}
|
||||
case "A1" -> {
|
||||
return PDRectangle.A1;
|
||||
case "A2":
|
||||
}
|
||||
case "A2" -> {
|
||||
return PDRectangle.A2;
|
||||
case "A3":
|
||||
}
|
||||
case "A3" -> {
|
||||
return PDRectangle.A3;
|
||||
case "A4":
|
||||
}
|
||||
case "A4" -> {
|
||||
return PDRectangle.A4;
|
||||
case "A5":
|
||||
}
|
||||
case "A5" -> {
|
||||
return PDRectangle.A5;
|
||||
case "A6":
|
||||
}
|
||||
case "A6" -> {
|
||||
return PDRectangle.A6;
|
||||
case "LETTER":
|
||||
}
|
||||
case "LETTER" -> {
|
||||
return PDRectangle.LETTER;
|
||||
case "LEGAL":
|
||||
}
|
||||
case "LEGAL" -> {
|
||||
return PDRectangle.LEGAL;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid standard page size: " + size);
|
||||
}
|
||||
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +143,17 @@ public class PdfUtils {
|
||||
int DPI,
|
||||
String filename)
|
||||
throws IOException, Exception {
|
||||
|
||||
// Validate and limit DPI to prevent excessive memory usage
|
||||
final int MAX_SAFE_DPI = 500; // Maximum safe DPI to prevent memory issues
|
||||
if (DPI > MAX_SAFE_DPI) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.dpiExceedsLimit",
|
||||
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
|
||||
DPI,
|
||||
MAX_SAFE_DPI);
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
@@ -158,7 +177,21 @@ public class PdfUtils {
|
||||
writer.prepareWriteSequence(null);
|
||||
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
.contains("Maximum size of image exceeded")) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pageTooBigForDpi",
|
||||
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
||||
i + 1,
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||
}
|
||||
|
||||
@@ -190,7 +223,20 @@ public class PdfUtils {
|
||||
PdfImageDimensionValue dimension = pageSizes.get(settings);
|
||||
if (dimension == null) {
|
||||
// Render the image to get the dimensions
|
||||
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
try {
|
||||
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
.contains("Maximum size of image exceeded")) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pageTooBigExceedsArray",
|
||||
"PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).",
|
||||
i + 1,
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
pdfSizeImageIndex = i;
|
||||
dimension =
|
||||
new PdfImageDimensionValue(
|
||||
@@ -218,7 +264,20 @@ public class PdfUtils {
|
||||
if (firstImageAlreadyRendered && i == 0) {
|
||||
pageImage = pdfSizeImage;
|
||||
} else {
|
||||
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
try {
|
||||
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
.contains("Maximum size of image exceeded")) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pageTooBigForDpi",
|
||||
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
||||
i + 1,
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the x-coordinate to center the image
|
||||
@@ -238,7 +297,20 @@ public class PdfUtils {
|
||||
// Zip the images and return as byte array
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pageTooBigForDpi",
|
||||
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
||||
i + 1,
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, imageType, baosImage);
|
||||
|
||||
@@ -276,7 +348,19 @@ public class PdfUtils {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||
BufferedImage bim;
|
||||
try {
|
||||
bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pageTooBigFor300Dpi",
|
||||
"PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.",
|
||||
page + 1);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
PDPage originalPage = document.getPage(page);
|
||||
|
||||
float width = originalPage.getMediaBox().getWidth();
|
||||
@@ -349,7 +433,7 @@ public class PdfUtils {
|
||||
}
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
doc.save(byteArrayOutputStream);
|
||||
log.info("PDF successfully saved to byte array");
|
||||
log.debug("PDF successfully saved to byte array");
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -495,8 +579,7 @@ public class PdfUtils {
|
||||
case "less":
|
||||
return actualPageCount < pageCount;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,16 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getCalibreSessionLimit();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getGhostscriptSessionLimit();
|
||||
case OCR_MY_PDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getOcrMyPdfSessionLimit();
|
||||
};
|
||||
|
||||
long timeoutMinutes =
|
||||
@@ -128,6 +138,16 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getCalibreTimeoutMinutes();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getGhostscriptTimeoutMinutes();
|
||||
case OCR_MY_PDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getOcrMyPdfTimeoutMinutes();
|
||||
};
|
||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
});
|
||||
@@ -278,7 +298,9 @@ public class ProcessExecutor {
|
||||
INSTALL_APP,
|
||||
CALIBRE,
|
||||
TESSERACT,
|
||||
QPDF
|
||||
QPDF,
|
||||
GHOSTSCRIPT,
|
||||
OCR_MY_PDF
|
||||
}
|
||||
|
||||
public class ProcessExecutorResult {
|
||||
|
||||
@@ -19,6 +19,7 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|| requestURI.endsWith(".txt")
|
||||
|| requestURI.endsWith(".webmanifest")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/info/status");
|
||||
}
|
||||
@@ -35,6 +36,7 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|| requestURI.endsWith(".css")
|
||||
|| requestURI.endsWith(".txt")
|
||||
|| requestURI.endsWith(".map")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith("popularity.txt")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Utility class to access Spring managed beans from non-Spring managed classes. This is especially
|
||||
* useful for classes that are instantiated by frameworks or created dynamically.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SpringContextHolder implements ApplicationContextAware {
|
||||
|
||||
private static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
SpringContextHolder.applicationContext = applicationContext;
|
||||
log.debug("Spring context holder initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Spring bean by class type
|
||||
*
|
||||
* @param <T> The bean type
|
||||
* @param beanClass The bean class
|
||||
* @return The bean instance, or null if not found
|
||||
*/
|
||||
public static <T> T getBean(Class<T> beanClass) {
|
||||
if (applicationContext == null) {
|
||||
log.warn(
|
||||
"Application context not initialized when attempting to get bean of type {}",
|
||||
beanClass.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return applicationContext.getBean(beanClass);
|
||||
} catch (BeansException e) {
|
||||
log.error("Error getting bean of type {}: {}", beanClass.getName(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Spring bean by name
|
||||
*
|
||||
* @param <T> The bean type
|
||||
* @param beanName The bean name
|
||||
* @return The bean instance, or null if not found
|
||||
*/
|
||||
public static <T> T getBean(String beanName) {
|
||||
if (applicationContext == null) {
|
||||
log.warn(
|
||||
"Application context not initialized when attempting to get bean '{}'",
|
||||
beanName);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
T bean = (T) applicationContext.getBean(beanName);
|
||||
return bean;
|
||||
} catch (BeansException e) {
|
||||
log.error("Error getting bean '{}': {}", beanName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the application context is initialized
|
||||
*
|
||||
* @return true if initialized, false otherwise
|
||||
*/
|
||||
public static boolean isInitialized() {
|
||||
return applicationContext != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* A wrapper class for a temporary directory that implements AutoCloseable. Can be used with
|
||||
* try-with-resources for automatic cleanup.
|
||||
*/
|
||||
@Slf4j
|
||||
public class TempDirectory implements AutoCloseable {
|
||||
|
||||
private final TempFileManager manager;
|
||||
private final Path directory;
|
||||
|
||||
public TempDirectory(TempFileManager manager) throws IOException {
|
||||
this.manager = manager;
|
||||
this.directory = manager.createTempDirectory();
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public String getAbsolutePath() {
|
||||
return directory.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return java.nio.file.Files.exists(directory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
manager.deleteTempDirectory(directory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TempDirectory{" + directory.toAbsolutePath() + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* A wrapper class for a temporary file that implements AutoCloseable. Can be used with
|
||||
* try-with-resources for automatic cleanup.
|
||||
*/
|
||||
@Slf4j
|
||||
public class TempFile implements AutoCloseable {
|
||||
|
||||
private final TempFileManager manager;
|
||||
private final File file;
|
||||
|
||||
public TempFile(TempFileManager manager, String suffix) throws IOException {
|
||||
this.manager = manager;
|
||||
this.file = manager.createTempFile(suffix);
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return file.toPath();
|
||||
}
|
||||
|
||||
public String getAbsolutePath() {
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
manager.deleteTempFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TempFile{" + file.getAbsolutePath() + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
/**
|
||||
* Service for managing temporary files in Stirling-PDF. Provides methods for creating, tracking,
|
||||
* and cleaning up temporary files.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TempFileManager {
|
||||
|
||||
private final TempFileRegistry registry;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
/**
|
||||
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
|
||||
* with the registry.
|
||||
*
|
||||
* @param suffix The suffix for the temporary file
|
||||
* @return The created temporary file
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public File createTempFile(String suffix) throws IOException {
|
||||
ApplicationProperties.TempFileManagement tempFiles =
|
||||
applicationProperties.getSystem().getTempFileManagement();
|
||||
Path tempFilePath;
|
||||
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||
Path tempDir = Path.of(customTempDirectory);
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
}
|
||||
tempFilePath = Files.createTempFile(tempDir, tempFiles.getPrefix(), suffix);
|
||||
} else {
|
||||
tempFilePath = Files.createTempFile(tempFiles.getPrefix(), suffix);
|
||||
}
|
||||
File tempFile = tempFilePath.toFile();
|
||||
return registry.register(tempFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary directory with the Stirling-PDF prefix. The directory is automatically
|
||||
* registered with the registry.
|
||||
*
|
||||
* @return The created temporary directory
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public Path createTempDirectory() throws IOException {
|
||||
ApplicationProperties.TempFileManagement tempFiles =
|
||||
applicationProperties.getSystem().getTempFileManagement();
|
||||
Path tempDirPath;
|
||||
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||
Path tempDir = Path.of(customTempDirectory);
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
}
|
||||
tempDirPath = Files.createTempDirectory(tempDir, tempFiles.getPrefix());
|
||||
} else {
|
||||
tempDirPath = Files.createTempDirectory(tempFiles.getPrefix());
|
||||
}
|
||||
return registry.registerDirectory(tempDirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MultipartFile to a temporary File and register it. This is a wrapper around
|
||||
* GeneralUtils.convertMultipartFileToFile that ensures the created temp file is registered.
|
||||
*
|
||||
* @param multipartFile The MultipartFile to convert
|
||||
* @return The created temporary file
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||
File tempFile = GeneralUtils.convertMultipartFileToFile(multipartFile);
|
||||
return registry.register(tempFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a temporary file and unregister it from the registry.
|
||||
*
|
||||
* @param file The file to delete
|
||||
* @return true if the file was deleted successfully, false otherwise
|
||||
*/
|
||||
public boolean deleteTempFile(File file) {
|
||||
if (file != null && file.exists()) {
|
||||
boolean deleted = file.delete();
|
||||
if (deleted) {
|
||||
registry.unregister(file);
|
||||
log.debug("Deleted temp file: {}", file.getAbsolutePath());
|
||||
} else {
|
||||
log.warn("Failed to delete temp file: {}", file.getAbsolutePath());
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a temporary file and unregister it from the registry.
|
||||
*
|
||||
* @param path The path to delete
|
||||
* @return true if the file was deleted successfully, false otherwise
|
||||
*/
|
||||
public boolean deleteTempFile(Path path) {
|
||||
if (path != null) {
|
||||
try {
|
||||
boolean deleted = Files.deleteIfExists(path);
|
||||
if (deleted) {
|
||||
registry.unregister(path);
|
||||
log.debug("Deleted temp file: {}", path.toString());
|
||||
} else {
|
||||
log.debug("Temp file already deleted or does not exist: {}", path.toString());
|
||||
}
|
||||
return deleted;
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp file: {}", path.toString(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a temporary directory and all its contents.
|
||||
*
|
||||
* @param directory The directory to delete
|
||||
*/
|
||||
public void deleteTempDirectory(Path directory) {
|
||||
if (directory != null && Files.isDirectory(directory)) {
|
||||
try {
|
||||
GeneralUtils.deleteDirectory(directory);
|
||||
log.debug("Deleted temp directory: {}", directory.toString());
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp directory: {}", directory.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an existing file with the registry.
|
||||
*
|
||||
* @param file The file to register
|
||||
* @return The same file for method chaining
|
||||
*/
|
||||
public File register(File file) {
|
||||
if (file != null && file.exists()) {
|
||||
return registry.register(file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old temporary files based on age.
|
||||
*
|
||||
* @param maxAgeMillis Maximum age in milliseconds for temp files
|
||||
* @return Number of files deleted
|
||||
*/
|
||||
public int cleanupOldTempFiles(long maxAgeMillis) {
|
||||
int deletedCount = 0;
|
||||
|
||||
// Get files older than max age
|
||||
Set<Path> oldFiles = registry.getFilesOlderThan(maxAgeMillis);
|
||||
|
||||
// Delete each old file
|
||||
for (Path file : oldFiles) {
|
||||
if (deleteTempFile(file)) {
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Cleaned up {} old temporary files", deletedCount);
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum age for temporary files in milliseconds.
|
||||
*
|
||||
* @return Maximum age in milliseconds
|
||||
*/
|
||||
public long getMaxAgeMillis() {
|
||||
long maxAgeHours =
|
||||
applicationProperties.getSystem().getTempFileManagement().getMaxAgeHours();
|
||||
return Duration.ofHours(maxAgeHours).toMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique temporary file name with the Stirling-PDF prefix.
|
||||
*
|
||||
* @param type Type identifier for the temp file
|
||||
* @param extension File extension (without the dot)
|
||||
* @return A unique temporary file name
|
||||
*/
|
||||
public String generateTempFileName(String type, String extension) {
|
||||
String tempFilePrefix =
|
||||
applicationProperties.getSystem().getTempFileManagement().getPrefix();
|
||||
String uuid = UUID.randomUUID().toString().substring(0, 8);
|
||||
return tempFilePrefix + type + "-" + uuid + "." + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a known LibreOffice temporary directory. This is used when integrating with
|
||||
* LibreOffice for file conversions.
|
||||
*
|
||||
* @return The LibreOffice temp directory
|
||||
* @throws IOException If directory creation fails
|
||||
*/
|
||||
public Path registerLibreOfficeTempDir() throws IOException {
|
||||
ApplicationProperties.TempFileManagement tempFiles =
|
||||
applicationProperties.getSystem().getTempFileManagement();
|
||||
Path loTempDir;
|
||||
String libreOfficeTempDir = tempFiles.getLibreofficeDir();
|
||||
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||
|
||||
// First check if explicitly configured
|
||||
if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) {
|
||||
loTempDir = Path.of(libreOfficeTempDir);
|
||||
}
|
||||
// Next check if we have a custom temp directory
|
||||
else if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||
loTempDir = Path.of(customTempDirectory, "libreoffice");
|
||||
}
|
||||
// Fall back to system temp dir with our application prefix
|
||||
else {
|
||||
loTempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf-libreoffice");
|
||||
}
|
||||
|
||||
if (!Files.exists(loTempDir)) {
|
||||
Files.createDirectories(loTempDir);
|
||||
}
|
||||
|
||||
return registry.registerDirectory(loTempDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Central registry for tracking temporary files created by Stirling-PDF. Maintains a thread-safe
|
||||
* collection of paths with their creation timestamps.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TempFileRegistry {
|
||||
|
||||
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||
private final Set<Path> thirdPartyTempFiles =
|
||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
private final Set<Path> tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
/**
|
||||
* Register a temporary file with the registry.
|
||||
*
|
||||
* @param file The temporary file to track
|
||||
* @return The same file for method chaining
|
||||
*/
|
||||
public File register(File file) {
|
||||
if (file != null) {
|
||||
registeredFiles.put(file.toPath(), Instant.now());
|
||||
log.debug("Registered temp file: {}", file.getAbsolutePath());
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a temporary path with the registry.
|
||||
*
|
||||
* @param path The temporary path to track
|
||||
* @return The same path for method chaining
|
||||
*/
|
||||
public Path register(Path path) {
|
||||
if (path != null) {
|
||||
registeredFiles.put(path, Instant.now());
|
||||
log.debug("Registered temp path: {}", path.toString());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a temporary directory to be cleaned up.
|
||||
*
|
||||
* @param directory Directory to register
|
||||
* @return The same directory for method chaining
|
||||
*/
|
||||
public Path registerDirectory(Path directory) {
|
||||
if (directory != null && Files.isDirectory(directory)) {
|
||||
tempDirectories.add(directory);
|
||||
log.debug("Registered temp directory: {}", directory.toString());
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a third-party temporary file that requires special handling.
|
||||
*
|
||||
* @param file The third-party temp file
|
||||
* @return The same file for method chaining
|
||||
*/
|
||||
public File registerThirdParty(File file) {
|
||||
if (file != null) {
|
||||
thirdPartyTempFiles.add(file.toPath());
|
||||
log.debug("Registered third-party temp file: {}", file.getAbsolutePath());
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a file from the registry.
|
||||
*
|
||||
* @param file The file to unregister
|
||||
*/
|
||||
public void unregister(File file) {
|
||||
if (file != null) {
|
||||
registeredFiles.remove(file.toPath());
|
||||
thirdPartyTempFiles.remove(file.toPath());
|
||||
log.debug("Unregistered temp file: {}", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a path from the registry.
|
||||
*
|
||||
* @param path The path to unregister
|
||||
*/
|
||||
public void unregister(Path path) {
|
||||
if (path != null) {
|
||||
registeredFiles.remove(path);
|
||||
thirdPartyTempFiles.remove(path);
|
||||
log.debug("Unregistered temp path: {}", path.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered temporary files.
|
||||
*
|
||||
* @return Set of registered file paths
|
||||
*/
|
||||
public Set<Path> getAllRegisteredFiles() {
|
||||
return registeredFiles.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get temporary files older than the specified duration in milliseconds.
|
||||
*
|
||||
* @param maxAgeMillis Maximum age in milliseconds
|
||||
* @return Set of paths older than the specified age
|
||||
*/
|
||||
public Set<Path> getFilesOlderThan(long maxAgeMillis) {
|
||||
Instant cutoffTime = Instant.now().minusMillis(maxAgeMillis);
|
||||
return registeredFiles.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().isBefore(cutoffTime))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered third-party temporary files.
|
||||
*
|
||||
* @return Set of third-party file paths
|
||||
*/
|
||||
public Set<Path> getThirdPartyTempFiles() {
|
||||
return thirdPartyTempFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered temporary directories.
|
||||
*
|
||||
* @return Set of temporary directory paths
|
||||
*/
|
||||
public Set<Path> getTempDirectories() {
|
||||
return tempDirectories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is registered in the registry.
|
||||
*
|
||||
* @param file The file to check
|
||||
* @return True if the file is registered, false otherwise
|
||||
*/
|
||||
public boolean contains(File file) {
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
Path path = file.toPath();
|
||||
return registeredFiles.containsKey(path) || thirdPartyTempFiles.contains(path);
|
||||
}
|
||||
|
||||
/** Clear all registry data. */
|
||||
public void clear() {
|
||||
registeredFiles.clear();
|
||||
thirdPartyTempFiles.clear();
|
||||
tempDirectories.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Utility class for handling temporary files with proper cleanup. Provides helper methods and
|
||||
* wrappers to ensure temp files are properly cleaned up.
|
||||
*/
|
||||
@Slf4j
|
||||
public class TempFileUtil {
|
||||
|
||||
/**
|
||||
* A collection of temporary files that implements AutoCloseable. All files in the collection
|
||||
* are cleaned up when close() is called.
|
||||
*/
|
||||
public static class TempFileCollection implements AutoCloseable {
|
||||
private final TempFileManager manager;
|
||||
private final List<File> tempFiles = new ArrayList<>();
|
||||
|
||||
public TempFileCollection(TempFileManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public File addTempFile(String suffix) throws IOException {
|
||||
File file = manager.createTempFile(suffix);
|
||||
tempFiles.add(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
public List<File> getFiles() {
|
||||
return new ArrayList<>(tempFiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (File file : tempFiles) {
|
||||
manager.deleteTempFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function with a temporary file, ensuring cleanup in a finally block.
|
||||
*
|
||||
* @param <R> The return type of the function
|
||||
* @param tempFileManager The temp file manager
|
||||
* @param suffix File suffix (e.g., ".pdf")
|
||||
* @param function The function to execute with the temp file
|
||||
* @return The result of the function
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public static <R> R withTempFile(
|
||||
TempFileManager tempFileManager, String suffix, Function<File, R> function)
|
||||
throws IOException {
|
||||
File tempFile = tempFileManager.createTempFile(suffix);
|
||||
try {
|
||||
return function.apply(tempFile);
|
||||
} finally {
|
||||
tempFileManager.deleteTempFile(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function with multiple temporary files, ensuring cleanup in a finally block.
|
||||
*
|
||||
* @param <R> The return type of the function
|
||||
* @param tempFileManager The temp file manager
|
||||
* @param count Number of temp files to create
|
||||
* @param suffix File suffix (e.g., ".pdf")
|
||||
* @param function The function to execute with the temp files
|
||||
* @return The result of the function
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public static <R> R withMultipleTempFiles(
|
||||
TempFileManager tempFileManager,
|
||||
int count,
|
||||
String suffix,
|
||||
Function<List<File>, R> function)
|
||||
throws IOException {
|
||||
List<File> tempFiles = new ArrayList<>(count);
|
||||
try {
|
||||
for (int i = 0; i < count; i++) {
|
||||
tempFiles.add(tempFileManager.createTempFile(suffix));
|
||||
}
|
||||
return function.apply(tempFiles);
|
||||
} finally {
|
||||
for (File file : tempFiles) {
|
||||
tempFileManager.deleteTempFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely delete a list of temporary files, logging any errors.
|
||||
*
|
||||
* @param files The list of files to delete
|
||||
*/
|
||||
public static void safeDeleteFiles(List<Path> files) {
|
||||
if (files == null) return;
|
||||
|
||||
for (Path file : files) {
|
||||
if (file == null) continue;
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
log.debug("Deleted temp file: {}", file);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp file: {}", file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an already created temp file with the registry. Use this for files created outside
|
||||
* of TempFileManager.
|
||||
*
|
||||
* @param tempFileManager The temp file manager
|
||||
* @param file The file to register
|
||||
* @return The registered file
|
||||
*/
|
||||
public static File registerExistingTempFile(TempFileManager tempFileManager, File file) {
|
||||
if (tempFileManager != null && file != null && file.exists()) {
|
||||
return tempFileManager.register(file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class ValidationUtil {
|
||||
|
||||
public static boolean isStringEmpty(String input) {
|
||||
return input == null || input.isBlank();
|
||||
}
|
||||
|
||||
public static boolean isCollectionEmpty(Collection<String> input) {
|
||||
return input == null || input.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,12 @@ import io.github.pixee.security.Filenames;
|
||||
|
||||
public class WebResponseUtils {
|
||||
|
||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
||||
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||
ByteArrayOutputStream baos, String docName) throws IOException {
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
|
||||
}
|
||||
|
||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
||||
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||
ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
|
||||
}
|
||||
@@ -44,8 +44,7 @@ public class WebResponseUtils {
|
||||
headers.setContentType(mediaType);
|
||||
headers.setContentLength(bytes.length);
|
||||
String encodedDocName =
|
||||
URLEncoder.encode(docName, StandardCharsets.UTF_8.toString())
|
||||
.replaceAll("\\+", "%20");
|
||||
URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
headers.setContentDispositionFormData("attachment", encodedDocName);
|
||||
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
||||
}
|
||||
@@ -61,9 +60,8 @@ public class WebResponseUtils {
|
||||
// Open Byte Array and save document to it
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
// Close the document
|
||||
document.close();
|
||||
|
||||
return boasToWebResponse(baos, docName);
|
||||
return baosToWebResponse(baos, docName);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -108,6 +108,12 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
||||
} catch (IllegalArgumentException ie) {
|
||||
log.info("text not supported by font ");
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} catch (UnsupportedOperationException ue) {
|
||||
log.info(
|
||||
"font does not support encoding operation: {} for text: '{}'",
|
||||
font.getClass().getSimpleName(),
|
||||
unicodeText);
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} finally {
|
||||
// if any other font is not supported, then replace default character *
|
||||
if (font == null) {
|
||||
|
||||
+40
-46
@@ -33,7 +33,6 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import stirling.software.common.aop.AutoJobAspect;
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
@@ -45,67 +44,64 @@ import stirling.software.common.service.ResourceMonitor;
|
||||
class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
private AutoJobAspect autoJobAspect;
|
||||
|
||||
|
||||
@Mock
|
||||
private JobExecutorService jobExecutorService;
|
||||
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
|
||||
@Mock
|
||||
private FileOrUploadService fileOrUploadService;
|
||||
|
||||
|
||||
@Mock
|
||||
private FileStorage fileStorage;
|
||||
|
||||
@Mock
|
||||
private WebSocketProgressController webSocketSender;
|
||||
|
||||
|
||||
|
||||
@Mock
|
||||
private ResourceMonitor resourceMonitor;
|
||||
|
||||
|
||||
@Mock
|
||||
private JobQueue jobQueue;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
autoJobAspect = new AutoJobAspect(
|
||||
jobExecutorService,
|
||||
request,
|
||||
fileOrUploadService,
|
||||
fileStorage,
|
||||
webSocketSender
|
||||
fileStorage
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Mock
|
||||
private ProceedingJoinPoint joinPoint;
|
||||
|
||||
|
||||
@Mock
|
||||
private AutoJobPostMapping autoJobPostMapping;
|
||||
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Supplier<Object>> workCaptor;
|
||||
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Boolean> asyncCaptor;
|
||||
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Long> timeoutCaptor;
|
||||
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Boolean> queueableCaptor;
|
||||
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Integer> resourceWeightCaptor;
|
||||
|
||||
|
||||
@Test
|
||||
void shouldExecuteWithCustomParameters() throws Throwable {
|
||||
// Given
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileId("test-file-id");
|
||||
Object[] args = new Object[] { pdfFile };
|
||||
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(args);
|
||||
when(request.getParameter("async")).thenReturn("true");
|
||||
when(autoJobPostMapping.timeout()).thenReturn(60000L);
|
||||
@@ -113,37 +109,37 @@ class AutoJobPostMappingIntegrationTest {
|
||||
when(autoJobPostMapping.trackProgress()).thenReturn(true);
|
||||
when(autoJobPostMapping.queueable()).thenReturn(true);
|
||||
when(autoJobPostMapping.resourceWeight()).thenReturn(75);
|
||||
|
||||
|
||||
MultipartFile mockFile = mock(MultipartFile.class);
|
||||
when(fileStorage.retrieveFile("test-file-id")).thenReturn(mockFile);
|
||||
|
||||
|
||||
|
||||
when(jobExecutorService.runJobGeneric(
|
||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||
.thenReturn(ResponseEntity.ok("success"));
|
||||
|
||||
|
||||
// When
|
||||
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(ResponseEntity.ok("success"), result);
|
||||
|
||||
|
||||
verify(jobExecutorService).runJobGeneric(
|
||||
asyncCaptor.capture(),
|
||||
workCaptor.capture(),
|
||||
workCaptor.capture(),
|
||||
timeoutCaptor.capture(),
|
||||
queueableCaptor.capture(),
|
||||
resourceWeightCaptor.capture());
|
||||
|
||||
|
||||
assertTrue(asyncCaptor.getValue(), "Async should be true");
|
||||
assertEquals(60000L, timeoutCaptor.getValue(), "Timeout should be 60000ms");
|
||||
assertTrue(queueableCaptor.getValue(), "Queueable should be true");
|
||||
assertEquals(75, resourceWeightCaptor.getValue(), "Resource weight should be 75");
|
||||
|
||||
|
||||
// Test that file was resolved
|
||||
assertNotNull(pdfFile.getFileInput(), "File input should be set");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldRetryOnError() throws Throwable {
|
||||
// Given
|
||||
@@ -154,12 +150,12 @@ class AutoJobPostMappingIntegrationTest {
|
||||
when(autoJobPostMapping.trackProgress()).thenReturn(false);
|
||||
when(autoJobPostMapping.queueable()).thenReturn(false);
|
||||
when(autoJobPostMapping.resourceWeight()).thenReturn(50);
|
||||
|
||||
|
||||
// First call throws exception, second succeeds
|
||||
when(joinPoint.proceed(any()))
|
||||
.thenThrow(new RuntimeException("First attempt failed"))
|
||||
.thenReturn(ResponseEntity.ok("retry succeeded"));
|
||||
|
||||
|
||||
// Mock jobExecutorService to execute the work immediately
|
||||
when(jobExecutorService.runJobGeneric(
|
||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||
@@ -167,48 +163,46 @@ class AutoJobPostMappingIntegrationTest {
|
||||
Supplier<Object> work = invocation.getArgument(1);
|
||||
return work.get();
|
||||
});
|
||||
|
||||
|
||||
// When
|
||||
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(ResponseEntity.ok("retry succeeded"), result);
|
||||
|
||||
|
||||
// Verify that proceed was called twice (initial attempt + 1 retry)
|
||||
verify(joinPoint, times(2)).proceed(any());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldHandlePDFFileWithAsyncRequests() throws Throwable {
|
||||
// Given
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileInput(mock(MultipartFile.class));
|
||||
Object[] args = new Object[] { pdfFile };
|
||||
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(args);
|
||||
when(request.getParameter("async")).thenReturn("true");
|
||||
when(autoJobPostMapping.retryCount()).thenReturn(1);
|
||||
|
||||
|
||||
when(fileStorage.storeFile(any(MultipartFile.class))).thenReturn("stored-file-id");
|
||||
when(fileStorage.retrieveFile("stored-file-id")).thenReturn(mock(MultipartFile.class));
|
||||
|
||||
|
||||
// Mock job executor to return a successful response
|
||||
when(jobExecutorService.runJobGeneric(
|
||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||
.thenReturn(ResponseEntity.ok("success"));
|
||||
|
||||
|
||||
// When
|
||||
autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals("stored-file-id", pdfFile.getFileId(),
|
||||
assertEquals("stored-file-id", pdfFile.getFileId(),
|
||||
"FileId should be set to the stored file id");
|
||||
assertNotNull(pdfFile.getFileInput(), "FileInput should be replaced with persistent file");
|
||||
|
||||
|
||||
// Verify storage operations
|
||||
verify(fileStorage).storeFile(any(MultipartFile.class));
|
||||
verify(fileStorage).retrieveFile("stored-file-id");
|
||||
}
|
||||
}
|
||||
|
||||
// Move PDFFileTest to its own class file if needed
|
||||
@@ -1,227 +0,0 @@
|
||||
package stirling.software.common.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.TaskManager;
|
||||
|
||||
class JobControllerTest {
|
||||
|
||||
@Mock
|
||||
private TaskManager taskManager;
|
||||
|
||||
@Mock
|
||||
private FileStorage fileStorage;
|
||||
|
||||
@InjectMocks
|
||||
private JobController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobStatus_ExistingJob() {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals(mockResult, response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobStatus_NonExistentJob() {
|
||||
// Arrange
|
||||
String jobId = "non-existent-job";
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobResult_CompletedSuccessfulWithObject() {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.setComplete(true);
|
||||
String resultObject = "Test result";
|
||||
mockResult.completeWithResult(resultObject);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals(resultObject, response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobResult_CompletedSuccessfulWithFile() throws Exception {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
String contentType = "application/pdf";
|
||||
byte[] fileContent = "Test file content".getBytes();
|
||||
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals(contentType, response.getHeaders().getFirst("Content-Type"));
|
||||
assertTrue(response.getHeaders().getFirst("Content-Disposition").contains(originalFileName));
|
||||
assertEquals(fileContent, response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobResult_CompletedWithError() {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
String errorMessage = "Test error";
|
||||
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.failWithError(errorMessage);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
assertTrue(response.getBody().toString().contains(errorMessage));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobResult_IncompleteJob() {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.setComplete(false);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
assertTrue(response.getBody().toString().contains("not complete"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobResult_NonExistentJob() {
|
||||
// Arrange
|
||||
String jobId = "non-existent-job";
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobResult_ErrorRetrievingFile() throws Exception {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
String contentType = "application/pdf";
|
||||
|
||||
JobResult mockResult = new JobResult();
|
||||
mockResult.setJobId(jobId);
|
||||
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
|
||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||
when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||
assertTrue(response.getBody().toString().contains("Error retrieving file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobStats() {
|
||||
// Arrange
|
||||
JobStats mockStats = JobStats.builder()
|
||||
.totalJobs(10)
|
||||
.activeJobs(3)
|
||||
.completedJobs(7)
|
||||
.build();
|
||||
|
||||
when(taskManager.getJobStats()).thenReturn(mockStats);
|
||||
|
||||
// Act
|
||||
ResponseEntity<JobStats> response = controller.getJobStats();
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals(mockStats, response.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCleanupOldJobs() {
|
||||
// Arrange
|
||||
when(taskManager.getJobStats())
|
||||
.thenReturn(JobStats.builder().totalJobs(10).build())
|
||||
.thenReturn(JobStats.builder().totalJobs(7).build());
|
||||
|
||||
// Act
|
||||
ResponseEntity<?> response = controller.cleanupOldJobs();
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
||||
assertEquals("Cleanup complete", responseBody.get("message"));
|
||||
assertEquals(3, responseBody.get("removedJobs"));
|
||||
assertEquals(7, responseBody.get("remainingJobs"));
|
||||
|
||||
verify(taskManager).cleanupOldJobs();
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package stirling.software.common.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
|
||||
class WebSocketProgressControllerTest {
|
||||
|
||||
@Mock
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@InjectMocks
|
||||
private WebSocketProgressController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendProgress_WithMessagingTemplate() {
|
||||
// Arrange
|
||||
String jobId = "test-job-id";
|
||||
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||
|
||||
// Act
|
||||
controller.sendProgress(jobId, progress);
|
||||
|
||||
// Assert
|
||||
verify(messagingTemplate).convertAndSend("/topic/progress/" + jobId, progress);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendProgress_WithNullMessagingTemplate() {
|
||||
// Arrange
|
||||
WebSocketProgressController controllerWithNullTemplate = new WebSocketProgressController();
|
||||
String jobId = "test-job-id";
|
||||
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||
|
||||
// Act - should not throw exception even with null template
|
||||
controllerWithNullTemplate.sendProgress(jobId, progress);
|
||||
|
||||
// No assertion needed - test passes if no exception is thrown
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetMessagingTemplate() {
|
||||
// Arrange
|
||||
WebSocketProgressController newController = new WebSocketProgressController();
|
||||
SimpMessagingTemplate newTemplate = mock(SimpMessagingTemplate.class);
|
||||
|
||||
// Act
|
||||
newController.setMessagingTemplate(newTemplate);
|
||||
String jobId = "test-job-id";
|
||||
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||
newController.sendProgress(jobId, progress);
|
||||
|
||||
// Assert
|
||||
verify(newTemplate).convertAndSend("/topic/progress/" + jobId, progress);
|
||||
}
|
||||
}
|
||||
@@ -23,20 +23,20 @@ class FileStorageTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
|
||||
@Mock
|
||||
private FileOrUploadService fileOrUploadService;
|
||||
|
||||
|
||||
@InjectMocks
|
||||
private FileStorage fileStorage;
|
||||
|
||||
|
||||
private MultipartFile mockFile;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
ReflectionTestUtils.setField(fileStorage, "tempDirPath", tempDir.toString());
|
||||
|
||||
|
||||
// Create a mock MultipartFile
|
||||
mockFile = mock(MultipartFile.class);
|
||||
when(mockFile.getOriginalFilename()).thenReturn("test.pdf");
|
||||
@@ -48,38 +48,38 @@ class FileStorageTest {
|
||||
// Arrange
|
||||
byte[] fileContent = "Test PDF content".getBytes();
|
||||
when(mockFile.getBytes()).thenReturn(fileContent);
|
||||
|
||||
|
||||
// Set up mock to handle transferTo by writing the file
|
||||
doAnswer(invocation -> {
|
||||
java.io.File file = invocation.getArgument(0);
|
||||
Files.write(file.toPath(), fileContent);
|
||||
return null;
|
||||
}).when(mockFile).transferTo(any(java.io.File.class));
|
||||
|
||||
|
||||
// Act
|
||||
String fileId = fileStorage.storeFile(mockFile);
|
||||
|
||||
|
||||
// Assert
|
||||
assertNotNull(fileId);
|
||||
assertTrue(Files.exists(tempDir.resolve(fileId)));
|
||||
verify(mockFile).transferTo(any(java.io.File.class));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testStoreBytes() throws IOException {
|
||||
// Arrange
|
||||
byte[] fileContent = "Test PDF content".getBytes();
|
||||
String originalName = "test.pdf";
|
||||
|
||||
|
||||
// Act
|
||||
String fileId = fileStorage.storeBytes(fileContent, originalName);
|
||||
|
||||
|
||||
// Assert
|
||||
assertNotNull(fileId);
|
||||
assertTrue(Files.exists(tempDir.resolve(fileId)));
|
||||
assertArrayEquals(fileContent, Files.readAllBytes(tempDir.resolve(fileId)));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testRetrieveFile() throws IOException {
|
||||
// Arrange
|
||||
@@ -87,19 +87,19 @@ class FileStorageTest {
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
|
||||
MultipartFile expectedFile = mock(MultipartFile.class);
|
||||
when(fileOrUploadService.toMockMultipartFile(eq(fileId), eq(fileContent)))
|
||||
.thenReturn(expectedFile);
|
||||
|
||||
|
||||
// Act
|
||||
MultipartFile result = fileStorage.retrieveFile(fileId);
|
||||
|
||||
|
||||
// Assert
|
||||
assertSame(expectedFile, result);
|
||||
verify(fileOrUploadService).toMockMultipartFile(eq(fileId), eq(fileContent));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testRetrieveBytes() throws IOException {
|
||||
// Arrange
|
||||
@@ -107,32 +107,32 @@ class FileStorageTest {
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
|
||||
// Act
|
||||
byte[] result = fileStorage.retrieveBytes(fileId);
|
||||
|
||||
|
||||
// Assert
|
||||
assertArrayEquals(fileContent, result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testRetrieveFile_FileNotFound() {
|
||||
// Arrange
|
||||
String nonExistentFileId = "non-existent-file";
|
||||
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IOException.class, () -> fileStorage.retrieveFile(nonExistentFileId));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testRetrieveBytes_FileNotFound() {
|
||||
// Arrange
|
||||
String nonExistentFileId = "non-existent-file";
|
||||
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IOException.class, () -> fileStorage.retrieveBytes(nonExistentFileId));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testDeleteFile() throws IOException {
|
||||
// Arrange
|
||||
@@ -140,27 +140,27 @@ class FileStorageTest {
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
|
||||
// Act
|
||||
boolean result = fileStorage.deleteFile(fileId);
|
||||
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
assertFalse(Files.exists(filePath));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testDeleteFile_FileNotFound() {
|
||||
// Arrange
|
||||
String nonExistentFileId = "non-existent-file";
|
||||
|
||||
|
||||
// Act
|
||||
boolean result = fileStorage.deleteFile(nonExistentFileId);
|
||||
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testFileExists() throws IOException {
|
||||
// Arrange
|
||||
@@ -168,23 +168,23 @@ class FileStorageTest {
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
|
||||
// Act
|
||||
boolean result = fileStorage.fileExists(fileId);
|
||||
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testFileExists_FileNotFound() {
|
||||
// Arrange
|
||||
String nonExistentFileId = "non-existent-file";
|
||||
|
||||
|
||||
// Act
|
||||
boolean result = fileStorage.fileExists(nonExistentFileId);
|
||||
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
import stirling.software.common.model.job.JobResponse;
|
||||
|
||||
@@ -42,35 +41,31 @@ import stirling.software.common.model.job.JobResponse;
|
||||
class JobExecutorServiceTest {
|
||||
|
||||
private JobExecutorService jobExecutorService;
|
||||
|
||||
|
||||
@Mock
|
||||
private TaskManager taskManager;
|
||||
|
||||
@Mock
|
||||
private WebSocketProgressController webSocketSender;
|
||||
|
||||
|
||||
@Mock
|
||||
private FileStorage fileStorage;
|
||||
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
|
||||
@Mock
|
||||
private ResourceMonitor resourceMonitor;
|
||||
|
||||
|
||||
@Mock
|
||||
private JobQueue jobQueue;
|
||||
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<String> jobIdCaptor;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Initialize the service manually with all its dependencies
|
||||
jobExecutorService = new JobExecutorService(
|
||||
taskManager,
|
||||
webSocketSender,
|
||||
fileStorage,
|
||||
taskManager,
|
||||
fileStorage,
|
||||
request,
|
||||
resourceMonitor,
|
||||
jobQueue,
|
||||
@@ -78,23 +73,23 @@ class JobExecutorServiceTest {
|
||||
"30m" // sessionTimeout
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldRunSyncJobSuccessfully() throws Exception {
|
||||
// Given
|
||||
Supplier<Object> work = () -> "test-result";
|
||||
|
||||
|
||||
// When
|
||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals("test-result", response.getBody());
|
||||
|
||||
|
||||
// Verify request attribute was set with jobId
|
||||
verify(request).setAttribute(eq("jobId"), anyString());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldRunAsyncJobSuccessfully() throws Exception {
|
||||
// Given
|
||||
@@ -112,74 +107,73 @@ class JobExecutorServiceTest {
|
||||
|
||||
// Verify task manager was called
|
||||
verify(taskManager).createTask(jobIdCaptor.capture());
|
||||
verify(webSocketSender, Mockito.atLeastOnce()).sendProgress(eq(jobIdCaptor.getValue()), any(JobProgress.class));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void shouldHandleSyncJobError() {
|
||||
// Given
|
||||
Supplier<Object> work = () -> {
|
||||
throw new RuntimeException("Test error");
|
||||
};
|
||||
|
||||
|
||||
// When
|
||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> errorMap = (Map<String, String>) response.getBody();
|
||||
assertEquals("Job failed: Test error", errorMap.get("error"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldQueueJobWhenResourcesLimited() {
|
||||
// Given
|
||||
Supplier<Object> work = () -> "test-result";
|
||||
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
||||
|
||||
|
||||
// Configure resourceMonitor to indicate job should be queued
|
||||
when(resourceMonitor.shouldQueueJob(80)).thenReturn(true);
|
||||
|
||||
|
||||
// Configure jobQueue to return our future
|
||||
when(jobQueue.queueJob(anyString(), eq(80), any(), anyLong())).thenReturn(future);
|
||||
|
||||
|
||||
// When
|
||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(
|
||||
true, work, 5000, true, 80);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertTrue(response.getBody() instanceof JobResponse);
|
||||
|
||||
|
||||
// Verify job was queued
|
||||
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
|
||||
verify(taskManager).createTask(anyString());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldUseCustomTimeoutWhenProvided() throws Exception {
|
||||
// Given
|
||||
Supplier<Object> work = () -> "test-result";
|
||||
long customTimeout = 60000L;
|
||||
|
||||
|
||||
// Use reflection to access the private executeWithTimeout method
|
||||
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
||||
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
||||
executeMethod.setAccessible(true);
|
||||
|
||||
|
||||
// Create a spy on the JobExecutorService to verify method calls
|
||||
JobExecutorService spy = Mockito.spy(jobExecutorService);
|
||||
|
||||
|
||||
// When
|
||||
spy.runJobGeneric(false, work, customTimeout);
|
||||
|
||||
|
||||
// Then
|
||||
verify(spy).runJobGeneric(eq(false), any(Supplier.class), eq(customTimeout));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldHandleTimeout() throws Exception {
|
||||
// Given
|
||||
@@ -192,12 +186,12 @@ class JobExecutorServiceTest {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Use reflection to access the private executeWithTimeout method
|
||||
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
||||
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
||||
executeMethod.setAccessible(true);
|
||||
|
||||
|
||||
// When/Then
|
||||
try {
|
||||
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
|
||||
@@ -205,4 +199,4 @@ class JobExecutorServiceTest {
|
||||
assertTrue(e.getCause() instanceof TimeoutException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
||||
|
||||
@@ -29,8 +28,6 @@ class JobQueueTest {
|
||||
@Mock
|
||||
private ResourceMonitor resourceMonitor;
|
||||
|
||||
@Mock
|
||||
private WebSocketProgressController webSocketSender;
|
||||
|
||||
private final AtomicReference<ResourceStatus> statusRef = new AtomicReference<>(ResourceStatus.OK);
|
||||
|
||||
@@ -40,7 +37,8 @@ class JobQueueTest {
|
||||
lenient().when(resourceMonitor.calculateDynamicQueueCapacity(anyInt(), anyInt())).thenReturn(10);
|
||||
lenient().when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
||||
|
||||
jobQueue = new JobQueue(resourceMonitor, webSocketSender);
|
||||
// Initialize JobQueue with mocked ResourceMonitor
|
||||
jobQueue = new JobQueue(resourceMonitor);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,9 +50,6 @@ class JobQueueTest {
|
||||
|
||||
jobQueue.queueJob(jobId, resourceWeight, work, timeoutMs);
|
||||
|
||||
verify(webSocketSender).sendProgress(
|
||||
org.mockito.ArgumentMatchers.eq(jobId),
|
||||
org.mockito.ArgumentMatchers.any(JobProgress.class));
|
||||
|
||||
assertTrue(jobQueue.isJobQueued(jobId));
|
||||
assertEquals(1, jobQueue.getTotalQueuedJobs());
|
||||
|
||||
@@ -32,19 +32,19 @@ class ResourceMonitorTest {
|
||||
|
||||
@InjectMocks
|
||||
private ResourceMonitor resourceMonitor;
|
||||
|
||||
|
||||
@Mock
|
||||
private OperatingSystemMXBean osMXBean;
|
||||
|
||||
|
||||
@Mock
|
||||
private MemoryMXBean memoryMXBean;
|
||||
|
||||
|
||||
@Spy
|
||||
private AtomicReference<ResourceStatus> currentStatus = new AtomicReference<>(ResourceStatus.OK);
|
||||
|
||||
|
||||
@Spy
|
||||
private AtomicReference<ResourceMetrics> latestMetrics = new AtomicReference<>(new ResourceMetrics());
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Set thresholds for testing
|
||||
@@ -57,45 +57,45 @@ class ResourceMonitorTest {
|
||||
ReflectionTestUtils.setField(resourceMonitor, "currentStatus", currentStatus);
|
||||
ReflectionTestUtils.setField(resourceMonitor, "latestMetrics", latestMetrics);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldCalculateDynamicQueueCapacity() {
|
||||
// Given
|
||||
int baseCapacity = 10;
|
||||
int minCapacity = 2;
|
||||
|
||||
|
||||
// Mock current status as OK
|
||||
currentStatus.set(ResourceStatus.OK);
|
||||
|
||||
|
||||
// When
|
||||
int capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(baseCapacity, capacity, "With OK status, capacity should equal base capacity");
|
||||
|
||||
|
||||
// Given
|
||||
currentStatus.set(ResourceStatus.WARNING);
|
||||
|
||||
|
||||
// When
|
||||
capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(6, capacity, "With WARNING status, capacity should be reduced to 60%");
|
||||
|
||||
|
||||
// Given
|
||||
currentStatus.set(ResourceStatus.CRITICAL);
|
||||
|
||||
|
||||
// When
|
||||
capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(3, capacity, "With CRITICAL status, capacity should be reduced to 30%");
|
||||
|
||||
|
||||
// Test minimum capacity enforcement
|
||||
assertEquals(minCapacity, resourceMonitor.calculateDynamicQueueCapacity(1, minCapacity),
|
||||
"Should never go below minimum capacity");
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"10, OK, false", // Light job, OK status
|
||||
@@ -111,27 +111,27 @@ class ResourceMonitorTest {
|
||||
void shouldQueueJobBasedOnWeightAndStatus(int weight, ResourceStatus status, boolean shouldQueue) {
|
||||
// Given
|
||||
currentStatus.set(status);
|
||||
|
||||
|
||||
// When
|
||||
boolean result = resourceMonitor.shouldQueueJob(weight);
|
||||
|
||||
|
||||
// Then
|
||||
assertEquals(shouldQueue, result,
|
||||
String.format("For weight %d and status %s, shouldQueue should be %s",
|
||||
String.format("For weight %d and status %s, shouldQueue should be %s",
|
||||
weight, status, shouldQueue));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void resourceMetricsShouldDetectStaleState() {
|
||||
// Given
|
||||
Instant now = Instant.now();
|
||||
Instant pastInstant = now.minusMillis(6000);
|
||||
|
||||
|
||||
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
||||
|
||||
|
||||
// When/Then
|
||||
assertTrue(staleMetrics.isStale(5000), "Metrics from 6 seconds ago should be stale with 5s threshold");
|
||||
assertFalse(freshMetrics.isStale(5000), "Fresh metrics should not be stale");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
|
||||
class TaskManagerTest {
|
||||
|
||||
@Mock
|
||||
private FileStorage fileStorage;
|
||||
|
||||
|
||||
@InjectMocks
|
||||
private TaskManager taskManager;
|
||||
|
||||
|
||||
private AutoCloseable closeable;
|
||||
|
||||
@BeforeEach
|
||||
@@ -34,7 +35,7 @@ class TaskManagerTest {
|
||||
closeable = MockitoAnnotations.openMocks(this);
|
||||
ReflectionTestUtils.setField(taskManager, "jobResultExpiryMinutes", 30);
|
||||
}
|
||||
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
closeable.close();
|
||||
@@ -45,7 +46,7 @@ class TaskManagerTest {
|
||||
// Act
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
|
||||
// Assert
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
@@ -53,17 +54,17 @@ class TaskManagerTest {
|
||||
assertFalse(result.isComplete());
|
||||
assertNotNull(result.getCreatedAt());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSetResult() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
Object resultObject = "Test result";
|
||||
|
||||
|
||||
// Act
|
||||
taskManager.setResult(jobId, resultObject);
|
||||
|
||||
|
||||
// Assert
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
@@ -71,39 +72,51 @@ class TaskManagerTest {
|
||||
assertEquals(resultObject, result.getResult());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSetFileResult() {
|
||||
void testSetFileResult() throws Exception {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
String contentType = "application/pdf";
|
||||
|
||||
long fileSize = 1024L;
|
||||
|
||||
// Mock the fileStorage.getFileSize() call
|
||||
when(fileStorage.getFileSize(fileId)).thenReturn(fileSize);
|
||||
|
||||
// Act
|
||||
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
||||
|
||||
|
||||
// Assert
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isComplete());
|
||||
assertEquals(fileId, result.getFileId());
|
||||
assertEquals(originalFileName, result.getOriginalFileName());
|
||||
assertEquals(contentType, result.getContentType());
|
||||
assertTrue(result.hasFiles());
|
||||
assertFalse(result.hasMultipleFiles());
|
||||
|
||||
var resultFiles = result.getAllResultFiles();
|
||||
assertEquals(1, resultFiles.size());
|
||||
|
||||
ResultFile resultFile = resultFiles.get(0);
|
||||
assertEquals(fileId, resultFile.getFileId());
|
||||
assertEquals(originalFileName, resultFile.getFileName());
|
||||
assertEquals(contentType, resultFile.getContentType());
|
||||
assertEquals(fileSize, resultFile.getFileSize());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSetError() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
String errorMessage = "Test error";
|
||||
|
||||
|
||||
// Act
|
||||
taskManager.setError(jobId, errorMessage);
|
||||
|
||||
|
||||
// Assert
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
@@ -111,7 +124,7 @@ class TaskManagerTest {
|
||||
assertEquals(errorMessage, result.getError());
|
||||
assertNotNull(result.getCompletedAt());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSetComplete_WithExistingResult() {
|
||||
// Arrange
|
||||
@@ -119,74 +132,77 @@ class TaskManagerTest {
|
||||
taskManager.createTask(jobId);
|
||||
Object resultObject = "Test result";
|
||||
taskManager.setResult(jobId, resultObject);
|
||||
|
||||
|
||||
// Act
|
||||
taskManager.setComplete(jobId);
|
||||
|
||||
|
||||
// Assert
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isComplete());
|
||||
assertEquals(resultObject, result.getResult());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testSetComplete_WithoutExistingResult() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
|
||||
// Act
|
||||
taskManager.setComplete(jobId);
|
||||
|
||||
|
||||
// Assert
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isComplete());
|
||||
assertEquals("Task completed successfully", result.getResult());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testIsComplete() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
|
||||
// Assert - not complete initially
|
||||
assertFalse(taskManager.isComplete(jobId));
|
||||
|
||||
|
||||
// Act - mark as complete
|
||||
taskManager.setComplete(jobId);
|
||||
|
||||
|
||||
// Assert - now complete
|
||||
assertTrue(taskManager.isComplete(jobId));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testGetJobStats() {
|
||||
void testGetJobStats() throws Exception {
|
||||
// Arrange
|
||||
// Mock fileStorage.getFileSize for file operations
|
||||
when(fileStorage.getFileSize("file-id")).thenReturn(1024L);
|
||||
|
||||
// 1. Create active job
|
||||
String activeJobId = "active-job";
|
||||
taskManager.createTask(activeJobId);
|
||||
|
||||
|
||||
// 2. Create completed successful job with file
|
||||
String successFileJobId = "success-file-job";
|
||||
taskManager.createTask(successFileJobId);
|
||||
taskManager.setFileResult(successFileJobId, "file-id", "test.pdf", "application/pdf");
|
||||
|
||||
|
||||
// 3. Create completed successful job without file
|
||||
String successJobId = "success-job";
|
||||
taskManager.createTask(successJobId);
|
||||
taskManager.setResult(successJobId, "Result");
|
||||
|
||||
|
||||
// 4. Create failed job
|
||||
String failedJobId = "failed-job";
|
||||
taskManager.createTask(failedJobId);
|
||||
taskManager.setError(failedJobId, "Error message");
|
||||
|
||||
|
||||
// Act
|
||||
JobStats stats = taskManager.getJobStats();
|
||||
|
||||
|
||||
// Assert
|
||||
assertEquals(4, stats.getTotalJobs());
|
||||
assertEquals(1, stats.getActiveJobs());
|
||||
@@ -198,7 +214,7 @@ class TaskManagerTest {
|
||||
assertNotNull(stats.getOldestActiveJobTime());
|
||||
assertTrue(stats.getAverageProcessingTimeMs() >= 0);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testCleanupOldJobs() throws Exception {
|
||||
// Arrange
|
||||
@@ -206,50 +222,88 @@ class TaskManagerTest {
|
||||
String recentJobId = "recent-job";
|
||||
taskManager.createTask(recentJobId);
|
||||
taskManager.setResult(recentJobId, "Result");
|
||||
|
||||
|
||||
// 2. Create an old completed job with file result
|
||||
String oldJobId = "old-job";
|
||||
taskManager.createTask(oldJobId);
|
||||
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
||||
|
||||
|
||||
// Manually set the completion time to be older than the expiry
|
||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
|
||||
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
|
||||
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
|
||||
|
||||
// Create a ResultFile and set it using the new approach
|
||||
ResultFile resultFile = ResultFile.builder()
|
||||
.fileId("file-id")
|
||||
.fileName("test.pdf")
|
||||
.contentType("application/pdf")
|
||||
.fileSize(1024L)
|
||||
.build();
|
||||
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
||||
|
||||
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
||||
|
||||
|
||||
// Obtain access to the private jobResults map
|
||||
Map<String, JobResult> jobResultsMap = (Map<String, JobResult>) ReflectionTestUtils.getField(taskManager, "jobResults");
|
||||
|
||||
|
||||
// 3. Create an active job
|
||||
String activeJobId = "active-job";
|
||||
taskManager.createTask(activeJobId);
|
||||
|
||||
|
||||
// Verify all jobs are in the map
|
||||
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||
assertTrue(jobResultsMap.containsKey(oldJobId));
|
||||
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||
|
||||
|
||||
// Act
|
||||
taskManager.cleanupOldJobs();
|
||||
|
||||
|
||||
// Assert - the old job should be removed
|
||||
assertFalse(jobResultsMap.containsKey(oldJobId));
|
||||
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||
verify(fileStorage).deleteFile("file-id");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testShutdown() throws Exception {
|
||||
// This mainly tests that the shutdown method doesn't throw exceptions
|
||||
taskManager.shutdown();
|
||||
|
||||
|
||||
// Verify the executor service is shutdown
|
||||
// This is difficult to test directly, but we can verify it doesn't throw exceptions
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddNote() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
String note = "Test note";
|
||||
|
||||
// Act
|
||||
boolean result = taskManager.addNote(jobId, note);
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
JobResult jobResult = taskManager.getJobResult(jobId);
|
||||
assertNotNull(jobResult);
|
||||
assertNotNull(jobResult.getNotes());
|
||||
assertEquals(1, jobResult.getNotes().size());
|
||||
assertEquals(note, jobResult.getNotes().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddNote_NonExistentJob() {
|
||||
// Arrange
|
||||
String jobId = "non-existent-job";
|
||||
String note = "Test note";
|
||||
|
||||
// Act
|
||||
boolean result = taskManager.addNote(jobId, note);
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Tests for the TempFileCleanupService, focusing on its pattern-matching and cleanup logic.
|
||||
*/
|
||||
public class TempFileCleanupServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Mock
|
||||
private TempFileRegistry registry;
|
||||
|
||||
@Mock
|
||||
private TempFileManager tempFileManager;
|
||||
|
||||
@Mock
|
||||
private ApplicationProperties applicationProperties;
|
||||
|
||||
@Mock
|
||||
private ApplicationProperties.System system;
|
||||
|
||||
@Mock
|
||||
private ApplicationProperties.TempFileManagement tempFileManagement;
|
||||
|
||||
@InjectMocks
|
||||
private TempFileCleanupService cleanupService;
|
||||
|
||||
private Path systemTempDir;
|
||||
private Path customTempDir;
|
||||
private Path libreOfficeTempDir;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws IOException {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
// Create test directories
|
||||
systemTempDir = tempDir.resolve("systemTemp");
|
||||
customTempDir = tempDir.resolve("customTemp");
|
||||
libreOfficeTempDir = tempDir.resolve("libreOfficeTemp");
|
||||
|
||||
Files.createDirectories(systemTempDir);
|
||||
Files.createDirectories(customTempDir);
|
||||
Files.createDirectories(libreOfficeTempDir);
|
||||
|
||||
// Configure ApplicationProperties mocks
|
||||
when(applicationProperties.getSystem()).thenReturn(system);
|
||||
when(system.getTempFileManagement()).thenReturn(tempFileManagement);
|
||||
when(tempFileManagement.getBaseTmpDir()).thenReturn(customTempDir.toString());
|
||||
when(tempFileManagement.getLibreofficeDir()).thenReturn(libreOfficeTempDir.toString());
|
||||
when(tempFileManagement.getSystemTempDir()).thenReturn(systemTempDir.toString());
|
||||
when(tempFileManagement.isStartupCleanup()).thenReturn(false);
|
||||
when(tempFileManagement.isCleanupSystemTemp()).thenReturn(false);
|
||||
when(tempFileManagement.getCleanupIntervalMinutes()).thenReturn(30L);
|
||||
|
||||
// Set machineType using reflection (still needed for this field)
|
||||
ReflectionTestUtils.setField(cleanupService, "machineType", "Standard");
|
||||
|
||||
when(tempFileManager.getMaxAgeMillis()).thenReturn(3600000L); // 1 hour
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScheduledCleanup_RegisteredFiles() {
|
||||
// Arrange
|
||||
when(tempFileManager.cleanupOldTempFiles(anyLong())).thenReturn(5); // 5 files deleted
|
||||
Set<Path> registeredDirs = new HashSet<>();
|
||||
registeredDirs.add(tempDir.resolve("registeredDir"));
|
||||
when(registry.getTempDirectories()).thenReturn(registeredDirs);
|
||||
|
||||
// Act
|
||||
cleanupService.scheduledCleanup();
|
||||
|
||||
// Assert
|
||||
verify(tempFileManager).cleanupOldTempFiles(anyLong());
|
||||
verify(registry, times(1)).getTempDirectories();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupTempFilePatterns() throws IOException {
|
||||
// Arrange - Create various temp files
|
||||
Path ourTempFile1 = Files.createFile(systemTempDir.resolve("output_123.pdf"));
|
||||
Path ourTempFile2 = Files.createFile(systemTempDir.resolve("compressedPDF456.pdf"));
|
||||
Path ourTempFile3 = Files.createFile(customTempDir.resolve("stirling-pdf-789.tmp"));
|
||||
Path ourTempFile4 = Files.createFile(customTempDir.resolve("pdf-save-123-456.tmp"));
|
||||
Path ourTempFile5 = Files.createFile(libreOfficeTempDir.resolve("input_file.pdf"));
|
||||
|
||||
// Old temporary files
|
||||
Path oldTempFile = Files.createFile(systemTempDir.resolve("output_old.pdf"));
|
||||
|
||||
// System temp files that should be cleaned in container mode
|
||||
Path sysTempFile1 = Files.createFile(systemTempDir.resolve("lu123abc.tmp"));
|
||||
Path sysTempFile2 = Files.createFile(customTempDir.resolve("ocr_process123"));
|
||||
Path sysTempFile3 = Files.createFile(customTempDir.resolve("tmp_upload.tmp"));
|
||||
|
||||
// Files that should be preserved
|
||||
Path jettyFile1 = Files.createFile(systemTempDir.resolve("jetty-123.tmp"));
|
||||
Path jettyFile2 = Files.createFile(systemTempDir.resolve("something-with-jetty-inside.tmp"));
|
||||
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||
|
||||
// Create a nested directory with temp files
|
||||
Path nestedDir = Files.createDirectories(systemTempDir.resolve("nested"));
|
||||
Path nestedTempFile = Files.createFile(nestedDir.resolve("output_nested.pdf"));
|
||||
|
||||
// Empty file (special case)
|
||||
Path emptyFile = Files.createFile(systemTempDir.resolve("empty.tmp"));
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Mock Files.list for each directory we'll process
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(
|
||||
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
||||
jettyFile1, jettyFile2, regularFile, emptyFile, nestedDir));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(customTempDir)))
|
||||
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(libreOfficeTempDir)))
|
||||
.thenReturn(Stream.of(ourTempFile5));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(nestedDir)))
|
||||
.thenReturn(Stream.of(nestedTempFile));
|
||||
|
||||
// Configure Files.isDirectory for each path
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||
|
||||
// Configure Files.exists to return true for all paths
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
// For files with "old" in the name, return a timestamp older than maxAgeMillis
|
||||
if (fileName.contains("old")) {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||
}
|
||||
// For empty.tmp file, return a timestamp older than 5 minutes (for empty file test)
|
||||
else if (fileName.equals("empty.tmp")) {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||
}
|
||||
// For all other files, return a recent timestamp
|
||||
else {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000); // 1 minute ago
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Files.size to return different sizes based on file names
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
// Return 0 bytes for the empty file
|
||||
if (fileName.equals("empty.tmp")) {
|
||||
return 0L;
|
||||
}
|
||||
// Return normal size for all other files
|
||||
else {
|
||||
return 1024L; // 1 KB
|
||||
}
|
||||
});
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act - set containerMode to false for this test
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(customTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 0, 3600000);
|
||||
|
||||
// Assert - Only old temp files and empty files should be deleted
|
||||
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
|
||||
assertTrue(deletedFiles.contains(emptyFile), "Empty file should be deleted");
|
||||
|
||||
// Regular temp files should not be deleted because they're too new
|
||||
assertFalse(deletedFiles.contains(ourTempFile1), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile2), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile3), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile4), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile5), "Recent temp file should be preserved");
|
||||
|
||||
// System temp files should not be deleted in non-container mode
|
||||
assertFalse(deletedFiles.contains(sysTempFile1), "System temp file should be preserved in non-container mode");
|
||||
assertFalse(deletedFiles.contains(sysTempFile2), "System temp file should be preserved in non-container mode");
|
||||
assertFalse(deletedFiles.contains(sysTempFile3), "System temp file should be preserved in non-container mode");
|
||||
|
||||
// Jetty files and regular files should never be deleted
|
||||
assertFalse(deletedFiles.contains(jettyFile1), "Jetty file should be preserved");
|
||||
assertFalse(deletedFiles.contains(jettyFile2), "File with jetty in name should be preserved");
|
||||
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainerModeCleanup() throws IOException {
|
||||
// Arrange - Create various temp files
|
||||
Path ourTempFile = Files.createFile(systemTempDir.resolve("output_123.pdf"));
|
||||
Path sysTempFile = Files.createFile(systemTempDir.resolve("lu123abc.tmp"));
|
||||
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Mock Files.list for systemTempDir
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
||||
|
||||
// Configure Files.isDirectory
|
||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||
|
||||
// Configure Files.exists
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenReturn(FileTime.fromMillis(System.currentTimeMillis() - 60000)); // 1 minute ago
|
||||
|
||||
// Configure Files.size to return normal size
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenReturn(1024L); // 1 KB
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
||||
|
||||
// Assert - In container mode, both our temp files and system temp files should be deleted
|
||||
// regardless of age (when maxAgeMillis is 0)
|
||||
assertTrue(deletedFiles.contains(ourTempFile), "Our temp file should be deleted in container mode");
|
||||
assertTrue(deletedFiles.contains(sysTempFile), "System temp file should be deleted in container mode");
|
||||
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyFileHandling() throws IOException {
|
||||
// Arrange - Create an empty file
|
||||
Path emptyFile = Files.createFile(systemTempDir.resolve("empty.tmp"));
|
||||
Path recentEmptyFile = Files.createFile(systemTempDir.resolve("recent_empty.tmp"));
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Mock Files.list for systemTempDir
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
||||
|
||||
// Configure Files.isDirectory
|
||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||
|
||||
// Configure Files.exists
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
if (fileName.equals("empty.tmp")) {
|
||||
// More than 5 minutes old
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||
} else {
|
||||
// Less than 5 minutes old
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 2 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Files.size to return 0 for empty files
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenReturn(0L);
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
|
||||
// Assert
|
||||
assertTrue(deletedFiles.contains(emptyFile),
|
||||
"Empty file older than 5 minutes should be deleted");
|
||||
assertFalse(deletedFiles.contains(recentEmptyFile),
|
||||
"Empty file newer than 5 minutes should not be deleted");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecursiveDirectoryCleaning() throws IOException {
|
||||
// Arrange - Create a nested directory structure with temp files
|
||||
Path dir1 = Files.createDirectories(systemTempDir.resolve("dir1"));
|
||||
Path dir2 = Files.createDirectories(dir1.resolve("dir2"));
|
||||
Path dir3 = Files.createDirectories(dir2.resolve("dir3"));
|
||||
|
||||
Path tempFile1 = Files.createFile(dir1.resolve("output_1.pdf"));
|
||||
Path tempFile2 = Files.createFile(dir2.resolve("output_2.pdf"));
|
||||
Path tempFile3 = Files.createFile(dir3.resolve("output_old_3.pdf"));
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Mock Files.list for each directory
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(dir1));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir1)))
|
||||
.thenReturn(Stream.of(tempFile1, dir2));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir2)))
|
||||
.thenReturn(Stream.of(tempFile2, dir3));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir3)))
|
||||
.thenReturn(Stream.of(tempFile3));
|
||||
|
||||
// Configure Files.isDirectory for each path
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(dir2))).thenReturn(true);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(dir3))).thenReturn(true);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(tempFile1))).thenReturn(false);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(tempFile2))).thenReturn(false);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(tempFile3))).thenReturn(false);
|
||||
|
||||
// Configure Files.exists to return true for all paths
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
if (fileName.contains("old")) {
|
||||
// Old file
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||
} else {
|
||||
// Recent file
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Files.size to return normal size
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenReturn(1024L);
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
|
||||
// Debug - print what was deleted
|
||||
System.out.println("Deleted files: " + deletedFiles);
|
||||
System.out.println("Looking for: " + tempFile3);
|
||||
|
||||
// Assert
|
||||
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(tempFile2), "Recent temp file should be preserved");
|
||||
assertTrue(deletedFiles.contains(tempFile3), "Old temp file in nested directory should be deleted");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to invoke the private cleanupDirectoryStreaming method using reflection
|
||||
*/
|
||||
private void invokeCleanupDirectoryStreaming(Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
||||
throws IOException {
|
||||
try {
|
||||
// Create a consumer that tracks deleted files
|
||||
AtomicInteger deleteCount = new AtomicInteger(0);
|
||||
Consumer<Path> deleteCallback = path -> deleteCount.incrementAndGet();
|
||||
|
||||
// Get the method with updated signature
|
||||
var method = TempFileCleanupService.class.getDeclaredMethod(
|
||||
"cleanupDirectoryStreaming",
|
||||
Path.class, boolean.class, int.class, long.class, boolean.class, Consumer.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
// Invoke the method with appropriate parameters
|
||||
method.invoke(cleanupService, directory, containerMode, depth, maxAgeMillis, false, deleteCallback);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error invoking cleanupDirectoryStreaming", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Matcher for exact path equality
|
||||
private static Path eq(Path path) {
|
||||
return argThat(arg -> arg != null && arg.equals(path));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
@@ -10,18 +19,6 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
class CheckProgramInstallTest {
|
||||
|
||||
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
||||
|
||||
@@ -0,0 +1,893 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@DisplayName("EML to PDF Conversion tests")
|
||||
class EmlToPdfTest {
|
||||
|
||||
// Focus on testing EML to HTML conversion functionality since the PDF conversion relies on WeasyPrint
|
||||
// But HTML to PDF conversion is also briefly tested at PdfConversionTests class.
|
||||
private void testEmailConversion(String emlContent, String[] expectedContent, boolean includeAttachments) throws IOException {
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = includeAttachments ? createRequestWithAttachments() : createBasicRequest();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
for (String expected : expectedContent) {
|
||||
assertTrue(htmlResult.contains(expected), "HTML should contain: " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Core EML Parsing")
|
||||
class CoreParsingTests {
|
||||
@Test
|
||||
@DisplayName("Should parse simple text email correctly")
|
||||
void parseSimpleTextEmail() throws IOException {
|
||||
String emlContent = createSimpleTextEmail(
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
"Simple Test Subject",
|
||||
"This is a simple plain text email body.");
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"Simple Test Subject",
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
"This is a simple plain text email body",
|
||||
"<!DOCTYPE html>"
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should parse email with missing Subject and To headers")
|
||||
void parseEmailWithMissingHeaders() throws IOException {
|
||||
String emlContent = createEmailWithCustomHeaders();
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains("sender@example.com"));
|
||||
assertTrue(htmlResult.contains("This is an email body"));
|
||||
assertTrue(htmlResult.contains("<title></title>") ||
|
||||
htmlResult.contains("<title>No Subject</title>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should parse HTML email with styling")
|
||||
void parseHtmlEmailWithStyling() throws IOException {
|
||||
String htmlBody = "<html><head><style>.header{color:blue;font-weight:bold;}" +
|
||||
".content{margin:10px;}.footer{font-size:12px;}</style></head>" +
|
||||
"<body><div class=\"header\">Important Notice</div>" +
|
||||
"<div class=\"content\">This is <strong>HTML content</strong> with styling.</div>" +
|
||||
"<div class=\"footer\">Best regards</div></body></html>";
|
||||
|
||||
String emlContent = createHtmlEmail(
|
||||
"html@example.com", "user@example.com", "HTML Email Test", htmlBody);
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"HTML Email Test",
|
||||
"Important Notice",
|
||||
"HTML content",
|
||||
"font-weight: bold"
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should parse multipart email with attachments")
|
||||
void parseMultipartEmailWithAttachments() throws IOException {
|
||||
String boundary = "----=_Part_" + getTimestamp();
|
||||
String emlContent = createMultipartEmailWithAttachment(
|
||||
"multipart@example.com",
|
||||
"user@example.com",
|
||||
"Multipart Email Test",
|
||||
"This email has both text content and an attachment.",
|
||||
boundary,
|
||||
"document.txt",
|
||||
"Sample attachment content");
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"Multipart Email Test",
|
||||
"This email has both text content"
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Email Encoding Support")
|
||||
class EncodingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle international characters and UTF-8")
|
||||
void handleInternationalCharacters() throws IOException {
|
||||
String bodyWithIntlChars = "Hello! 你好 Привет مرحبا Hëllö Thañks! Önë Mörë";
|
||||
String emlContent = createSimpleTextEmail(
|
||||
"intl@example.com",
|
||||
"user@example.com",
|
||||
"International Characters Test",
|
||||
bodyWithIntlChars);
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"你好", "Привет", "مرحبا", "Hëllö", "Önë", "Mörë"
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should decode quoted-printable content correctly")
|
||||
void decodeQuotedPrintableContent() throws IOException {
|
||||
String content = createQuotedPrintableEmail(
|
||||
);
|
||||
|
||||
testEmailConversion(content, new String[] {
|
||||
"Quoted-Printable Test",
|
||||
"This is quoted printable content with special chars: éàè."
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should decode Base64 content")
|
||||
void decodeBase64Content() throws IOException {
|
||||
String originalText = "This is Base64 encoded content: éàü ñ";
|
||||
String content = createBase64Email(
|
||||
originalText);
|
||||
|
||||
testEmailConversion(content, new String[] {
|
||||
"Base64 Test", "Base64 encoded content"
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should correctly handle inline images with CID references")
|
||||
void handleInlineImages() throws IOException {
|
||||
String boundary = "----=_Part_CID_1234567890";
|
||||
String cid = "image123@example.com";
|
||||
String htmlBody = "<html><body><p>Here is an image:</p><img src=\"cid:" + cid + "\"></body></html>";
|
||||
String imageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
||||
|
||||
String emlContent = createEmailWithInlineImage(htmlBody, boundary, cid, imageBase64);
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createRequestWithAttachments();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains("data:image/png;base64," + imageBase64));
|
||||
assertFalse(htmlResult.contains("cid:" + cid));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("HTML Output Quality")
|
||||
class HtmlOutputTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate valid HTML structure")
|
||||
void generateValidHtmlStructure() throws IOException {
|
||||
String emlContent = createSimpleTextEmail(
|
||||
"structure@test.com",
|
||||
"user@test.com",
|
||||
"HTML Structure Test",
|
||||
"Testing HTML structure output");
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"<!DOCTYPE html>", "<html", "</html>", "HTML Structure Test"
|
||||
}, false);
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, createBasicRequest());
|
||||
assertTrue(htmlResult.length() > 100, "HTML should have substantial content");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should preserve safe CSS and remove problematic styles")
|
||||
void handleCssStylesCorrectly() throws IOException {
|
||||
String styledHtml = "<html><head><style>" +
|
||||
".safe { color: blue; font-size: 14px; }" +
|
||||
".problematic { position: fixed; word-break: break-all; }" +
|
||||
".good { margin: 10px; padding: 5px; }" +
|
||||
"</style></head><body>" +
|
||||
"<div class=\"safe\">Safe styling</div>" +
|
||||
"<div class=\"problematic\">Problematic styling</div>" +
|
||||
"<div class=\"good\">Good styling</div>" +
|
||||
"</body></html>";
|
||||
|
||||
String emlContent = createHtmlEmail("css@test.com", "user@test.com", "CSS Test", styledHtml);
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains("color: blue"));
|
||||
assertTrue(htmlResult.contains("font-size: 14px"));
|
||||
assertTrue(htmlResult.contains("margin: 10px"));
|
||||
|
||||
assertFalse(htmlResult.contains("position: fixed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle complex nested HTML structures")
|
||||
void handleComplexNestedHtml() throws IOException {
|
||||
String complexHtml = "<html><head><title>Complex Email</title></head><body>" +
|
||||
"<div class=\"container\"><header><h1>Email Header</h1></header><main><section>" +
|
||||
"<p>Paragraph with <a href=\"https://example.com\">link</a></p><ul>" +
|
||||
"<li>List item 1</li><li>List item 2 with <em>emphasis</em></li></ul><table>" +
|
||||
"<tr><td>Cell 1</td><td>Cell 2</td></tr><tr><td>Cell 3</td><td>Cell 4</td></tr>" +
|
||||
"</table></section></main></div></body></html>";
|
||||
|
||||
String emlContent = createHtmlEmail(
|
||||
"complex@test.com", "user@test.com", "Complex HTML Test", complexHtml);
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"Email Header", "List item 2", "Cell 3", "example.com"
|
||||
}, false);
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, createBasicRequest());
|
||||
assertTrue(htmlResult.length() > 300, "HTML should have substantial content");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Error Handling & Edge Cases")
|
||||
class ErrorHandlingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject null input")
|
||||
void rejectNullInput() {
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
Exception exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> EmlToPdf.convertEmlToHtml(null, request));
|
||||
assertTrue(exception.getMessage().contains("EML file is empty or null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject empty input")
|
||||
void rejectEmptyInput() {
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
Exception exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> EmlToPdf.convertEmlToHtml(new byte[0], request));
|
||||
assertTrue(exception.getMessage().contains("EML file is empty or null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle malformed EML gracefully")
|
||||
void handleMalformedEmlGracefully() {
|
||||
String malformedEml = """
|
||||
From: sender@test.com
|
||||
Subject: Malformed EML
|
||||
This line breaks header format
|
||||
Content-Type: text/plain
|
||||
|
||||
Body content""";
|
||||
|
||||
byte[] emlBytes = malformedEml.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
try {
|
||||
String result = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
assertNotNull(result, "Result should not be null");
|
||||
assertFalse(result.isEmpty(), "Result should not be empty");
|
||||
} catch (Exception e) {
|
||||
assertNotNull(e.getMessage(), "Exception message should not be null");
|
||||
assertFalse(e.getMessage().isEmpty(), "Exception message should not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject invalid EML format")
|
||||
void rejectInvalidEmlFormat() {
|
||||
byte[] invalidEml = "This is definitely not an EML file".getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
Exception exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> EmlToPdf.convertEmlToHtml(invalidEml, request));
|
||||
assertTrue(exception.getMessage().contains("Invalid EML file format"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Advanced Parsing Tests (Jakarta Mail)")
|
||||
class AdvancedParsingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully parse email using advanced parser")
|
||||
void initializeDependencyMailSession() {
|
||||
assertDoesNotThrow(() -> {
|
||||
String emlContent = createSimpleTextEmail(
|
||||
"Dependency@test.com",
|
||||
"user@test.com",
|
||||
"Dependency Mail Test",
|
||||
"Testing Dependency Mail integration.");
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains("Dependency Mail Test"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should parse complex MIME structures and select HTML part")
|
||||
void parseComplexMimeStructures() throws IOException {
|
||||
String boundary = "----=_Advanced_1234567890";
|
||||
String textBody = "This is the plain text part.";
|
||||
String htmlBody = "<html><body><h1>This is the HTML part</h1></body></html>";
|
||||
String emlContent = createMultipartAlternativeEmail(textBody, htmlBody, boundary);
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createRequestWithAttachments();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains("This is the HTML part"));
|
||||
assertFalse(htmlResult.contains("This is the plain text part"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Additional Correctness Tests")
|
||||
class AdditionalCorrectnessTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle email with only an attachment and no body")
|
||||
void handleAttachmentOnlyEmail() throws IOException {
|
||||
String boundary = "----=_Part_AttachmentOnly_1234567890";
|
||||
String emlContent = createMultipartEmailWithAttachment(
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
"Attachment Only Test",
|
||||
"",
|
||||
boundary,
|
||||
"data.bin",
|
||||
"binary data");
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"Attachment Only Test", "data.bin", "No content available"
|
||||
}, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle mixed inline and regular attachments")
|
||||
void handleMixedAttachments() throws IOException {
|
||||
String boundary = "----=_Part_MixedAttachments_1234567890";
|
||||
String cid = "inline_image@example.com";
|
||||
String htmlBody = "<html><body><img src=\"cid:" + cid + "\"></body></html>";
|
||||
String imageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR42mNkAAIAAAoAAb6A/yoAAAAASUVORK5CYII=";
|
||||
String attachmentText = "This is a text attachment.";
|
||||
|
||||
String emlContent = createEmailWithMixedAttachments(
|
||||
htmlBody, boundary, cid, imageBase64, attachmentText);
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createRequestWithAttachments();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains("data:image/png;base64," + imageBase64));
|
||||
assertTrue(htmlResult.contains("text.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle non-standard but valid character sets like ISO-8859-1")
|
||||
void handleIso88591Charset() throws IOException {
|
||||
String subject = "Subject with special characters: ñ é ü";
|
||||
String body = "Body with special characters: ñ é ü";
|
||||
|
||||
String emlContent = createSimpleTextEmailWithCharset(
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
subject,
|
||||
body,
|
||||
"ISO-8859-1");
|
||||
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.ISO_8859_1);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
String htmlResult = EmlToPdf.convertEmlToHtml(emlBytes, request);
|
||||
|
||||
assertNotNull(htmlResult);
|
||||
assertTrue(htmlResult.contains(subject));
|
||||
assertTrue(htmlResult.contains(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle emails with extremely long lines")
|
||||
void handleLongLines() throws IOException {
|
||||
StringBuilder longLine = new StringBuilder("This is a very long line: ");
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
longLine.append("word").append(i).append(" ");
|
||||
}
|
||||
|
||||
String emlContent = createSimpleTextEmail(
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
"Long Line Test",
|
||||
longLine.toString());
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"Long Line Test", "This is a very long line", "word999"
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle .eml files as attachments")
|
||||
void handleEmlAttachment() throws IOException {
|
||||
String boundary = "----=_Part_EmlAttachment_1234567890";
|
||||
String innerEmlContent = createSimpleTextEmail(
|
||||
"inner@example.com",
|
||||
"inner_recipient@example.com",
|
||||
"Inner Email Subject",
|
||||
"This is the body of the attached email.");
|
||||
|
||||
String emlContent = createEmailWithEmlAttachment(
|
||||
boundary,
|
||||
innerEmlContent);
|
||||
|
||||
testEmailConversion(emlContent, new String[] {
|
||||
"Fwd: Inner Email Subject",
|
||||
"Please see the attached email.",
|
||||
"attached_email.eml"
|
||||
}, true);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"windows-1252", "ISO-8859-2", "KOI8-R", "Shift_JIS"})
|
||||
@DisplayName("Should handle various character encodings")
|
||||
void handleVariousEncodings(String charset) throws IOException {
|
||||
String subject = "Encoding Test";
|
||||
String body = "Testing " + charset + " encoding";
|
||||
|
||||
String emlContent = createSimpleTextEmailWithCharset(
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
subject,
|
||||
body,
|
||||
charset);
|
||||
|
||||
testEmailConversion(emlContent, new String[] {subject, body}, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("PDF Conversion Tests")
|
||||
class PdfConversionTests {
|
||||
|
||||
@Mock private CustomPDFDocumentFactory mockPdfDocumentFactory;
|
||||
|
||||
@Mock private PDDocument mockPdDocument;
|
||||
|
||||
@Mock private TempFileManager mockTempFileManager;
|
||||
|
||||
@Test
|
||||
@DisplayName("Should convert EML to PDF without attachments when not requested")
|
||||
void convertEmlToPdfWithoutAttachments() throws Exception {
|
||||
String emlContent =
|
||||
createSimpleTextEmail("from@test.com", "to@test.com", "Subject", "Body");
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
|
||||
PDDocument testPdf = new PDDocument();
|
||||
testPdf.addPage(new PDPage());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
testPdf.save(baos);
|
||||
testPdf.close();
|
||||
byte[] fakePdfBytes = baos.toByteArray();
|
||||
|
||||
when(mockPdfDocumentFactory.load(any(byte[].class))).thenReturn(mockPdDocument);
|
||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf = mockStatic(FileToPdf.class)) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
anyString(),
|
||||
any(),
|
||||
any(byte[].class),
|
||||
anyString(),
|
||||
anyBoolean(),
|
||||
any(TempFileManager.class)))
|
||||
.thenReturn(fakePdfBytes);
|
||||
|
||||
byte[] resultPdf =
|
||||
EmlToPdf.convertEmlToPdf(
|
||||
"weasyprint",
|
||||
request,
|
||||
emlBytes,
|
||||
"test.eml",
|
||||
false,
|
||||
mockPdfDocumentFactory,
|
||||
mockTempFileManager);
|
||||
|
||||
assertArrayEquals(fakePdfBytes, resultPdf);
|
||||
|
||||
try (PDDocument resultDoc = mockPdfDocumentFactory.load(resultPdf)) {
|
||||
assertNotNull(resultDoc);
|
||||
assertTrue(resultDoc.getNumberOfPages() > 0);
|
||||
}
|
||||
|
||||
fileToPdf.verify(
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
anyString(),
|
||||
any(),
|
||||
any(byte[].class),
|
||||
anyString(),
|
||||
anyBoolean(),
|
||||
any(TempFileManager.class)));
|
||||
verify(mockPdfDocumentFactory).load(resultPdf);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should convert EML to PDF with attachments when requested")
|
||||
void convertEmlToPdfWithAttachments() throws Exception {
|
||||
String boundary = "----=_Part_1234567890";
|
||||
String emlContent = createMultipartEmailWithAttachment(
|
||||
"multipart@example.com",
|
||||
"user@example.com",
|
||||
"Multipart Email Test",
|
||||
"This email has both text content and an attachment.",
|
||||
boundary,
|
||||
"document.txt",
|
||||
"Sample attachment content");
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createRequestWithAttachments();
|
||||
|
||||
PDDocument testPdf = new PDDocument();
|
||||
testPdf.addPage(new PDPage());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
testPdf.save(baos);
|
||||
testPdf.close();
|
||||
byte[] fakePdfBytes = baos.toByteArray();
|
||||
|
||||
when(mockPdfDocumentFactory.load(any(byte[].class))).thenReturn(mockPdDocument);
|
||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf = mockStatic(FileToPdf.class)) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
anyString(),
|
||||
any(),
|
||||
any(byte[].class),
|
||||
anyString(),
|
||||
anyBoolean(),
|
||||
any(TempFileManager.class)))
|
||||
.thenReturn(fakePdfBytes);
|
||||
|
||||
try (MockedStatic<EmlToPdf> ignored =
|
||||
mockStatic(
|
||||
EmlToPdf.class,
|
||||
invocation -> {
|
||||
String methodName = invocation.getMethod().getName();
|
||||
return switch (methodName) {
|
||||
case "shouldAttachFiles" -> true;
|
||||
case "attachFilesToPdf" -> fakePdfBytes;
|
||||
default -> invocation.callRealMethod();
|
||||
};
|
||||
})) {
|
||||
byte[] resultPdf =
|
||||
EmlToPdf.convertEmlToPdf(
|
||||
"weasyprint",
|
||||
request,
|
||||
emlBytes,
|
||||
"test.eml",
|
||||
false,
|
||||
mockPdfDocumentFactory,
|
||||
mockTempFileManager);
|
||||
|
||||
assertArrayEquals(fakePdfBytes, resultPdf);
|
||||
|
||||
try (PDDocument resultDoc = mockPdfDocumentFactory.load(resultPdf)) {
|
||||
assertNotNull(resultDoc);
|
||||
assertTrue(resultDoc.getNumberOfPages() > 0);
|
||||
}
|
||||
|
||||
fileToPdf.verify(
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
anyString(),
|
||||
any(),
|
||||
any(byte[].class),
|
||||
anyString(),
|
||||
anyBoolean(),
|
||||
any(TempFileManager.class)));
|
||||
|
||||
verify(mockPdfDocumentFactory).load(resultPdf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle errors during EML to PDF conversion")
|
||||
void handleErrorsDuringConversion() {
|
||||
String emlContent =
|
||||
createSimpleTextEmail("from@test.com", "to@test.com", "Subject", "Body");
|
||||
byte[] emlBytes = emlContent.getBytes(StandardCharsets.UTF_8);
|
||||
EmlToPdfRequest request = createBasicRequest();
|
||||
String errorMessage = "Conversion failed";
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf = mockStatic(FileToPdf.class)) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
anyString(),
|
||||
any(),
|
||||
any(byte[].class),
|
||||
anyString(),
|
||||
anyBoolean(),
|
||||
any(TempFileManager.class)))
|
||||
.thenThrow(new IOException(errorMessage));
|
||||
|
||||
IOException exception = assertThrows(
|
||||
IOException.class,
|
||||
() -> EmlToPdf.convertEmlToPdf(
|
||||
"weasyprint",
|
||||
request,
|
||||
emlBytes,
|
||||
"test.eml",
|
||||
false,
|
||||
mockPdfDocumentFactory,
|
||||
mockTempFileManager));
|
||||
|
||||
assertTrue(exception.getMessage().contains(errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private String getTimestamp() {
|
||||
java.time.ZonedDateTime fixedDateTime = java.time.ZonedDateTime.of(2023, 1, 1, 12, 0, 0, 0, java.time.ZoneId.of("GMT"));
|
||||
return java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(fixedDateTime);
|
||||
}
|
||||
private String createSimpleTextEmail(String from, String to, String subject, String body) {
|
||||
return createSimpleTextEmailWithCharset(from, to, subject, body, "UTF-8");
|
||||
}
|
||||
|
||||
private String createSimpleTextEmailWithCharset(String from, String to, String subject, String body, String charset) {
|
||||
return String.format(
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/plain; charset=%s\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||
from, to, subject, getTimestamp(), charset, body);
|
||||
}
|
||||
|
||||
private String createEmailWithCustomHeaders() {
|
||||
return String.format(
|
||||
"From: sender@example.com\nDate: %s\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||
getTimestamp(), "This is an email body with some headers missing.");
|
||||
}
|
||||
|
||||
private String createHtmlEmail(String from, String to, String subject, String htmlBody) {
|
||||
return String.format(
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||
from, to, subject, getTimestamp(), htmlBody);
|
||||
}
|
||||
|
||||
private String createMultipartEmailWithAttachment(String from, String to, String subject, String body,
|
||||
String boundary, String filename, String attachmentContent) {
|
||||
String encodedContent = Base64.getEncoder().encodeToString(attachmentContent.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
Date: %s
|
||||
Content-Type: multipart/mixed; boundary="%s"
|
||||
|
||||
--%s
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
%s
|
||||
|
||||
--%s
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Disposition: attachment; filename="%s"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
%s
|
||||
|
||||
--%s--""",
|
||||
from, to, subject, getTimestamp(), boundary, boundary, body, boundary, filename, encodedContent, boundary);
|
||||
}
|
||||
|
||||
private String createEmailWithEmlAttachment(String boundary, String attachmentEmlContent) {
|
||||
String encodedContent = Base64.getEncoder().encodeToString(attachmentEmlContent.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
Date: %s
|
||||
Content-Type: multipart/mixed; boundary="%s"
|
||||
|
||||
--%s
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
%s
|
||||
|
||||
--%s
|
||||
Content-Type: message/rfc822; name="%s"
|
||||
Content-Disposition: attachment; filename="%s"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
%s
|
||||
|
||||
--%s--""",
|
||||
"outer@example.com", "outer_recipient@example.com", "Fwd: Inner Email Subject", getTimestamp(), boundary, boundary, "Please see the attached email.", boundary, "attached_email.eml", "attached_email.eml", encodedContent, boundary);
|
||||
}
|
||||
|
||||
private String createMultipartAlternativeEmail(String textBody, String htmlBody, String boundary) {
|
||||
return String.format(
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
Date: %s
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="%s"
|
||||
|
||||
--%s
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
%s
|
||||
|
||||
--%s
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
%s
|
||||
|
||||
--%s--""",
|
||||
"sender@example.com", "receiver@example.com", "Multipart/Alternative Test", getTimestamp(),
|
||||
boundary, boundary, textBody, boundary, htmlBody, boundary);
|
||||
}
|
||||
|
||||
private String createQuotedPrintableEmail() {
|
||||
return String.format(
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n%s",
|
||||
"sender@example.com", "recipient@example.com", "Quoted-Printable Test", getTimestamp(), "This is quoted=20printable content with special chars: =C3=A9=C3=A0=C3=A8.");
|
||||
}
|
||||
|
||||
private String createBase64Email(String body) {
|
||||
String encodedBody = Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n%s",
|
||||
"sender@example.com", "recipient@example.com", "Base64 Test", getTimestamp(), encodedBody);
|
||||
}
|
||||
|
||||
private String createEmailWithInlineImage(String htmlBody, String boundary, String contentId, String base64Image) {
|
||||
return String.format(
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
Date: %s
|
||||
Content-Type: multipart/related; boundary="%s"
|
||||
|
||||
--%s
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
%s
|
||||
|
||||
--%s
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <%s>
|
||||
Content-Disposition: inline; filename="image.png"
|
||||
|
||||
%s
|
||||
|
||||
--%s--""",
|
||||
"sender@example.com", "receiver@example.com", "Inline Image Test", getTimestamp(),
|
||||
boundary, boundary, htmlBody, boundary, contentId, base64Image, boundary);
|
||||
}
|
||||
|
||||
private String createEmailWithMixedAttachments(String htmlBody, String boundary, String contentId,
|
||||
String base64Image, String attachmentBody) {
|
||||
String encodedAttachment = Base64.getEncoder().encodeToString(attachmentBody.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
Subject: %s
|
||||
Date: %s
|
||||
Content-Type: multipart/mixed; boundary="%s"
|
||||
|
||||
--%s
|
||||
Content-Type: multipart/related; boundary="related-%s"
|
||||
|
||||
--related-%s
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
%s
|
||||
|
||||
--related-%s
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <%s>
|
||||
Content-Disposition: inline; filename="image.png"
|
||||
|
||||
%s
|
||||
|
||||
--related-%s--
|
||||
|
||||
--%s
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Disposition: attachment; filename="%s"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
%s
|
||||
|
||||
--%s--""",
|
||||
"sender@example.com", "receiver@example.com", "Mixed Attachments Test", getTimestamp(),
|
||||
boundary, boundary, boundary, boundary, htmlBody, boundary, contentId, base64Image, boundary,
|
||||
boundary, "text.txt", encodedAttachment, boundary);
|
||||
}
|
||||
// Creates a basic EmlToPdfRequest with default settings
|
||||
private EmlToPdfRequest createBasicRequest() {
|
||||
EmlToPdfRequest request = new EmlToPdfRequest();
|
||||
request.setIncludeAttachments(false);
|
||||
return request;
|
||||
}
|
||||
|
||||
private EmlToPdfRequest createRequestWithAttachments() {
|
||||
EmlToPdfRequest request = new EmlToPdfRequest();
|
||||
request.setIncludeAttachments(true);
|
||||
request.setMaxAttachmentSizeMB(10);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
||||
@@ -3,7 +3,11 @@ package stirling.software.common.util;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -22,14 +26,24 @@ public class FileToPdfTest {
|
||||
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
|
||||
String fileName = "test.html"; // Sample file name indicating an HTML file
|
||||
boolean disableSanitize = false; // Flag to control sanitization
|
||||
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
|
||||
|
||||
// Expect an IOException to be thrown due to empty input
|
||||
// Mock the temp file creation to return real temp files
|
||||
try {
|
||||
when(tempFileManager.createTempFile(anyString()))
|
||||
.thenReturn(File.createTempFile("test", ".pdf"))
|
||||
.thenReturn(File.createTempFile("test", ".html"));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// Expect an IOException to be thrown due to empty input or invalid weasyprint path
|
||||
Throwable thrown =
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
Exception.class,
|
||||
() ->
|
||||
FileToPdf.convertHtmlToPdf(
|
||||
"/path/", request, fileBytes, fileName, disableSanitize));
|
||||
"/path/", request, fileBytes, fileName, disableSanitize, tempFileManager));
|
||||
assertNotNull(thrown);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -12,11 +9,13 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProviderUtilsTest {
|
||||
@@ -29,19 +28,19 @@ class ProviderUtilsTest {
|
||||
when(provider.getClientSecret()).thenReturn("clientSecret");
|
||||
when(provider.getScopes()).thenReturn(List.of("read:user"));
|
||||
|
||||
Assertions.assertTrue(ProviderUtils.validateProvider(provider));
|
||||
assertTrue(ProviderUtils.validateProvider(provider));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("providerParams")
|
||||
void testUnsuccessfulValidation(Provider provider) {
|
||||
Assertions.assertFalse(ProviderUtils.validateProvider(provider));
|
||||
assertFalse(ProviderUtils.validateProvider(provider));
|
||||
}
|
||||
|
||||
public static Stream<Arguments> providerParams() {
|
||||
Provider generic = null;
|
||||
var google =
|
||||
new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
|
||||
new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
|
||||
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
|
||||
|
||||
return Stream.of(Arguments.of(generic), Arguments.of(google), Arguments.of(github));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user