mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
159
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4899041067 | ||
|
|
06efab5cb2 | ||
|
|
085b8795d5 | ||
|
|
fda1d6bc73 | ||
|
|
60c7ba40a6 | ||
|
|
13a6fb9cfe | ||
|
|
df870e6902 | ||
|
|
2c325a911b | ||
|
|
4a2f6e7a5e | ||
|
|
599beb7912 | ||
|
|
0a02e3e231 | ||
|
|
f59b2e4f86 | ||
|
|
b18c652727 | ||
|
|
5b90ad4a92 | ||
|
|
1120f54cda | ||
|
|
31b03475e7 | ||
|
|
612c7e06e7 | ||
|
|
c229fff250 | ||
|
|
6364f0d001 | ||
|
|
b9fdd8823e | ||
|
|
d2ffef215a | ||
|
|
655471ef29 | ||
|
|
3476c36a2a | ||
|
|
24cb3252a4 | ||
|
|
ec1ac4cb2d | ||
|
|
fd95876d8f | ||
|
|
b661eb8ff0 | ||
|
|
575854952a | ||
|
|
dabc52ef73 | ||
|
|
7bd31a954e | ||
|
|
f337c8485e | ||
|
|
bdb721a482 | ||
|
|
927bf3d004 | ||
|
|
8f413819c5 | ||
|
|
8884e65c29 | ||
|
|
2198ad840c | ||
|
|
10263ffc7f | ||
|
|
4af3637c06 | ||
|
|
94c05e90e8 | ||
|
|
69b2701fbf | ||
|
|
51aa03b256 | ||
|
|
413cd0c697 | ||
|
|
d01b853335 | ||
|
|
d9c0223703 | ||
|
|
37877582e7 | ||
|
|
9c098103a1 | ||
|
|
e44d5f3714 | ||
|
|
d5a3f768bc | ||
|
|
cb583fb42f | ||
|
|
edb555f517 | ||
|
|
140e11865d | ||
|
|
b950484bfc | ||
|
|
634630ebbc | ||
|
|
ae1db0dd13 | ||
|
|
b7036bd284 | ||
|
|
045f4cc591 | ||
|
|
133e6d3de6 | ||
|
|
07392ed25e | ||
|
|
4ad039d034 | ||
|
|
16576c1789 | ||
|
|
591a70f9b0 | ||
|
|
bddc6bf5d1 | ||
|
|
e2733e34b4 | ||
|
|
58909340db | ||
|
|
8afa0c9b23 | ||
|
|
37b581e0dd | ||
|
|
5b3b7575c2 | ||
|
|
aa1fc43ad9 | ||
|
|
18ad38a8da | ||
|
|
ae2cfb6312 | ||
|
|
ef7030d5a9 | ||
|
|
93fb62047a | ||
|
|
798fc257aa | ||
|
|
a35bd09b5e | ||
|
|
8eb03c44c3 | ||
|
|
c684a51cf9 | ||
|
|
a438a15105 | ||
|
|
8192b1a44f | ||
|
|
47bce86ae2 | ||
|
|
5e72dce0de | ||
|
|
6d03ab27d4 | ||
|
|
830b346945 | ||
|
|
e5da63554e | ||
|
|
9b3e2c29a5 | ||
|
|
9a39aff19f | ||
|
|
f14955a019 | ||
|
|
dde6cc2d49 | ||
|
|
5617740db9 | ||
|
|
7f4071204e | ||
|
|
7a73a62a9c | ||
|
|
cb7471024b | ||
|
|
74870615df | ||
|
|
02d096d622 | ||
|
|
0d7649bee8 | ||
|
|
0776ecc96b | ||
|
|
8113728d3d | ||
|
|
528968bfe9 | ||
|
|
61b85a9273 | ||
|
|
c055f9456a | ||
|
|
fe84b3ff15 | ||
|
|
9a213c4bf6 | ||
|
|
a4a57cef92 | ||
|
|
6f6f4a14dc | ||
|
|
c50aadeb35 | ||
|
|
963b4ee69d | ||
|
|
cd76f5e50a | ||
|
|
763d50ba8d | ||
|
|
4987932f60 | ||
|
|
1036befaf1 | ||
|
|
8a4acd4c98 | ||
|
|
f93d8511e8 | ||
|
|
54c7b0e689 | ||
|
|
58ca41e5c5 | ||
|
|
bf90f4b1da | ||
|
|
7e276e8406 | ||
|
|
18e2078b8b | ||
|
|
4b6ac87419 | ||
|
|
1d527db305 | ||
|
|
3af93f0adb | ||
|
|
40cf337b23 | ||
|
|
f5f011f1e0 | ||
|
|
73df0ae1a8 | ||
|
|
f0cfd87a5a | ||
|
|
97132c28a4 | ||
|
|
930fcf01bf | ||
|
|
d9a1ed6df1 | ||
|
|
4a28c64dee | ||
|
|
cec5d1e1b6 | ||
|
|
4cd1de4101 | ||
|
|
5fb207492e | ||
|
|
9779c75df4 | ||
|
|
2baa258e11 | ||
|
|
3f004dcad3 | ||
|
|
73d419cb39 | ||
|
|
44dbeebd40 | ||
|
|
0d63bc4a41 | ||
|
|
ae53492751 | ||
|
|
1d89917e88 | ||
|
|
409cada93a | ||
|
|
c141a15215 | ||
|
|
ab7cef5a97 | ||
|
|
c10474fd30 | ||
|
|
246a59a794 | ||
|
|
12d4e26aa3 | ||
|
|
fbee4b99e4 | ||
|
|
28b1b96cfb | ||
|
|
d23c2eaa30 | ||
|
|
12ad8211fe | ||
|
|
b41230db53 | ||
|
|
8211fd8dc4 | ||
|
|
0afbd148cd | ||
|
|
91b2f5da53 | ||
|
|
1dd5e9c649 | ||
|
|
2c293d2231 | ||
|
|
84142bb42a | ||
|
|
bb07eced6e | ||
|
|
901218cdb2 | ||
|
|
6699facc24 | ||
|
|
74c92ef215 |
@@ -7,12 +7,11 @@
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(npm test)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(echo)",
|
||||
"Bash(rm:*)"
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(strings:*)",
|
||||
"Bash(pkill:*)",
|
||||
"Bash(true)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{*.js,*.jsx,*.ts,*.tsx}]
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
|
||||
@@ -29,3 +29,10 @@ project: &project
|
||||
- settings.gradle
|
||||
- frontend/**
|
||||
- docker/**
|
||||
- testing/**
|
||||
|
||||
frontend: &frontend
|
||||
- frontend/**
|
||||
- .github/workflows/testdriver.yml
|
||||
- testing/**
|
||||
- docker/**
|
||||
|
||||
@@ -46,6 +46,9 @@ labels:
|
||||
- label: 'API'
|
||||
title: '.*openapi.*|.*swagger.*|.*api.*'
|
||||
|
||||
- label: 'v2'
|
||||
base-branch: 'V2'
|
||||
|
||||
- label: 'Translation'
|
||||
files:
|
||||
- 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
|
||||
@@ -62,6 +65,7 @@ labels:
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
- 'frontend/**'
|
||||
|
||||
- label: 'Java'
|
||||
files:
|
||||
@@ -120,6 +124,7 @@ labels:
|
||||
- 'scripts/installFonts.sh'
|
||||
- 'test.sh'
|
||||
- 'test2.sh'
|
||||
- 'docker/**'
|
||||
|
||||
- label: 'Devtools'
|
||||
files:
|
||||
@@ -131,7 +136,6 @@ labels:
|
||||
- '.github/workflows/pre_commit.yml'
|
||||
- 'devGuide/.*'
|
||||
- 'devTools/.*'
|
||||
- 'devTools/.*'
|
||||
|
||||
- label: 'Test'
|
||||
files:
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
color: "DEDEDE"
|
||||
- name: "v2"
|
||||
color: "FFFF00"
|
||||
description: "Issues or pull requests related to the v2 branch"
|
||||
- name: "wontfix"
|
||||
description: "This will not be worked on"
|
||||
color: "FFFFFF"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
pip
|
||||
setuptools
|
||||
WeasyPrint
|
||||
pdf2image
|
||||
pillow
|
||||
unoserver
|
||||
opencv-python-headless
|
||||
pre-commit
|
||||
@@ -0,0 +1,638 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||
#
|
||||
brotli==1.1.0 \
|
||||
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
|
||||
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
|
||||
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
|
||||
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
|
||||
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
|
||||
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
|
||||
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
|
||||
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
|
||||
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
|
||||
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
|
||||
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
|
||||
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
|
||||
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
|
||||
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
|
||||
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
|
||||
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
|
||||
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
|
||||
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
|
||||
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
|
||||
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
|
||||
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
|
||||
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
|
||||
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
|
||||
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
|
||||
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
|
||||
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
|
||||
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
|
||||
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
|
||||
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
|
||||
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
|
||||
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
|
||||
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
|
||||
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
|
||||
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
|
||||
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
|
||||
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
|
||||
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
|
||||
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
|
||||
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
|
||||
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
|
||||
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
|
||||
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
|
||||
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
|
||||
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
|
||||
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
|
||||
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
|
||||
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
|
||||
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
|
||||
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
|
||||
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
|
||||
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
|
||||
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
|
||||
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
|
||||
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
|
||||
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
|
||||
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
|
||||
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
|
||||
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
|
||||
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
|
||||
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
|
||||
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
|
||||
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
|
||||
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
|
||||
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
|
||||
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
|
||||
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
|
||||
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
|
||||
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
|
||||
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
|
||||
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
|
||||
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
|
||||
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
|
||||
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
|
||||
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
|
||||
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
|
||||
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
|
||||
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
|
||||
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
|
||||
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
|
||||
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
|
||||
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
|
||||
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
|
||||
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
|
||||
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
|
||||
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
|
||||
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
|
||||
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
|
||||
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
|
||||
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
|
||||
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
|
||||
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
|
||||
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
|
||||
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
|
||||
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
|
||||
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
|
||||
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
|
||||
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
|
||||
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
|
||||
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
|
||||
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
|
||||
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
|
||||
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
|
||||
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
|
||||
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
|
||||
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
|
||||
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
|
||||
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
|
||||
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
|
||||
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
|
||||
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
|
||||
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
|
||||
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
|
||||
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
|
||||
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
|
||||
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
|
||||
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
|
||||
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
|
||||
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
|
||||
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
|
||||
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
|
||||
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
|
||||
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
|
||||
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
|
||||
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
|
||||
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
|
||||
# via fonttools
|
||||
cffi==1.17.1 \
|
||||
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
|
||||
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
|
||||
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
|
||||
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
|
||||
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
|
||||
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
|
||||
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
|
||||
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
|
||||
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
|
||||
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
|
||||
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
|
||||
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
|
||||
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
|
||||
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
|
||||
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
|
||||
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
|
||||
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
|
||||
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
|
||||
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
|
||||
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
|
||||
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
|
||||
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
|
||||
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
|
||||
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
|
||||
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
|
||||
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
|
||||
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
|
||||
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
|
||||
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
|
||||
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
|
||||
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
|
||||
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
|
||||
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
|
||||
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
|
||||
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
|
||||
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
|
||||
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
|
||||
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
|
||||
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
|
||||
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
|
||||
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
|
||||
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
|
||||
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
|
||||
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
|
||||
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
|
||||
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
|
||||
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
|
||||
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
|
||||
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
|
||||
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
|
||||
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
|
||||
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
|
||||
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
|
||||
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
|
||||
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
|
||||
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
|
||||
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
|
||||
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
|
||||
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
|
||||
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
|
||||
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
|
||||
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
|
||||
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
|
||||
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
|
||||
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
|
||||
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
|
||||
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
|
||||
# via weasyprint
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
# via pre-commit
|
||||
cssselect2==0.8.0 \
|
||||
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
|
||||
--hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a
|
||||
# via weasyprint
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
# via virtualenv
|
||||
fonttools==4.59.0 \
|
||||
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
|
||||
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
|
||||
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
|
||||
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
|
||||
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
|
||||
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
|
||||
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
|
||||
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
|
||||
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
|
||||
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
|
||||
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
|
||||
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
|
||||
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
|
||||
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
|
||||
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
|
||||
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
|
||||
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
|
||||
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
|
||||
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
|
||||
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
|
||||
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
|
||||
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
|
||||
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
|
||||
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
|
||||
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
|
||||
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
|
||||
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
|
||||
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
|
||||
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
|
||||
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
|
||||
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
|
||||
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
|
||||
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
|
||||
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
|
||||
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
|
||||
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
|
||||
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
|
||||
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
|
||||
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
|
||||
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
|
||||
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
|
||||
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
|
||||
# via weasyprint
|
||||
identify==2.6.13 \
|
||||
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
|
||||
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
numpy==2.2.6 \
|
||||
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
|
||||
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
|
||||
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
|
||||
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
|
||||
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
|
||||
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
|
||||
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
|
||||
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
|
||||
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
|
||||
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
|
||||
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
|
||||
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
|
||||
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
|
||||
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
|
||||
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
|
||||
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
|
||||
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
|
||||
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
|
||||
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
|
||||
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
|
||||
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
|
||||
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
|
||||
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
|
||||
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
|
||||
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
|
||||
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
|
||||
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
|
||||
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
|
||||
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
|
||||
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
|
||||
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
|
||||
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
|
||||
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
|
||||
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
|
||||
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
|
||||
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
|
||||
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
|
||||
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
|
||||
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
|
||||
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
|
||||
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
|
||||
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
|
||||
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
|
||||
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
|
||||
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
|
||||
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
|
||||
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
|
||||
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
|
||||
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
|
||||
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
|
||||
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
|
||||
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
|
||||
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
|
||||
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
|
||||
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
|
||||
# via opencv-python-headless
|
||||
opencv-python-headless==4.12.0.88 \
|
||||
--hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \
|
||||
--hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \
|
||||
--hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \
|
||||
--hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \
|
||||
--hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \
|
||||
--hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \
|
||||
--hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pdf2image==1.17.0 \
|
||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pillow==11.3.0 \
|
||||
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
|
||||
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
|
||||
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
|
||||
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
|
||||
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
|
||||
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
|
||||
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
|
||||
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
|
||||
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
|
||||
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
|
||||
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
|
||||
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
|
||||
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
|
||||
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
|
||||
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
|
||||
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
|
||||
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
|
||||
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
|
||||
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
|
||||
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
|
||||
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
|
||||
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
|
||||
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
|
||||
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
|
||||
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
|
||||
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
|
||||
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
|
||||
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
|
||||
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
|
||||
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
|
||||
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
|
||||
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
|
||||
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
|
||||
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
|
||||
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
|
||||
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
|
||||
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
|
||||
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
|
||||
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
|
||||
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
|
||||
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
|
||||
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
|
||||
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
|
||||
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
|
||||
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
|
||||
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
|
||||
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
|
||||
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
|
||||
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
|
||||
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
|
||||
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
|
||||
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
|
||||
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
|
||||
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
|
||||
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
|
||||
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
|
||||
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
|
||||
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
|
||||
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
|
||||
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
|
||||
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
|
||||
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
|
||||
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
|
||||
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
|
||||
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
|
||||
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
|
||||
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
|
||||
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
|
||||
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
|
||||
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
|
||||
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
|
||||
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
|
||||
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
|
||||
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
|
||||
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
|
||||
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
|
||||
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
|
||||
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
|
||||
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
|
||||
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
|
||||
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
|
||||
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
|
||||
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
|
||||
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
|
||||
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
|
||||
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
|
||||
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
|
||||
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
|
||||
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
|
||||
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
|
||||
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
|
||||
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
|
||||
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
|
||||
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
|
||||
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
|
||||
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
|
||||
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
|
||||
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
|
||||
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
|
||||
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
|
||||
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
|
||||
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
|
||||
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
|
||||
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
|
||||
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
|
||||
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
# via virtualenv
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==2.22 \
|
||||
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
|
||||
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
|
||||
# via cffi
|
||||
pydyf==0.11.0 \
|
||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
|
||||
# via weasyprint
|
||||
pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||
# via weasyprint
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
# via pre-commit
|
||||
tinycss2==1.4.0 \
|
||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||
# via
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
tinyhtml5==2.0.0 \
|
||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||
# via weasyprint
|
||||
unoserver==3.3.2 \
|
||||
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
|
||||
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==20.33.1 \
|
||||
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
|
||||
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
|
||||
# via pre-commit
|
||||
weasyprint==66.0 \
|
||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
webencodings==0.5.1 \
|
||||
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||
# via
|
||||
# cssselect2
|
||||
# tinycss2
|
||||
# tinyhtml5
|
||||
zopfli==0.2.3.post1 \
|
||||
--hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \
|
||||
--hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \
|
||||
--hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \
|
||||
--hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \
|
||||
--hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \
|
||||
--hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \
|
||||
--hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \
|
||||
--hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \
|
||||
--hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \
|
||||
--hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \
|
||||
--hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \
|
||||
--hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \
|
||||
--hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \
|
||||
--hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \
|
||||
--hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \
|
||||
--hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \
|
||||
--hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \
|
||||
--hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \
|
||||
--hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \
|
||||
--hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \
|
||||
--hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \
|
||||
--hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \
|
||||
--hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \
|
||||
--hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \
|
||||
--hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \
|
||||
--hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \
|
||||
--hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \
|
||||
--hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \
|
||||
--hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \
|
||||
--hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \
|
||||
--hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \
|
||||
--hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \
|
||||
--hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \
|
||||
--hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \
|
||||
--hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \
|
||||
--hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \
|
||||
--hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \
|
||||
--hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \
|
||||
--hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \
|
||||
--hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \
|
||||
--hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \
|
||||
--hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \
|
||||
--hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \
|
||||
--hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \
|
||||
--hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \
|
||||
--hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \
|
||||
--hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \
|
||||
--hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \
|
||||
--hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \
|
||||
--hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \
|
||||
--hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \
|
||||
--hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \
|
||||
--hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \
|
||||
--hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \
|
||||
--hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \
|
||||
--hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \
|
||||
--hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \
|
||||
--hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \
|
||||
--hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \
|
||||
--hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \
|
||||
--hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \
|
||||
--hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \
|
||||
--hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \
|
||||
--hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \
|
||||
--hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \
|
||||
--hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \
|
||||
--hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \
|
||||
--hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \
|
||||
--hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \
|
||||
--hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \
|
||||
--hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==25.2 \
|
||||
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
|
||||
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
|
||||
@@ -8,86 +8,106 @@ cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||
# via pre-commit
|
||||
distlib==0.3.9 \
|
||||
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
||||
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
||||
distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
filelock==3.19.1 \
|
||||
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
|
||||
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
|
||||
# via virtualenv
|
||||
identify==2.6.12 \
|
||||
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
||||
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
platformdirs==4.4.0 \
|
||||
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
|
||||
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
|
||||
# via virtualenv
|
||||
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 \
|
||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github/scripts/requirements_pre_commit.in
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
virtualenv==20.31.2 \
|
||||
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
||||
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
||||
virtualenv==20.34.0 \
|
||||
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
|
||||
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
|
||||
# via pre-commit
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||
@@ -7,4 +7,4 @@
|
||||
tomlkit==0.13.3 \
|
||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||
# via -r .github\scripts\requirements_sync_readme.in
|
||||
# via -r .github/scripts/requirements_sync_readme.in
|
||||
|
||||
@@ -1,504 +0,0 @@
|
||||
name: Auto PR V2 Deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-pr:
|
||||
if: github.event.action != 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_deploy: ${{ steps.check-conditions.outputs.should_deploy }}
|
||||
pr_number: ${{ github.event.number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check deployment conditions
|
||||
id: check-conditions
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
echo "PR Title: $PR_TITLE"
|
||||
echo "PR Author: $PR_AUTHOR"
|
||||
echo "PR Branch: $PR_BRANCH"
|
||||
echo "PR Base Branch: ${{ github.event.pull_request.base.ref }}"
|
||||
|
||||
# Define authorized users
|
||||
authorized_users=(
|
||||
"Frooodle"
|
||||
"sf298"
|
||||
"Ludy87"
|
||||
"LaserKaspar"
|
||||
"sbplat"
|
||||
"reecebrowne"
|
||||
"DarioGii"
|
||||
"ConnorYoh"
|
||||
"EthanHealy01"
|
||||
"jbrunton96"
|
||||
)
|
||||
|
||||
# Check if author is in the authorized list
|
||||
is_authorized=false
|
||||
for user in "${authorized_users[@]}"; do
|
||||
if [[ "$PR_AUTHOR" == "$user" ]]; then
|
||||
is_authorized=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# If PR is targeting V2 and user is authorized, deploy unconditionally
|
||||
PR_BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [[ "$PR_BASE_BRANCH" == "V2" && "$is_authorized" == "true" ]]; then
|
||||
echo "✅ Deployment forced: PR targets V2 and author is authorized."
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Otherwise, continue with original keyword checks
|
||||
has_v2_keyword=false
|
||||
if [[ "$PR_TITLE" =~ [Vv]2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?[Tt]wo ]]; then
|
||||
has_v2_keyword=true
|
||||
fi
|
||||
|
||||
has_branch_keyword=false
|
||||
if [[ "$PR_BRANCH" =~ [Vv]2 ]] || [[ "$PR_BRANCH" =~ [Rr]eact ]]; then
|
||||
has_branch_keyword=true
|
||||
fi
|
||||
|
||||
if [[ "$is_authorized" == "true" && ( "$has_v2_keyword" == "true" || "$has_branch_keyword" == "true" ) ]]; then
|
||||
echo "✅ Deployment conditions met"
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Deployment conditions not met"
|
||||
echo " - Authorized user: $is_authorized"
|
||||
echo " - Has V2 keyword in title: $has_v2_keyword"
|
||||
echo " - Has V2/React keyword in branch: $has_branch_keyword"
|
||||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
if: steps.check-conditions.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
# For forks, use the full repository name, for internal PRs use the current repo
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
repository="${{ github.event.pull_request.head.repo.full_name }}"
|
||||
else
|
||||
repository="${{ github.repository }}"
|
||||
fi
|
||||
|
||||
echo "repository=$repository" >> $GITHUB_OUTPUT
|
||||
echo "ref=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: check-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-pr.outputs.should_deploy == 'true'
|
||||
# Concurrency control - only one deployment per PR at a time
|
||||
concurrency:
|
||||
group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
|
||||
- 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: Add deployment started comment
|
||||
id: deployment-started
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
|
||||
// Delete previous V2 deployment comments to avoid clutter
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(comment =>
|
||||
comment.body.includes('🚀 **Auto-deploying V2 version**') ||
|
||||
comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') ||
|
||||
comment.body.includes('❌ **V2 Auto-deployment failed**')
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
console.log(`Deleting old V2 comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
// Create new deployment started comment
|
||||
const { data: newComment } = await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
|
||||
});
|
||||
|
||||
return newComment.id;
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy V2 to VPS
|
||||
id: deploy
|
||||
run: |
|
||||
# Use same port strategy as regular PRs - just the PR number
|
||||
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
|
||||
|
||||
# Create docker-compose for V2 with separate frontend and backend
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf-v2-backend:
|
||||
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080" # Backend API port
|
||||
volumes:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
||||
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:${V2_PORT}"
|
||||
restart: on-failure:5
|
||||
|
||||
stirling-pdf-v2-frontend:
|
||||
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:${BACKEND_PORT}"
|
||||
depends_on:
|
||||
- stirling-pdf-v2-backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Deploy to VPS
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose-v2.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
# Create V2 PR-specific directories
|
||||
mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose-v2.yml /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Stop any existing container and clean up
|
||||
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
|
||||
docker-compose down --remove-orphans 2>/dev/null || true
|
||||
|
||||
# Start the new container
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
# Clean up unused Docker resources to save space
|
||||
docker system prune -af --volumes || true
|
||||
|
||||
# Clean up old backend/frontend images (older than 2 weeks)
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
# Set port for output
|
||||
echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post V2 deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
const v2Port = ${{ steps.deploy.outputs.v2_port }};
|
||||
|
||||
// Delete the "deploying..." comment since we're posting the final result
|
||||
const deploymentStartedId = ${{ steps.deployment-started.outputs.result }};
|
||||
if (deploymentStartedId) {
|
||||
console.log(`Deleting deployment started comment: ${deploymentStartedId}`);
|
||||
try {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: deploymentStartedId
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Could not delete deployment started comment: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
|
||||
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- 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: Clean up V2 deployment comments
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
|
||||
// Find and delete V2 deployment comments
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(c =>
|
||||
c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") &&
|
||||
c.user?.type === "Bot"
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
console.log(`Deleted V2 deployment comment (ID: ${comment.id})`);
|
||||
}
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup V2 deployment
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found V2 PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove V2 containers
|
||||
cd /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove V2 PR-specific directories
|
||||
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Clean up V2 containers by name (in case compose cleanup missed them)
|
||||
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
|
||||
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "V2 cleanup completed"
|
||||
else
|
||||
echo "V2 PR directory not found, nothing to clean up"
|
||||
fi
|
||||
|
||||
# Clean up old unused images (older than 2 weeks) but keep recent ones for reuse
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
|
||||
# Note: We don't remove the commit-based images since they can be reused across PRs
|
||||
# Only remove PR-specific containers and directories
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
continue-on-error: true
|
||||
|
||||
@@ -29,25 +29,22 @@ jobs:
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
comment_id: ${{ github.event.comment.id }}
|
||||
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@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -60,36 +57,13 @@ jobs:
|
||||
|
||||
- name: Get PR data
|
||||
id: get-pr
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.issue.number;
|
||||
console.log(`PR Number: ${prNumber}`);
|
||||
core.setOutput('pr_number', prNumber);
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.issue.number;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// For forks, use the full repository name, for internal PRs use the current repo
|
||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
||||
|
||||
console.log(`PR Repository: ${repository}`);
|
||||
console.log(`PR Branch: ${pr.head.ref}`);
|
||||
|
||||
core.setOutput('repository', repository);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
|
||||
- name: Check for security/login flag
|
||||
id: check-security-flag
|
||||
env:
|
||||
@@ -124,7 +98,7 @@ jobs:
|
||||
|
||||
- name: Add 'in_progress' reaction to comment
|
||||
id: add-eyes-reaction
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -153,12 +127,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -170,14 +144,13 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
@@ -197,7 +170,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
@@ -297,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Add success reaction to comment
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -332,7 +305,7 @@ jobs:
|
||||
|
||||
- name: Add failure reaction to comment
|
||||
if: failure()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -352,7 +325,7 @@ jobs:
|
||||
|
||||
- name: Post deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Remove 'pr-deployed' label if present
|
||||
id: remove-label-comment
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: AI PR Title Analysis
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: ai-title-analysis
|
||||
uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7
|
||||
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt-file: ".github/config/system-prompt.txt"
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Post comment on PR if needed
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
@@ -13,11 +13,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
+80
-69
@@ -1,9 +1,11 @@
|
||||
name: Build and Test Workflow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main", "V2", "V2-gha"]
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
@@ -25,52 +27,60 @@ jobs:
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
build: ${{ steps.changes.outputs.build }}
|
||||
app: ${{ steps.changes.outputs.app }}
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@v3.0.2
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
filters: ".github/config/.files.yaml"
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
spring-security: [true, false]
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build -PnoSpotless
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
id: check-reports
|
||||
if: always()
|
||||
run: |
|
||||
declare -a dirs=(
|
||||
@@ -81,102 +91,103 @@ jobs:
|
||||
"app/proprietary/build/reports/tests/"
|
||||
"app/proprietary/build/test-results/"
|
||||
)
|
||||
missing_reports=()
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Missing $dir"
|
||||
exit 1
|
||||
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@v4.6.2
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
app/core/build/reports/tests/
|
||||
app/core/build/test-results/
|
||||
app/core/build/reports/problems/
|
||||
app/common/build/reports/tests/
|
||||
app/common/build/test-results/
|
||||
app/common/build/reports/problems/
|
||||
app/proprietary/build/reports/tests/
|
||||
app/proprietary/build/test-results/
|
||||
app/proprietary/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed]
|
||||
needs: [files-changed, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@v2.12.2
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
- uses: gradle/actions/setup-gradle@v4.4.1
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: openapi-docs
|
||||
path: ./SwaggerDoc.json
|
||||
|
||||
frontend-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
- name: Build frontend
|
||||
run: cd frontend && npm run build
|
||||
- name: Run frontend tests
|
||||
run: cd frontend && npm run test -- --run
|
||||
- name: Upload frontend build artifacts
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/dist/
|
||||
retention-days: 3
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
- name: check the licenses for compatibility
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
- name: FAILED - Check licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
path: |
|
||||
build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
retention-days: 3
|
||||
|
||||
docker-compose-tests:
|
||||
@@ -200,15 +211,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
@@ -222,7 +233,7 @@ jobs:
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: 'pip' # caching pip dependencies
|
||||
@@ -249,21 +260,21 @@ jobs:
|
||||
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -285,7 +296,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/backend/${{ matrix.docker-rev }}
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
- name: Get PR data
|
||||
id: get-pr-data
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
|
||||
- name: Determine reference file test
|
||||
id: determine-file
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
|
||||
- name: Post comment on PR
|
||||
if: env.SCRIPT_OUTPUT != ''
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -17,11 +17,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
name: Auto V2 Deploy on Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- deploy-on-v2-commit
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-v2-on-push:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
chmod 600 ../private.key
|
||||
|
||||
|
||||
- name: Deploy to VPS on port 3000
|
||||
run: |
|
||||
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||
|
||||
cat > $UNIQUE_NAME << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
backend:
|
||||
container_name: stirling-v2-backend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "13000:8080"
|
||||
volumes:
|
||||
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2/config:/configs:rw
|
||||
- /stirling/V2/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2"
|
||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "http://${{ secrets.VPS_HOST }}:3000"
|
||||
restart: on-failure:5
|
||||
|
||||
frontend:
|
||||
container_name: stirling-v2-frontend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Copy to remote with unique name
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||
|
||||
# SSH and rename/move atomically to avoid interference
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
mkdir -p /stirling/V2/{data,config,logs}
|
||||
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||
cd /stirling/V2
|
||||
docker-compose down || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker system prune -af --volumes || true
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
name: Frontend License Report Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
pull_request:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generate-frontend-license-report:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Generate frontend license report
|
||||
working-directory: frontend
|
||||
run: npm run generate-licenses
|
||||
|
||||
- name: Check for license warnings
|
||||
run: |
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
// Get all comments on the PR
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Filter for license check comments
|
||||
const licenseComments = comments.filter(comment =>
|
||||
comment.body.includes('## ✅ Frontend License Check Passed') ||
|
||||
comment.body.includes('## ❌ Frontend License Check Failed')
|
||||
);
|
||||
|
||||
// Delete old license check comments
|
||||
for (const comment of licenseComments) {
|
||||
console.log(`Deleting old license check comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
|
||||
|
||||
let commentBody;
|
||||
|
||||
if (hasWarnings) {
|
||||
// Read warnings file to get specific issues
|
||||
const fs = require('fs');
|
||||
let warningDetails = '';
|
||||
try {
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
|
||||
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
|
||||
} catch (e) {
|
||||
warningDetails = 'Unable to read warning details';
|
||||
}
|
||||
|
||||
commentBody = `## ❌ Frontend License Check Failed
|
||||
|
||||
The frontend license check has detected compatibility warnings that require review:
|
||||
|
||||
${warningDetails}
|
||||
|
||||
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
||||
|
||||
_This check will fail the PR until license issues are resolved._`;
|
||||
} else {
|
||||
commentBody = `## ✅ Frontend License Check Passed
|
||||
|
||||
All frontend licenses have been validated and no compatibility warnings were detected.
|
||||
|
||||
The frontend license report has been updated successfully.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
- name: Fail workflow if license warnings exist (PR only)
|
||||
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: |
|
||||
echo "❌ License warnings detected. Failing the workflow."
|
||||
exit 1
|
||||
|
||||
# Push Event: Commit license files and create PR
|
||||
- name: Commit changes (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
git add frontend/src/assets/3rdPartyLicenses.json
|
||||
# Note: Do NOT commit license-warnings.json - it's only for PR review
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Prepare PR body (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||
|
||||
This PR updates the frontend license report based on changes to package.json dependencies."
|
||||
|
||||
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
|
||||
## ⚠️ License Compatibility Warnings
|
||||
|
||||
The following licenses may require review for corporate compatibility:
|
||||
|
||||
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
|
||||
Please review these licenses to ensure they are acceptable for your use case."
|
||||
fi
|
||||
|
||||
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$PR_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request (Push only)
|
||||
id: cpr
|
||||
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update Frontend 3rd Party Licenses"
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: update-frontend-3rd-party-licenses
|
||||
base: V2
|
||||
title: "Update Frontend 3rd Party Licenses"
|
||||
body: ${{ env.PR_BODY }}
|
||||
labels: Licenses,github-actions,frontend
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
- name: Enable Pull Request Automerge (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Add review required label (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -48,16 +48,19 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
- name: Upload artifact on failure
|
||||
if: failure()
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
|
||||
@@ -21,14 +21,14 @@ jobs:
|
||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
@@ -60,19 +60,19 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -110,12 +110,12 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: stirling-${{ matrix.file_suffix }}binaries
|
||||
|
||||
@@ -148,19 +148,19 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -238,12 +238,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: ${{ matrix.platform }}binaries
|
||||
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- name: Generate key pair
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -301,16 +301,16 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download signed artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- 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@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||
with:
|
||||
tag_name: v${{ needs.read_versions.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -12,17 +12,21 @@ permissions:
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Prevents sdist builds → no tar extraction
|
||||
PIP_ONLY_BINARY: ":all:"
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -34,7 +38,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: 'pip' # caching pip dependencies
|
||||
@@ -42,13 +46,13 @@ jobs:
|
||||
|
||||
- name: Run Pre-Commit Hooks
|
||||
run: |
|
||||
pip install --require-hashes -r ./.github/scripts/requirements_pre_commit.txt
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_pre_commit.txt
|
||||
|
||||
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
||||
continue-on-error: true
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: "temurin"
|
||||
|
||||
@@ -30,19 +30,19 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
@@ -67,13 +67,13 @@ jobs:
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -23,19 +23,19 @@ jobs:
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -83,19 +83,19 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: binaries${{ matrix.file_suffix }}
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- name: Generate key pair
|
||||
run: cosign generate-key-pair
|
||||
@@ -161,17 +161,17 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download signed artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: signed${{ matrix.file_suffix }}
|
||||
|
||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||
with:
|
||||
tag_name: v${{ needs.build.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -34,17 +34,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -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@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -30,16 +30,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Build and analyze with Gradle
|
||||
env:
|
||||
|
||||
@@ -16,12 +16,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 30 days stale issues
|
||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
|
||||
@@ -26,19 +26,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
|
||||
@@ -30,13 +30,17 @@ permissions:
|
||||
jobs:
|
||||
sync-files:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Prevents sdist builds → no tar extraction
|
||||
PIP_ONLY_BINARY: ":all:"
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -46,7 +50,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
@@ -61,7 +65,9 @@ jobs:
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
# Wheels-only + Hash-Pinning
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
|
||||
@@ -24,21 +24,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
@@ -116,28 +116,47 @@ jobs:
|
||||
docker-compose up -d
|
||||
EOF
|
||||
|
||||
files-changed:
|
||||
if: always()
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: ".github/config/.files.yaml"
|
||||
|
||||
test:
|
||||
needs: deploy
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
needs: [deploy, files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Run TestDriver.ai
|
||||
uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
|
||||
with:
|
||||
key: ${{secrets.TESTDRIVER_API_KEY}}
|
||||
prerun: |
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
npm install dashcam-chrome --save
|
||||
@@ -156,7 +175,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -167,6 +186,7 @@ jobs:
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup deployment
|
||||
if: always()
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << EOF
|
||||
cd /stirling/test-${{ github.sha }}
|
||||
@@ -174,3 +194,4 @@ jobs:
|
||||
cd /stirling
|
||||
rm -rf test-${{ github.sha }}
|
||||
EOF
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
|
||||
+3
-3
@@ -27,7 +27,6 @@ clientWebUI/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
/stirling/
|
||||
/testing/file_snapshots
|
||||
SwaggerDoc.json
|
||||
|
||||
@@ -197,9 +196,10 @@ id_ed25519.pub
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
|
||||
|
||||
**/jcef-bundle/
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
|
||||
# weasyPrint
|
||||
**/LOCAL_APPDATA_FONTCONFIG_CACHE/**
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
- **Build project**: `./gradlew clean build`
|
||||
- **Run locally**: `./gradlew bootRun`
|
||||
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
|
||||
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
|
||||
|
||||
### Docker Development
|
||||
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
|
||||
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
|
||||
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
|
||||
- **Example compose files**: Located in `exampleYmlFiles/` directory
|
||||
|
||||
### Security Mode Development
|
||||
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
|
||||
|
||||
### Frontend Development
|
||||
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
|
||||
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
|
||||
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
|
||||
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
|
||||
- **Package Installation**: DO NOT run npm install commands - package management handled separately
|
||||
- **Deployment Options**:
|
||||
- **Desktop App**: `npm run tauri-build` (native desktop application)
|
||||
- **Web Server**: `npm run build` then serve dist/ folder
|
||||
- **Development**: `npm run tauri-dev` for desktop dev mode
|
||||
|
||||
#### Multi-Tool Workflow Architecture
|
||||
Frontend designed for **stateful document processing**:
|
||||
- Users upload PDFs once, then chain tools (split → merge → compress → view)
|
||||
- File state and processing results persist across tool switches
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `src/contexts/FileContext.tsx`
|
||||
- **Active files**: Currently loaded PDFs and their variants
|
||||
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
|
||||
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
|
||||
- **IndexedDB persistence**: File storage with thumbnail caching
|
||||
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
|
||||
|
||||
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
|
||||
|
||||
#### Processing Services
|
||||
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
|
||||
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
|
||||
- **fileStorage**: IndexedDB with LRU cache management
|
||||
|
||||
#### Memory Management Strategy
|
||||
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
|
||||
- PDF.js documents that need explicit .destroy() calls
|
||||
- Blob URLs from tool outputs that need revocation
|
||||
- Web Workers that need termination
|
||||
Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
#### Tool Development
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- Coordinates all tool operations with consistent interface
|
||||
- Integrates with FileContext for operation tracking
|
||||
- Handles validation, error handling, and UI state management
|
||||
|
||||
- **Supporting Hooks**:
|
||||
- **useToolState**: UI state management (loading, progress, error, files)
|
||||
- **useToolApiCalls**: HTTP requests and file processing
|
||||
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
|
||||
|
||||
- **Utilities**:
|
||||
- **toolErrorHandler**: Standardized error extraction and i18n support
|
||||
- **toolResponseProcessor**: API response handling (single/zip/custom)
|
||||
- **toolOperationTracker**: FileContext integration utilities
|
||||
|
||||
**Three Tool Patterns**:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
buildFormData: (params, file: File) => { /* single file */ },
|
||||
multiFileEndpoint: false,
|
||||
filePrefix: 'compressed_'
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'split',
|
||||
endpoint: '/api/v1/general/split-pages',
|
||||
buildFormData: (params, files: File[]) => { /* all files */ },
|
||||
multiFileEndpoint: true,
|
||||
filePrefix: 'split_'
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'convert',
|
||||
customProcessor: async (params, files) => { /* custom logic */ },
|
||||
filePrefix: 'converted_'
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
|
||||
- **Consistent**: All tools follow same pattern and interface
|
||||
- **Maintainable**: Single responsibility hooks, easy to test and modify
|
||||
- **i18n Ready**: Built-in internationalization support
|
||||
- **Type Safe**: Full TypeScript support with generic interfaces
|
||||
- **Memory Safe**: Automatic resource cleanup and blob URL management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- **Configuration**: YAML-based configuration with environment variable overrides
|
||||
|
||||
### Controller Architecture
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
|
||||
- Pattern: `@Controller` + return template names
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
- **ConfigInitializer**: Handles runtime configuration and settings files
|
||||
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
|
||||
- **Security Layer**: Authentication, authorization, and user management (when enabled)
|
||||
|
||||
### Component Architecture
|
||||
- **React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
|
||||
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
|
||||
- **Internationalization**:
|
||||
- Backend: `messages_*.properties` files
|
||||
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
|
||||
- Conversion Script: `scripts/convert_properties_to_json.py`
|
||||
|
||||
### Configuration Modes
|
||||
- **Ultra-lite**: Basic PDF operations only
|
||||
- **Standard**: Full feature set
|
||||
- **Fat**: Pre-downloaded dependencies for air-gapped environments
|
||||
- **Security Mode**: Adds authentication, user management, and enterprise features
|
||||
|
||||
### Testing Strategy
|
||||
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
|
||||
- **Docker Testing**: `test.sh` validates all Docker variants
|
||||
- **Manual Testing**: No unit tests currently - relies on UI and API testing
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Local Development**:
|
||||
- Backend: `./gradlew bootRun` (runs on localhost:8080)
|
||||
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
|
||||
2. **Docker Testing**: Use `./test.sh` before submitting PRs
|
||||
3. **Code Style**: Spotless enforces Google Java Format automatically
|
||||
4. **Translations**:
|
||||
- Backend: Use helper scripts in `/scripts` for multi-language updates
|
||||
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
|
||||
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
|
||||
|
||||
## Frontend Architecture Status
|
||||
|
||||
- **Core Status**: React SPA architecture complete with multi-tool workflow support
|
||||
- **State Management**: FileContext handles all file operations and tool navigation
|
||||
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
|
||||
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
|
||||
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
|
||||
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
|
||||
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **Desktop Mode**: Set `STIRLING_PDF_DESKTOP_UI=true` for desktop application mode
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
|
||||
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
|
||||
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
|
||||
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
|
||||
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
|
||||
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
|
||||
|
||||
## Communication Style
|
||||
- Be direct and to the point
|
||||
- No apologies or conversational filler
|
||||
- Answer questions directly without preamble
|
||||
- Explain reasoning concisely when asked
|
||||
- Avoid unnecessary elaboration
|
||||
|
||||
## Decision Making
|
||||
- Ask clarifying questions before making assumptions
|
||||
- Stop and ask when uncertain about project-specific details
|
||||
- Confirm approach before making structural changes
|
||||
- Request guidance on preferences (cross-platform vs specific tools, etc.)
|
||||
- Verify understanding of requirements before proceeding
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
||||
|
||||
1. Automatic Backup Creation
|
||||
- The system automatically creates a database backup every day at midnight. This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
||||
- The system automatically creates a database backup on a configurable schedule (default: daily at midnight via `system.databaseBackup.cron`). This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
||||
2. Manual Backup Export
|
||||
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
|
||||
3. Importing Database Backups
|
||||
|
||||
@@ -1,692 +0,0 @@
|
||||
# Stirling-PDF Developer Guide
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
|
||||
|
||||
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||
|
||||
## 2. Project Overview
|
||||
|
||||
**Stirling 2.0** is built using:
|
||||
|
||||
**Backend:**
|
||||
- Spring Boot (Java 17+, JDK 21 recommended)
|
||||
- PDFBox for core PDF operations
|
||||
- LibreOffice for document conversions
|
||||
- qpdf for PDF optimization
|
||||
- Spring Security (optional, controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- Lombok for reducing boilerplate code
|
||||
|
||||
**Frontend (React SPA):**
|
||||
- React + TypeScript
|
||||
- Vite for build tooling and development server
|
||||
- Mantine UI component library
|
||||
- TailwindCSS for styling
|
||||
- PDF.js for client-side PDF rendering
|
||||
- PDF-LIB.js for client-side PDF manipulation
|
||||
- IndexedDB for client-side file storage and thumbnails
|
||||
- i18next for internationalization
|
||||
|
||||
**Infrastructure:**
|
||||
- Docker for containerization
|
||||
- Gradle for build management
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
## 3. Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker
|
||||
- Git
|
||||
- Java JDK 17 or later (JDK 21 recommended)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
|
||||
### Setup Steps
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
|
||||
cd Stirling-PDF
|
||||
```
|
||||
|
||||
2. Install Docker and JDK17 if not already installed.
|
||||
|
||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||
1. Only VSCode
|
||||
1. Open VS Code.
|
||||
2. When prompted, install the recommended extensions.
|
||||
3. Alternatively, open the command palette (`Ctrl + Shift + P` or `Cmd + Shift + P` on macOS) and run:
|
||||
|
||||
```sh
|
||||
Extensions: Show Recommended Extensions
|
||||
```
|
||||
|
||||
4. Install the required extensions from the list.
|
||||
|
||||
4. Lombok Setup
|
||||
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||
|
||||
5. Add environment variable
|
||||
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||
5. **Frontend Setup (Required for Stirling 2.0)**
|
||||
Navigate to the frontend directory and install dependencies using npm.
|
||||
|
||||
## 4. Stirling 2.0 Development Workflow
|
||||
|
||||
### Frontend Development (React)
|
||||
The frontend is a React SPA that runs independently during development:
|
||||
|
||||
1. **Start the backend**: Run the Spring Boot application (serves API endpoints on localhost:8080)
|
||||
2. **Start the frontend dev server**: Navigate to the frontend directory and run the development server (serves UI on localhost:5173)
|
||||
3. **Development flow**: The Vite dev server automatically proxies API calls to the backend
|
||||
|
||||
### File Storage Architecture
|
||||
Stirling 2.0 uses client-side file storage:
|
||||
- **IndexedDB**: Stores files locally in the browser with automatic thumbnail generation
|
||||
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
## 5. Project Structure
|
||||
|
||||
```bash
|
||||
Stirling-PDF/
|
||||
├── .github/ # GitHub-specific files (workflows, issue templates)
|
||||
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
|
||||
├── frontend/ # React SPA frontend (Stirling 2.0)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── tools/ # Tool-specific React components
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── services/ # API and utility services
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── public/
|
||||
│ │ └── locales/ # Internationalization files (JSON)
|
||||
│ ├── package.json # Frontend dependencies
|
||||
│ └── vite.config.ts # Vite configuration
|
||||
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||
├── docs/ # Documentation files
|
||||
├── exampleYmlFiles/ # Example YAML configuration files
|
||||
├── images/ # Image assets
|
||||
├── pipeline/ # Pipeline-related files (generated at runtime)
|
||||
├── scripts/ # Utility scripts
|
||||
├── src/ # Source code
|
||||
│ ├── main/
|
||||
│ │ ├── java/
|
||||
│ │ │ └── stirling/
|
||||
│ │ │ └── software/
|
||||
│ │ │ └── SPDF/
|
||||
│ │ │ ├── config/
|
||||
│ │ │ ├── controller/
|
||||
│ │ │ ├── model/
|
||||
│ │ │ ├── repository/
|
||||
│ │ │ ├── service/
|
||||
│ │ │ └── utils/
|
||||
│ │ └── resources/
|
||||
│ │ ├── static/ # Legacy static assets (reference only)
|
||||
│ │ │ ├── css/
|
||||
│ │ │ ├── js/
|
||||
│ │ │ └── pdfjs/
|
||||
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
|
||||
│ └── test/
|
||||
├── testing/ # Cucumber and integration tests
|
||||
│ └── cucumber/ # Cucumber test files
|
||||
├── build.gradle # Gradle build configuration
|
||||
├── Dockerfile # Main Dockerfile
|
||||
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version
|
||||
├── Dockerfile.fat # Dockerfile for fat version
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
||||
```
|
||||
|
||||
## 6. Docker-based Development
|
||||
|
||||
Stirling-PDF offers several Docker versions:
|
||||
|
||||
- Full: All features included
|
||||
- Ultra-Lite: Basic PDF operations only
|
||||
- Fat: Includes additional libraries and fonts predownloaded
|
||||
|
||||
### Example Docker Compose Files
|
||||
|
||||
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as:
|
||||
|
||||
- `docker-compose-latest.yml`: Latest version without login and security features
|
||||
- `docker-compose-latest-security.yml`: Latest version with login and security features enabled
|
||||
- `docker-compose-latest-fat-security.yml`: Fat version with login and security features enabled
|
||||
|
||||
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
PUID: 1002
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
SHOW_SURVEY: "true"
|
||||
restart: on-failure:5
|
||||
```
|
||||
|
||||
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
|
||||
```
|
||||
|
||||
### Building Docker Images
|
||||
|
||||
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
|
||||
|
||||
1. Set the security environment variable:
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
|
||||
```
|
||||
|
||||
2. Build the project with Gradle:
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
3. Build the Docker images:
|
||||
|
||||
For the latest version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||
```
|
||||
|
||||
For the ultra-lite version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||
```
|
||||
|
||||
For the fat version (with login and security features enabled):
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=false
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
## 7. Testing
|
||||
|
||||
### Comprehensive Testing Script
|
||||
|
||||
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
||||
|
||||
To run the test script:
|
||||
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
This script performs the following actions:
|
||||
|
||||
1. Builds all Docker images (full, ultra-lite, fat).
|
||||
2. Runs each version to ensure it starts correctly.
|
||||
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged.
|
||||
|
||||
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
||||
|
||||
### Full Testing with Docker
|
||||
|
||||
1. Build and run the Docker container per the above instructions:
|
||||
|
||||
2. Access the application at `http://localhost:8080` and manually test all features developed.
|
||||
|
||||
### Frontend Development Testing (Stirling 2.0)
|
||||
|
||||
For React frontend development:
|
||||
|
||||
1. Start the backend: Run the Spring Boot application to serve API endpoints on localhost:8080
|
||||
2. Start the frontend dev server: Navigate to the frontend directory and run the development server on localhost:5173
|
||||
3. The Vite dev server automatically proxies API calls to the backend
|
||||
4. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
|
||||
|
||||
### Local Testing (Java and UI Components)
|
||||
|
||||
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
|
||||
|
||||
- Java backend logic
|
||||
- RESTful API endpoints
|
||||
- JavaScript functionality
|
||||
- User interface components and styling
|
||||
- Thymeleaf templates
|
||||
|
||||
To run Stirling-PDF locally:
|
||||
|
||||
1. Compile and run the project using built-in IDE methods or by running:
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
2. Access the application at `http://localhost:8080` in your web browser.
|
||||
|
||||
3. Manually test the features you're working on through the UI.
|
||||
|
||||
4. For API changes, use tools like Postman or curl to test endpoints directly.
|
||||
|
||||
Important notes:
|
||||
|
||||
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||
|
||||
## 8. Contributing
|
||||
|
||||
1. Fork the repository on GitHub.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
|
||||
4. Test your changes thoroughly in the Docker environment.
|
||||
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
|
||||
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
6. Push your changes to your fork.
|
||||
7. Submit a pull request to the main repository.
|
||||
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md).
|
||||
|
||||
When you raise a PR:
|
||||
|
||||
- The `test.sh` script will run automatically against your PR.
|
||||
- The PR checks will verify versioning and dependency updates.
|
||||
- Documentation will be automatically updated for dependency changes.
|
||||
- Security issues will be checked using Snyk and PixeeBot.
|
||||
|
||||
Address any issues that arise from these checks before finalizing your pull request.
|
||||
|
||||
## 9. API Documentation
|
||||
|
||||
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
||||
|
||||
## 10. Customization
|
||||
|
||||
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
||||
|
||||
- Application name and branding
|
||||
- Security settings
|
||||
- UI customization
|
||||
- Endpoint management
|
||||
|
||||
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
||||
```
|
||||
|
||||
Refer to the main README for a full list of customization options.
|
||||
|
||||
## 11. Language Translations
|
||||
|
||||
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
||||
|
||||
```bash
|
||||
/scripts/replace_translation_line.sh
|
||||
```
|
||||
|
||||
This script helps you make consistent replacements across language files.
|
||||
|
||||
When contributing translations:
|
||||
|
||||
1. Use the helper script for multi-file changes.
|
||||
2. Ensure all language files are updated consistently.
|
||||
3. The PR checks will verify consistency in language file updates.
|
||||
|
||||
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
|
||||
|
||||
## Code examples
|
||||
|
||||
### React Component Development (Stirling 2.0)
|
||||
|
||||
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
|
||||
|
||||
#### Creating a New Tool Component
|
||||
|
||||
1. **Create the React Component:**
|
||||
```typescript
|
||||
// frontend/src/tools/NewTool.tsx
|
||||
import { useState } from 'react';
|
||||
import { Button, FileInput, Container } from '@mantine/core';
|
||||
|
||||
interface NewToolProps {
|
||||
params: Record<string, any>;
|
||||
updateParams: (updates: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export default function NewTool({ params, updateParams }: NewToolProps) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const handleProcess = async () => {
|
||||
// Process files using API or client-side logic
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<FileInput
|
||||
multiple
|
||||
accept="application/pdf"
|
||||
onChange={setFiles}
|
||||
/>
|
||||
<Button onClick={handleProcess}>Process</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add API Integration:**
|
||||
```typescript
|
||||
// Use existing API endpoints or create new ones
|
||||
const response = await fetch('/api/v1/new-tool', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
```
|
||||
|
||||
3. **Register in Tool Picker:**
|
||||
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||
|
||||
### Legacy Reference: Overview of Thymeleaf
|
||||
|
||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||
|
||||
### Thymeleaf overview
|
||||
|
||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||
|
||||
Some examples of this are:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
```
|
||||
or
|
||||
```html
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
```
|
||||
|
||||
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||
|
||||
They can be more complex, such as:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||
```
|
||||
|
||||
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||
|
||||
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("exampleData", exampleData);
|
||||
return "new-feature";
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||
|
||||
```html
|
||||
<tbody>
|
||||
<!-- Use th:each to iterate over the list -->
|
||||
<tr th:each="person : ${exampleData}">
|
||||
<td th:text="${person.id}"></td>
|
||||
<td th:text="${person.name}"></td>
|
||||
<td th:text="${person.age}"></td>
|
||||
<td th:text="${person.email}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
```
|
||||
|
||||
This would generate n entries of tr for each person in exampleData
|
||||
|
||||
### Adding a New Feature to the Backend (API)
|
||||
|
||||
1. **Create a New Controller:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
|
||||
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
||||
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/new-feature")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class NewFeatureController {
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Define the Service Layer:** (Not required but often useful)
|
||||
- Create a new service class in the `stirling-pdf/src/main/java/stirling/software/SPDF/service` directory.
|
||||
- Implement the business logic for the new feature.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class NewFeatureService {
|
||||
|
||||
public String getNewFeatureData() {
|
||||
// Implement business logic here
|
||||
return "New Feature Data";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2b. **Integrate the Service with the Controller:**
|
||||
|
||||
- Autowire the service class in the controller and use it to handle the API request.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/new-feature")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class NewFeatureController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return newFeatureService.getNewFeatureData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Feature to the Frontend (UI)
|
||||
|
||||
1. **Create a New Thymeleaf Template:**
|
||||
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||
- Use Thymeleaf attributes to dynamically generate content.
|
||||
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<input type="hidden" id="customMode" name="customMode" value="">
|
||||
<div class="mb-3">
|
||||
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create a New Controller for the UI:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.ui;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/new-feature")
|
||||
public class NewFeatureUIController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||
return "new-feature";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update the Navigation Bar:**
|
||||
- Add a link to the new feature page in the navigation bar.
|
||||
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||
|
||||
```html
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
|
||||
### 1. Locate Existing Language Files
|
||||
|
||||
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
||||
|
||||
- `messages.properties` (default, usually English)
|
||||
- `messages_en_GB.properties`
|
||||
- `messages_fr_FR.properties`
|
||||
- `messages_de_DE.properties`
|
||||
- etc.
|
||||
|
||||
### 2. Add New Translation Entries
|
||||
|
||||
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
|
||||
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
|
||||
you might add:
|
||||
|
||||
```properties
|
||||
pdfSplitter.title=PDF Splitter
|
||||
pdfSplitter.description=Split your PDF into multiple documents
|
||||
pdfSplitter.button.split=Split PDF
|
||||
pdfSplitter.input.pages=Enter page numbers to split
|
||||
```
|
||||
|
||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||
|
||||
### 3. Use Translations in Thymeleaf Templates
|
||||
|
||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||
|
||||
```html
|
||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||
```
|
||||
|
||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||
@@ -1,40 +1,15 @@
|
||||
# Backend Dockerfile - Java Spring Boot with all dependencies and build stage
|
||||
# Build the application
|
||||
FROM gradle:8.14-jdk21 AS build
|
||||
|
||||
COPY build.gradle .
|
||||
COPY settings.gradle .
|
||||
COPY gradlew .
|
||||
COPY gradle gradle/
|
||||
COPY app/core/build.gradle core/.
|
||||
COPY app/common/build.gradle common/.
|
||||
COPY app/proprietary/build.gradle proprietary/.
|
||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the entire project to the working directory
|
||||
COPY . .
|
||||
|
||||
# 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
|
||||
|
||||
# Main stage
|
||||
FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
|
||||
|
||||
# Copy necessary files
|
||||
COPY scripts /scripts
|
||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||
# first /app directory is for the build stage, second is for the final image
|
||||
COPY --from=build /app/app/core/build/libs/*.jar app.jar
|
||||
COPY app/core/build/libs/*.jar app.jar
|
||||
|
||||
ARG VERSION_TAG
|
||||
|
||||
LABEL org.opencontainers.image.title="Stirling-PDF Backend"
|
||||
LABEL org.opencontainers.image.description="Backend service for Stirling-PDF - Java Spring Boot with PDF processing capabilities"
|
||||
LABEL org.opencontainers.image.title="Stirling-PDF"
|
||||
LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more."
|
||||
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
||||
@@ -43,10 +18,10 @@ LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
||||
LABEL maintainer="Stirling-Tools"
|
||||
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
||||
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, backend, API, Spring Boot"
|
||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark"
|
||||
|
||||
# Set Environment Variables
|
||||
ENV DISABLE_ADDITIONAL_FEATURES=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="" \
|
||||
@@ -63,7 +38,8 @@ ENV DISABLE_ADDITIONAL_FEATURES=false \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
# JDK for app and all dependencies
|
||||
|
||||
# JDK for app
|
||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||
@@ -86,37 +62,40 @@ 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 features)
|
||||
unpaper \
|
||||
tesseract-ocr-data-eng \
|
||||
tesseract-ocr-data-chi_sim \
|
||||
tesseract-ocr-data-deu \
|
||||
tesseract-ocr-data-fra \
|
||||
tesseract-ocr-data-por \
|
||||
ocrmypdf \
|
||||
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 pip setuptools && \
|
||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /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 /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
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 -Djava.io.tmpdir=/tmp/stirling-pdf -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"]
|
||||
@@ -0,0 +1,60 @@
|
||||
# dockerfile.dev
|
||||
|
||||
# Basisimage: Gradle mit JDK 17 (Debian-basiert)
|
||||
FROM gradle:8.14-jdk17
|
||||
|
||||
# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren
|
||||
USER root
|
||||
|
||||
# Set GRADLE_HOME und füge Gradle zum PATH hinzu
|
||||
ENV GRADLE_HOME=/opt/gradle
|
||||
ENV PATH="$GRADLE_HOME/bin:$PATH"
|
||||
|
||||
# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
sudo \
|
||||
libreoffice \
|
||||
poppler-utils \
|
||||
qpdf \
|
||||
# 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-urw-base35 \
|
||||
python3-uno \
|
||||
python3-venv \
|
||||
# ss -tln
|
||||
iproute2 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setze die Environment Variable für setuptools
|
||||
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
|
||||
COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt
|
||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||
&& . /opt/venv/bin/activate \
|
||||
&& pip install --no-cache-dir --require-hashes -r /tmp/requirements_dev.txt
|
||||
|
||||
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY . /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
|
||||
|
||||
# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben)
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN chmod +x /workspace/.devcontainer/git-init.sh /workspace/.devcontainer/init-setup.sh
|
||||
|
||||
# Wechsel zum Nicht‑Root Benutzer
|
||||
USER devuser
|
||||
@@ -1,4 +1,3 @@
|
||||
# Backend fat Dockerfile - Java Spring Boot with all dependencies and build stage
|
||||
# Build the application
|
||||
FROM gradle:8.14-jdk21 AS build
|
||||
|
||||
@@ -53,6 +52,7 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
|
||||
# JDK for app
|
||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||
@@ -76,37 +76,38 @@ 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)
|
||||
unpaper \
|
||||
tesseract-ocr-data-eng \
|
||||
tesseract-ocr-data-chi_sim \
|
||||
tesseract-ocr-data-deu \
|
||||
tesseract-ocr-data-fra \
|
||||
tesseract-ocr-data-por \
|
||||
ocrmypdf \
|
||||
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 && \
|
||||
python3 -m venv /opt/venv && \
|
||||
/opt/venv/bin/pip install --upgrade pip setuptools && \
|
||||
/opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \
|
||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /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 /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
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 -Djava.io.tmpdir=/tmp/stirling-pdf -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"]
|
||||
@@ -1,28 +1,4 @@
|
||||
# Backend ultra-lite Dockerfile - Java Spring Boot with minimal dependencies and build stage
|
||||
# Build the application
|
||||
FROM gradle:8.14-jdk21 AS build
|
||||
|
||||
COPY build.gradle .
|
||||
COPY settings.gradle .
|
||||
COPY gradlew .
|
||||
COPY gradle gradle/
|
||||
COPY app/core/build.gradle core/.
|
||||
COPY app/common/build.gradle common/.
|
||||
COPY app/proprietary/build.gradle proprietary/.
|
||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the entire project to the working directory
|
||||
COPY . .
|
||||
|
||||
# Build the application with DISABLE_ADDITIONAL_FEATURES=true
|
||||
RUN DISABLE_ADDITIONAL_FEATURES=true \
|
||||
STIRLING_PDF_DESKTOP_UI=false \
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
# Main stage
|
||||
# use alpine
|
||||
FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
|
||||
|
||||
ARG VERSION_TAG
|
||||
@@ -42,9 +18,10 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||
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 --from=build /app/app/core/build/libs/*.jar app.jar
|
||||
COPY app/core/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 && \
|
||||
@@ -64,7 +41,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||
chmod +x /scripts/*.sh && \
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
|
||||
# Set environment variables
|
||||
@@ -21,82 +21,97 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
||||
|
||||
## Features
|
||||
|
||||
- 50+ PDF Operations
|
||||
- Parallel file processing and downloads
|
||||
- Dark mode support
|
||||
- Custom download options
|
||||
- Custom 'Pipelines' to run multiple features in a automated queue
|
||||
- Custom 'Pipelines' to run multiple features in an automated queue
|
||||
- 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/Advanced%20Configuration/Single%20Sign-On%20Configuration) for documentation)
|
||||
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
|
||||
|
||||
## PDF Features
|
||||
### 50+ PDF Operations
|
||||
|
||||
### Page Operations
|
||||
#### Organise
|
||||
- **Merge**: Combine multiple PDFs into one
|
||||
- **Split**: Divide PDFs into multiple files
|
||||
- **Extract page(s)**: Extract specific pages from PDF
|
||||
- **Remove**: Delete pages from PDF
|
||||
- **Crop PDF**: Adjust PDF page boundaries
|
||||
- **Rotate**: Rotate pages in 90-degree increments
|
||||
- **Adjust page size/scale**: Resize page contents
|
||||
- **Multi-Page Layout**: Add multiple pages to PDF
|
||||
- **PDF to Single Large Page**: Convert to single continuous page
|
||||
- **Organize**: Rearrange PDF pages
|
||||
|
||||
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus, on-page edit features like annotating, drawing, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
|
||||
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
|
||||
- Merge multiple PDFs into a single resultant file
|
||||
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
|
||||
- Reorganize PDF pages into different orders
|
||||
- Rotate PDFs in 90-degree increments
|
||||
- Remove pages
|
||||
- Multi-page layout (format PDFs into a multi-paged page)
|
||||
- Scale page contents size by set percentage
|
||||
- Adjust contrast
|
||||
- Crop PDF
|
||||
- Auto-split PDF (with physically scanned page dividers)
|
||||
- Extract page(s)
|
||||
- Convert PDF to a single page
|
||||
- Overlay PDFs on top of each other
|
||||
- PDF to a single page
|
||||
- Split PDF by sections
|
||||
#### Convert to PDF
|
||||
- **Image to PDF**: Convert images to PDF format
|
||||
- **Convert file to PDF**: Convert various common file types to PDF
|
||||
- **HTML to PDF**: Transform HTML documents to PDF
|
||||
- **Markdown to PDF**: Convert Markdown files to PDF
|
||||
- **CBZ to PDF**: Convert comic book archives
|
||||
- **CBR to PDF**: Convert comic book rar archives
|
||||
- **Email to PDF**: Convert email files to PDF
|
||||
|
||||
### Conversion Operations
|
||||
|
||||
- Convert PDFs to and from images
|
||||
- Convert any common file to PDF (using LibreOffice)
|
||||
- Convert PDF to Word/PowerPoint/others (using LibreOffice)
|
||||
- Convert HTML to PDF
|
||||
- Convert PDF to XML
|
||||
- Convert PDF to CSV
|
||||
- URL to PDF
|
||||
- Markdown to PDF
|
||||
|
||||
### Security & Permissions
|
||||
|
||||
- Add and remove passwords
|
||||
- Change/set PDF permissions
|
||||
- Add watermark(s)
|
||||
- Certify/sign PDFs
|
||||
- Sanitize PDFs
|
||||
- Auto-redact text
|
||||
|
||||
### Other Operations
|
||||
|
||||
- Add/generate/write signatures
|
||||
- Split by Size or PDF
|
||||
- Repair PDFs
|
||||
- Detect and remove blank pages
|
||||
- Compare two PDFs and show differences in text
|
||||
- Add images to PDFs
|
||||
- Compress PDFs to decrease their filesize (using qpdf)
|
||||
- Extract images from PDF
|
||||
- Remove images from PDF
|
||||
- Extract images from scans
|
||||
- Remove annotations
|
||||
- Add page numbers
|
||||
- Auto-rename files by detecting PDF header text
|
||||
- OCR on PDF (using Tesseract OCR)
|
||||
- PDF/A conversion (using LibreOffice)
|
||||
- Edit metadata
|
||||
- Flatten PDFs
|
||||
- Get all information on a PDF to view or export as JSON
|
||||
- Show/detect embedded JavaScript
|
||||
#### Convert from PDF
|
||||
- **PDF to Word**: Convert to documet (docx, doc, odt) format
|
||||
- **PDF to Image**: Extract PDF pages as images
|
||||
- **PDF to RTF (Text)**: Convert to Rich Text Format
|
||||
- **PDF to Presentation**: Convert to presentation (pptx, ppt, odp) format
|
||||
- **PDF to CSV**: Extract tables to CSV
|
||||
- **PDF to XML**: Convert to XML format
|
||||
- **PDF to HTML**: Transform to HTML
|
||||
- **PDF to PDF/A**: Convert to archival (PDF/A-1b, PDF/A-2b) format
|
||||
- **PDF to Markdown**: Convert PDF to Markdown
|
||||
- **PDF to CBZ**: Convert to comic book archive
|
||||
- **PDF to CBR**: Convert to comic book rar archive
|
||||
|
||||
#### Sign & Security
|
||||
- **Sign**: Add digital signatures
|
||||
- **Remove Password**: Remove PDF security
|
||||
- **Add Watermark**: Apply watermarks
|
||||
- **Sign with Certificate**: Certificate-based signing
|
||||
- **Add Stamp to PDF**: Apply digital stamps
|
||||
- **Auto Redact**: Automatically redact content
|
||||
- **Change Permissions**: Modify access permissions
|
||||
- **Add Password**: Apply PDF encryption
|
||||
- **Manual Redaction**: Manual content redaction
|
||||
- **Remove Certificate Sign**: Remove digital signatures
|
||||
- **Sanitize**: Clean PDF of potential security issues
|
||||
- **Validate PDF Signature**: Verify digital signatures
|
||||
|
||||
#### View & Edit
|
||||
- **OCR / Cleanup scans**: Optical Character Recognition
|
||||
- **Add Image**: Insert images into PDF
|
||||
- **Extract Images**: Extract embedded images
|
||||
- **Change Metadata**: Edit PDF metadata
|
||||
- **Get ALL Info on PDF**: Comprehensive PDF analysis
|
||||
- **Advanced Colour options**: Colour manipulation (various options for colour inversion, CMYK conversion)
|
||||
- **Compare**: Compare PDF documents
|
||||
- **Add Page Numbers**: Insert page numbering
|
||||
- **Flatten**: Flatten PDF layers, and interactive elements
|
||||
- **Remove Annotations**: Delete comments and markups
|
||||
- **Remove Blank pages**: Delete empty pages
|
||||
- **Remove Image**: Delete embedded images
|
||||
- **View/Edit PDF**: Interactive PDF editing
|
||||
- **Unlock PDF Forms**: Enable form editing
|
||||
- **Add Attachments**: Attach files to PDF
|
||||
|
||||
#### Advanced
|
||||
- **Compress**: Reduce file size
|
||||
- **Pipeline**: Automated workflow processing (OCR images pipeline, prepare PDFs for emailing pipeline)
|
||||
- **Adjust Colours/Contrast**: Colour and contrast adjustment
|
||||
- **Auto Rename PDF File**: Automatic file renaming
|
||||
- **Auto Split Pages**: Automatic page splitting
|
||||
- **Detect/Split Scanned photos**: Photo detection and splitting
|
||||
- **Overlay PDFs**: Layer PDFs over each other
|
||||
- **Repair**: Fix corrupted PDFs
|
||||
- **Show JavaScript**: Display embedded JavaScript
|
||||
- **Auto Split by Size/Count**: Split by file size or page count
|
||||
- **Split PDF by Chapters**: Chapter-based splitting
|
||||
- **Split PDF by Sections**: Section-based splitting
|
||||
- **Scanner Effect**: Apply scanner-like effects
|
||||
- **Edit Table of Contents**: Modify PDF bookmarks and TOC
|
||||
|
||||
# 📖 Get Started
|
||||
|
||||
@@ -115,48 +130,48 @@ Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.sti
|
||||
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
|
||||
|
||||
|
||||
@@ -167,6 +167,10 @@
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "The W3C License"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "UnRar License"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,17 +29,18 @@ spotless {
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
|
||||
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.18.0'
|
||||
api 'org.apache.commons:commons-lang3:3.19.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 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||
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.4'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13"
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||
}
|
||||
|
||||
+9
-9
@@ -3,11 +3,12 @@ package stirling.software.common.annotations;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.http.MediaType;
|
||||
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.
|
||||
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
||||
*
|
||||
* <p>Behaviour notes:
|
||||
*
|
||||
@@ -16,7 +17,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
* 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
|
||||
* 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>
|
||||
@@ -37,7 +38,7 @@ public @interface AutoJobPostMapping {
|
||||
|
||||
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||
String[] consumes() default {"multipart/form-data"};
|
||||
String[] consumes() default {MediaType.MULTIPART_FORM_DATA_VALUE};
|
||||
|
||||
/**
|
||||
* Maximum execution time in milliseconds before the job is aborted. A negative value means "use
|
||||
@@ -48,8 +49,8 @@ public @interface AutoJobPostMapping {
|
||||
long timeout() default -1;
|
||||
|
||||
/**
|
||||
* Total number of attempts (initial + retries). Must be at least 1. Retries are executed
|
||||
* with exponential back-off.
|
||||
* 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}.
|
||||
*/
|
||||
@@ -71,9 +72,8 @@ public @interface AutoJobPostMapping {
|
||||
boolean queueable() default false;
|
||||
|
||||
/**
|
||||
* Credit cost for this endpoint in the API credit system. Also used as relative resource weight
|
||||
* (1-100) by the scheduler to prioritise / throttle jobs. Values below 1 are clamped to 1,
|
||||
* values above 100 to 100.
|
||||
* 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 1;
|
||||
int resourceWeight() default 50;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
|
||||
@@ -34,7 +33,6 @@ public class AutoJobAspect {
|
||||
|
||||
private final JobExecutorService jobExecutorService;
|
||||
private final HttpServletRequest request;
|
||||
private final FileOrUploadService fileOrUploadService;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
@@ -53,7 +51,8 @@ public class AutoJobAspect {
|
||||
boolean trackProgress = autoJobPostMapping.trackProgress();
|
||||
|
||||
log.debug(
|
||||
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={}, trackProgress={}",
|
||||
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={},"
|
||||
+ " trackProgress={}",
|
||||
async,
|
||||
timeout > 0 ? timeout : "default",
|
||||
retryCount,
|
||||
@@ -148,7 +147,8 @@ public class AutoJobAspect {
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
||||
"AutoJobAspect caught exception during job execution (attempt"
|
||||
+ " {}/{}): {}",
|
||||
currentAttempt,
|
||||
maxRetries,
|
||||
ex.getMessage(),
|
||||
|
||||
@@ -8,8 +8,10 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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;
|
||||
@@ -18,7 +20,9 @@ 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;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -48,7 +52,14 @@ public class AppConfig {
|
||||
@Value("${server.port:8080}")
|
||||
private String serverPort;
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Value("${v2}")
|
||||
public boolean v2Enabled;
|
||||
|
||||
@Bean
|
||||
public boolean v2Enabled() {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
@@ -56,7 +67,6 @@ public class AppConfig {
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
return templateEngine;
|
||||
}
|
||||
*/
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
@@ -119,7 +129,7 @@ public class AppConfig {
|
||||
public boolean rateLimit() {
|
||||
String rateLimit = System.getProperty("rateLimit");
|
||||
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
||||
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
|
||||
return Boolean.parseBoolean(rateLimit);
|
||||
}
|
||||
|
||||
@Bean(name = "RunningInDocker")
|
||||
@@ -139,8 +149,8 @@ public class AppConfig {
|
||||
if (!Files.exists(mountInfo)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
|
||||
try (Stream<String> lines = Files.lines(mountInfo)) {
|
||||
return lines.anyMatch(line -> line.contains(" /configs "));
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -261,9 +271,14 @@ public class AppConfig {
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
@Bean(name = "disablePixel")
|
||||
public boolean disablePixel() {
|
||||
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
|
||||
@Bean(name = "scarfEnabled")
|
||||
public boolean scarfEnabled() {
|
||||
return applicationProperties.getSystem().isScarfEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "posthogEnabled")
|
||||
public boolean posthogEnabled() {
|
||||
return applicationProperties.getSystem().isPosthogEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "machineType")
|
||||
|
||||
+21
-1
@@ -23,10 +23,30 @@ import stirling.software.common.util.YamlHelper;
|
||||
@Slf4j
|
||||
public class ConfigInitializer {
|
||||
|
||||
private static final int MIN_SETTINGS_FILE_LINES = 31;
|
||||
|
||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||
// 1) If settings file doesn't exist, create from template
|
||||
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
if (Files.notExists(destPath)) {
|
||||
|
||||
boolean settingsFileExists = Files.exists(destPath);
|
||||
|
||||
long lineCount = settingsFileExists ? Files.readAllLines(destPath).size() : 0;
|
||||
|
||||
log.info("Current settings file line count: {}", lineCount);
|
||||
|
||||
if (!settingsFileExists || lineCount < MIN_SETTINGS_FILE_LINES) {
|
||||
if (settingsFileExists) {
|
||||
// move settings.yml to settings.yml.{timestamp}.bak
|
||||
Path backupPath =
|
||||
Paths.get(
|
||||
InstallationPathConfig.getSettingsPath()
|
||||
+ "."
|
||||
+ System.currentTimeMillis()
|
||||
+ ".bak");
|
||||
Files.move(destPath, backupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("Moved existing settings file to backup: {}", backupPath);
|
||||
}
|
||||
Files.createDirectories(destPath.getParent());
|
||||
try (InputStream in =
|
||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
|
||||
+13
-3
@@ -1,8 +1,19 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.InputStreamTemplateResource;
|
||||
|
||||
@Slf4j
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
@@ -46,4 +57,3 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
+19
-1
@@ -14,12 +14,17 @@ public class InstallationPathConfig {
|
||||
private static final String CONFIG_PATH;
|
||||
private static final String CUSTOM_FILES_PATH;
|
||||
private static final String CLIENT_WEBUI_PATH;
|
||||
private static final String SCRIPTS_PATH;
|
||||
private static final String PIPELINE_PATH;
|
||||
|
||||
// Config paths
|
||||
private static final String SETTINGS_PATH;
|
||||
private static final String CUSTOM_SETTINGS_PATH;
|
||||
private static final String SCRIPTS_PATH;
|
||||
private static final String BACKUP_PATH;
|
||||
|
||||
// Backup paths
|
||||
private static final String BACKUP_DB_PATH;
|
||||
private static final String BACKUP_PRIVATE_KEY_PATH;
|
||||
|
||||
// Custom file paths
|
||||
private static final String STATIC_PATH;
|
||||
@@ -40,6 +45,11 @@ public class InstallationPathConfig {
|
||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||
SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator;
|
||||
BACKUP_PATH = CONFIG_PATH + "backup" + File.separator;
|
||||
|
||||
// Initialize backup paths
|
||||
BACKUP_DB_PATH = BACKUP_PATH + "db" + File.separator;
|
||||
BACKUP_PRIVATE_KEY_PATH = BACKUP_PATH + "keys" + File.separator;
|
||||
|
||||
// Initialize custom file paths
|
||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||
@@ -120,4 +130,12 @@ public class InstallationPathConfig {
|
||||
public static String getSignaturesPath() {
|
||||
return SIGNATURES_PATH;
|
||||
}
|
||||
|
||||
public static String getPrivateKeyPath() {
|
||||
return BACKUP_PRIVATE_KEY_PATH;
|
||||
}
|
||||
|
||||
public static String getBackupPath() {
|
||||
return BACKUP_DB_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
+46
-15
@@ -41,6 +41,7 @@ import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
import stirling.software.common.model.oauth2.Provider;
|
||||
import stirling.software.common.service.SsrfProtectionService.SsrfProtectionLevel;
|
||||
import stirling.software.common.util.ValidationUtils;
|
||||
|
||||
@Data
|
||||
@@ -72,11 +73,11 @@ public class ApplicationProperties {
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
throws IOException {
|
||||
String configPath = InstallationPathConfig.getSettingsPath();
|
||||
log.debug("Attempting to load settings from: " + configPath);
|
||||
log.debug("Attempting to load settings from: {}", configPath);
|
||||
|
||||
File file = new File(configPath);
|
||||
if (!file.exists()) {
|
||||
log.error("Warning: Settings file does not exist at: " + configPath);
|
||||
log.error("Warning: Settings file does not exist at: {}", configPath);
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(configPath);
|
||||
@@ -89,7 +90,7 @@ public class ApplicationProperties {
|
||||
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
|
||||
log.debug("Loaded properties: " + propertySource.getSource());
|
||||
log.debug("Loaded properties: {}", propertySource.getSource());
|
||||
|
||||
return propertySource;
|
||||
}
|
||||
@@ -119,6 +120,7 @@ public class ApplicationProperties {
|
||||
private long loginResetTimeMinutes;
|
||||
private String loginMethod = "all";
|
||||
private String customGlobalAPIKey;
|
||||
private Jwt jwt = new Jwt();
|
||||
|
||||
public Boolean isAltLogin() {
|
||||
return saml2.getEnabled() || oauth2.getEnabled();
|
||||
@@ -298,6 +300,14 @@ public class ApplicationProperties {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Jwt {
|
||||
private boolean enabled = true;
|
||||
private boolean keyCleanup = true;
|
||||
private int keyRetentionDays = 7;
|
||||
private Boolean secureCookie;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -310,6 +320,8 @@ public class ApplicationProperties {
|
||||
private String tessdataDir;
|
||||
private Boolean enableAlphaFunctionality;
|
||||
private Boolean enableAnalytics;
|
||||
private Boolean enablePosthog;
|
||||
private Boolean enableScarf;
|
||||
private Datasource datasource;
|
||||
private Boolean disableSanitize;
|
||||
private int maxDPI;
|
||||
@@ -318,10 +330,28 @@ public class ApplicationProperties {
|
||||
private CustomPaths customPaths = new CustomPaths();
|
||||
private String fileUploadLimit;
|
||||
private TempFileManagement tempFileManagement = new TempFileManagement();
|
||||
private DatabaseBackup databaseBackup = new DatabaseBackup();
|
||||
|
||||
public boolean isAnalyticsEnabled() {
|
||||
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
|
||||
}
|
||||
|
||||
public boolean isPosthogEnabled() {
|
||||
// Treat null as enabled when analytics is enabled
|
||||
return this.isAnalyticsEnabled()
|
||||
&& (this.getEnablePosthog() == null || this.getEnablePosthog());
|
||||
}
|
||||
|
||||
public boolean isScarfEnabled() {
|
||||
// Treat null as enabled when analytics is enabled
|
||||
return this.isAnalyticsEnabled()
|
||||
&& (this.getEnableScarf() == null || this.getEnableScarf());
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DatabaseBackup {
|
||||
private String cron = "0 0 0 * * ?"; // daily at midnight
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -360,16 +390,19 @@ public class ApplicationProperties {
|
||||
|
||||
@JsonIgnore
|
||||
public String getBaseTmpDir() {
|
||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
||||
? baseTmpDir
|
||||
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
|
||||
if (baseTmpDir != null && !baseTmpDir.isEmpty()) {
|
||||
return baseTmpDir;
|
||||
}
|
||||
String tmp = java.lang.System.getProperty("java.io.tmpdir");
|
||||
return new File(tmp, "stirling-pdf").getPath();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getLibreofficeDir() {
|
||||
return libreofficeDir != null && !libreofficeDir.isEmpty()
|
||||
? libreofficeDir
|
||||
: getBaseTmpDir() + "/libreoffice";
|
||||
if (libreofficeDir != null && !libreofficeDir.isEmpty()) {
|
||||
return libreofficeDir;
|
||||
}
|
||||
return new File(getBaseTmpDir(), "libreoffice").getPath();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +413,7 @@ public class ApplicationProperties {
|
||||
@Data
|
||||
public static class UrlSecurity {
|
||||
private boolean enabled = true;
|
||||
private String level = "MEDIUM"; // MAX, MEDIUM, OFF
|
||||
private SsrfProtectionLevel level = SsrfProtectionLevel.MEDIUM; // MAX, MEDIUM, OFF
|
||||
private List<String> allowedDomains = new ArrayList<>();
|
||||
private List<String> blockedDomains = new ArrayList<>();
|
||||
private List<String> internalTlds =
|
||||
@@ -435,19 +468,17 @@ public class ApplicationProperties {
|
||||
private List<String> languages;
|
||||
|
||||
public String getAppName() {
|
||||
return appName != null && appName.trim().length() > 0 ? appName : null;
|
||||
return appName != null && !appName.trim().isEmpty() ? appName : null;
|
||||
}
|
||||
|
||||
public String getHomeDescription() {
|
||||
return homeDescription != null && homeDescription.trim().length() > 0
|
||||
return homeDescription != null && !homeDescription.trim().isEmpty()
|
||||
? homeDescription
|
||||
: null;
|
||||
}
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar != null && appNameNavbar.trim().length() > 0
|
||||
? appNameNavbar
|
||||
: null;
|
||||
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ import java.util.Locale;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FileInfo {
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
+12
-10
@@ -1,17 +1,20 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public class InputStreamTemplateResource implements ITemplateResource {
|
||||
private InputStream inputStream;
|
||||
private String characterEncoding;
|
||||
|
||||
public InputStreamTemplateResource(InputStream inputStream, String characterEncoding) {
|
||||
this.inputStream = inputStream;
|
||||
this.characterEncoding = characterEncoding;
|
||||
}
|
||||
private final InputStream inputStream;
|
||||
private final String characterEncoding;
|
||||
|
||||
@Override
|
||||
public Reader reader() throws IOException {
|
||||
@@ -39,4 +42,3 @@ public class InputStreamTemplateResource implements ITemplateResource {
|
||||
return inputStream != null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PdfMetadata {
|
||||
private String author;
|
||||
private String producer;
|
||||
@@ -14,6 +18,6 @@ public class PdfMetadata {
|
||||
private String creator;
|
||||
private String subject;
|
||||
private String keywords;
|
||||
private Calendar creationDate;
|
||||
private Calendar modificationDate;
|
||||
private ZonedDateTime creationDate;
|
||||
private ZonedDateTime modificationDate;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package stirling.software.common.model.api;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -14,7 +15,7 @@ import lombok.NoArgsConstructor;
|
||||
public class PDFFile {
|
||||
@Schema(
|
||||
description = "The input PDF file",
|
||||
contentMediaType = "application/pdf",
|
||||
contentMediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
format = "binary")
|
||||
private MultipartFile fileInput;
|
||||
|
||||
|
||||
@@ -4,4 +4,5 @@ public enum ReplaceAndInvert {
|
||||
HIGH_CONTRAST_COLOR,
|
||||
CUSTOM_COLOR,
|
||||
FULL_INVERSION,
|
||||
COLOR_SPACE_CONVERSION,
|
||||
}
|
||||
|
||||
@@ -10,25 +10,21 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public enum Role {
|
||||
|
||||
// System-wide administrator - can manage all organizations
|
||||
SYSTEM_ADMIN(
|
||||
"ROLE_SYSTEM_ADMIN",
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
"adminUserSettings.systemAdmin"),
|
||||
|
||||
// Organization administrator - can manage their organization and all its teams
|
||||
ORG_ADMIN("ROLE_ORG_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.orgAdmin"),
|
||||
|
||||
// Team leader - can manage users in their specific team
|
||||
TEAM_LEAD("ROLE_TEAM_LEAD", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.teamLead"),
|
||||
|
||||
// Legacy admin role - equivalent to SYSTEM_ADMIN for backward compatibility
|
||||
// Unlimited access
|
||||
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.admin"),
|
||||
|
||||
// Regular user with unlimited access within their team/org
|
||||
// Unlimited access
|
||||
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
|
||||
|
||||
// 40 API calls Per Day, 40 web calls
|
||||
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40, "adminUserSettings.apiUser"),
|
||||
|
||||
// 20 API calls Per Day, 20 web calls
|
||||
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20, "adminUserSettings.extraApiUser"),
|
||||
|
||||
// 0 API calls per day and 20 web calls
|
||||
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20, "adminUserSettings.webOnlyUser"),
|
||||
|
||||
INTERNAL_API_USER(
|
||||
"STIRLING-PDF-BACKEND-API-USER",
|
||||
Integer.MAX_VALUE,
|
||||
@@ -67,35 +63,4 @@ public enum Role {
|
||||
}
|
||||
throw new IllegalArgumentException("No Role defined for id: " + roleId);
|
||||
}
|
||||
|
||||
/** Checks if this role can manage users across all organizations */
|
||||
public boolean isSystemAdmin() {
|
||||
return this == SYSTEM_ADMIN || this == ADMIN; // ADMIN for backward compatibility
|
||||
}
|
||||
|
||||
/** Checks if this role can manage an organization and all its teams */
|
||||
public boolean isOrgAdmin() {
|
||||
return isSystemAdmin() || this == ORG_ADMIN;
|
||||
}
|
||||
|
||||
/** Checks if this role can manage a specific team */
|
||||
public boolean isTeamLead() {
|
||||
return isOrgAdmin() || this == TEAM_LEAD;
|
||||
}
|
||||
|
||||
/** Gets the hierarchy level of this role (higher number = more permissions) */
|
||||
public int getHierarchyLevel() {
|
||||
return switch (this) {
|
||||
case SYSTEM_ADMIN, ADMIN -> 4;
|
||||
case ORG_ADMIN -> 3;
|
||||
case TEAM_LEAD -> 2;
|
||||
case USER -> 1;
|
||||
default -> 0; // Limited users
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks if this role has higher or equal authority than another role */
|
||||
public boolean hasAuthorityOver(Role otherRole) {
|
||||
return this.getHierarchyLevel() >= otherRole.getHierarchyLevel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.job.JobResponse;
|
||||
import stirling.software.common.util.ExecutorFactory;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
|
||||
/** Service for executing jobs asynchronously or synchronously */
|
||||
@Service
|
||||
@@ -227,7 +228,8 @@ public class JobExecutorService {
|
||||
if (result instanceof byte[]) {
|
||||
// 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");
|
||||
taskManager.setFileResult(
|
||||
jobId, fileId, "result.pdf", MediaType.APPLICATION_PDF_VALUE);
|
||||
log.debug("Stored byte[] result with fileId: {}", fileId);
|
||||
|
||||
// Let the byte array get collected naturally in the next GC cycle
|
||||
@@ -239,7 +241,7 @@ public class JobExecutorService {
|
||||
if (body instanceof byte[]) {
|
||||
// Extract filename from content-disposition header if available
|
||||
String filename = "result.pdf";
|
||||
String contentType = "application/pdf";
|
||||
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||
|
||||
if (response.getHeaders().getContentDisposition() != null) {
|
||||
String disposition =
|
||||
@@ -252,8 +254,10 @@ public class JobExecutorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (response.getHeaders().getContentType() != null) {
|
||||
contentType = response.getHeaders().getContentType().toString();
|
||||
MediaType mediaType = response.getHeaders().getContentType();
|
||||
|
||||
if (mediaType != null) {
|
||||
contentType = mediaType.toString();
|
||||
}
|
||||
|
||||
// Store byte array directly to disk
|
||||
@@ -274,7 +278,7 @@ public class JobExecutorService {
|
||||
if (fileId != null && !fileId.isEmpty()) {
|
||||
// Try to get filename and content type
|
||||
String filename = "result.pdf";
|
||||
String contentType = "application/pdf";
|
||||
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method getOriginalFileName =
|
||||
@@ -315,8 +319,7 @@ public class JobExecutorService {
|
||||
// Store generic result
|
||||
taskManager.setResult(jobId, body);
|
||||
}
|
||||
} else if (result instanceof MultipartFile) {
|
||||
MultipartFile file = (MultipartFile) result;
|
||||
} else if (result instanceof MultipartFile file) {
|
||||
String fileId = fileStorage.storeFile(file);
|
||||
taskManager.setFileResult(
|
||||
jobId, fileId, file.getOriginalFilename(), file.getContentType());
|
||||
@@ -333,7 +336,7 @@ public class JobExecutorService {
|
||||
if (fileId != null && !fileId.isEmpty()) {
|
||||
// Try to get filename and content type
|
||||
String filename = "result.pdf";
|
||||
String contentType = "application/pdf";
|
||||
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method getOriginalFileName =
|
||||
@@ -396,9 +399,8 @@ public class JobExecutorService {
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
"form-data; name=\"attachment\"; filename=\"result.pdf\"")
|
||||
.body(result);
|
||||
} else if (result instanceof MultipartFile) {
|
||||
} else if (result instanceof MultipartFile file) {
|
||||
// Return MultipartFile content
|
||||
MultipartFile file = (MultipartFile) result;
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(file.getContentType()))
|
||||
.header(
|
||||
@@ -425,8 +427,16 @@ public class JobExecutorService {
|
||||
}
|
||||
|
||||
try {
|
||||
String value = timeout.replaceAll("[^\\d.]", "");
|
||||
String unit = timeout.replaceAll("[\\d.]", "");
|
||||
String value =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getNonDigitDotPattern()
|
||||
.matcher(timeout)
|
||||
.replaceAll("");
|
||||
String unit =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getDigitDotPattern()
|
||||
.matcher(timeout)
|
||||
.replaceAll("");
|
||||
|
||||
double numericValue = Double.parseDouble(value);
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Calendar;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -29,17 +33,19 @@ public class PdfMetadataService {
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
}
|
||||
|
||||
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
||||
return PdfMetadata.builder()
|
||||
.author(pdf.getDocumentInformation().getAuthor())
|
||||
.producer(pdf.getDocumentInformation().getProducer())
|
||||
.title(pdf.getDocumentInformation().getTitle())
|
||||
.creator(pdf.getDocumentInformation().getCreator())
|
||||
.subject(pdf.getDocumentInformation().getSubject())
|
||||
.keywords(pdf.getDocumentInformation().getKeywords())
|
||||
.creationDate(pdf.getDocumentInformation().getCreationDate())
|
||||
.modificationDate(pdf.getDocumentInformation().getModificationDate())
|
||||
.build();
|
||||
/**
|
||||
* Converts ZonedDateTime to Calendar for PDFBox compatibility.
|
||||
*
|
||||
* @param zonedDateTime the ZonedDateTime to convert
|
||||
* @return Calendar instance or null if input is null
|
||||
*/
|
||||
public static Calendar toCalendar(ZonedDateTime zonedDateTime) {
|
||||
if (zonedDateTime == null) {
|
||||
return null;
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(zonedDateTime.toInstant().toEpochMilli());
|
||||
return calendar;
|
||||
}
|
||||
|
||||
public void setDefaultMetadata(PDDocument pdf) {
|
||||
@@ -58,6 +64,52 @@ public class PdfMetadataService {
|
||||
setCommonMetadata(pdf, pdfMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a date string and converts it to Calendar for PDFBox compatibility.
|
||||
*
|
||||
* @param dateString the date string in "yyyy/MM/dd HH:mm:ss" format
|
||||
* @return Calendar instance or null if parsing fails or input is empty
|
||||
*/
|
||||
public static Calendar parseToCalendar(String dateString) {
|
||||
if (dateString == null || dateString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
|
||||
ZonedDateTime zonedDateTime =
|
||||
LocalDateTime.parse(dateString, formatter).atZone(ZoneId.systemDefault());
|
||||
return toCalendar(zonedDateTime);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
||||
Calendar creationCal = pdf.getDocumentInformation().getCreationDate();
|
||||
Calendar modificationCal = pdf.getDocumentInformation().getModificationDate();
|
||||
|
||||
ZonedDateTime creationDate =
|
||||
creationCal != null
|
||||
? ZonedDateTime.ofInstant(creationCal.toInstant(), ZoneId.systemDefault())
|
||||
: null;
|
||||
ZonedDateTime modificationDate =
|
||||
modificationCal != null
|
||||
? ZonedDateTime.ofInstant(
|
||||
modificationCal.toInstant(), ZoneId.systemDefault())
|
||||
: null;
|
||||
|
||||
return PdfMetadata.builder()
|
||||
.author(pdf.getDocumentInformation().getAuthor())
|
||||
.producer(pdf.getDocumentInformation().getProducer())
|
||||
.title(pdf.getDocumentInformation().getTitle())
|
||||
.creator(pdf.getDocumentInformation().getCreator())
|
||||
.subject(pdf.getDocumentInformation().getSubject())
|
||||
.keywords(pdf.getDocumentInformation().getKeywords())
|
||||
.creationDate(creationDate)
|
||||
.modificationDate(modificationDate)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||
|
||||
String creator = stirlingPDFLabel;
|
||||
@@ -79,7 +131,13 @@ public class PdfMetadataService {
|
||||
}
|
||||
|
||||
pdf.getDocumentInformation().setCreator(creator);
|
||||
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
||||
|
||||
// Use existing creation date if available, otherwise create new one
|
||||
Calendar creationCal =
|
||||
pdfMetadata.getCreationDate() != null
|
||||
? toCalendar(pdfMetadata.getCreationDate())
|
||||
: Calendar.getInstance();
|
||||
pdf.getDocumentInformation().setCreationDate(creationCal);
|
||||
}
|
||||
|
||||
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||
@@ -88,7 +146,13 @@ public class PdfMetadataService {
|
||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
||||
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||
|
||||
// Convert ZonedDateTime to Calendar for PDFBox compatibility
|
||||
Calendar modificationCal =
|
||||
pdfMetadata.getModificationDate() != null
|
||||
? toCalendar(pdfMetadata.getModificationDate())
|
||||
: Calendar.getInstance();
|
||||
pdf.getDocumentInformation().setModificationDate(modificationCal);
|
||||
|
||||
String author = pdfMetadata.getAuthor();
|
||||
if (applicationProperties
|
||||
|
||||
@@ -56,7 +56,7 @@ public class PostHogService {
|
||||
}
|
||||
|
||||
private void captureSystemInfo() {
|
||||
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
|
||||
if (!applicationProperties.getSystem().isPosthogEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -67,7 +67,7 @@ public class PostHogService {
|
||||
}
|
||||
|
||||
public void captureEvent(String eventName, Map<String, Object> properties) {
|
||||
if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
|
||||
if (!applicationProperties.getSystem().isPosthogEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+83
-22
@@ -1,5 +1,7 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -11,6 +13,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -20,8 +23,9 @@ public class SsrfProtectionService {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private static final Pattern DATA_URL_PATTERN =
|
||||
Pattern.compile("^data:.*", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FRAGMENT_PATTERN = Pattern.compile("^#.*");
|
||||
RegexPatternUtils.getInstance().getPattern("^data:.*", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FRAGMENT_PATTERN =
|
||||
RegexPatternUtils.getInstance().getPattern("^#.*");
|
||||
|
||||
public enum SsrfProtectionLevel {
|
||||
OFF, // No SSRF protection - allows all URLs
|
||||
@@ -51,21 +55,17 @@ public class SsrfProtectionService {
|
||||
|
||||
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
||||
|
||||
switch (level) {
|
||||
case OFF:
|
||||
return true;
|
||||
case MAX:
|
||||
return isMaxSecurityAllowed(trimmedUrl, config);
|
||||
case MEDIUM:
|
||||
return isMediumSecurityAllowed(trimmedUrl, config);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return switch (level) {
|
||||
case OFF -> true;
|
||||
case MAX -> isMaxSecurityAllowed(trimmedUrl, config);
|
||||
case MEDIUM -> isMediumSecurityAllowed(trimmedUrl, config);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private SsrfProtectionLevel parseProtectionLevel(String level) {
|
||||
private SsrfProtectionLevel parseProtectionLevel(SsrfProtectionLevel level) {
|
||||
try {
|
||||
return SsrfProtectionLevel.valueOf(level.toUpperCase());
|
||||
return SsrfProtectionLevel.valueOf(level.name());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Invalid SSRF protection level '{}', defaulting to MEDIUM", level);
|
||||
return SsrfProtectionLevel.MEDIUM;
|
||||
@@ -172,15 +172,62 @@ public class SsrfProtectionService {
|
||||
}
|
||||
|
||||
private boolean isPrivateAddress(InetAddress address) {
|
||||
return address.isSiteLocalAddress()
|
||||
|| address.isAnyLocalAddress()
|
||||
|| isPrivateIPv4Range(address.getHostAddress());
|
||||
if (address.isAnyLocalAddress() || address.isLoopbackAddress()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (address instanceof Inet4Address) {
|
||||
return isPrivateIPv4Range(address.getHostAddress());
|
||||
}
|
||||
|
||||
if (address instanceof Inet6Address addr6) {
|
||||
if (addr6.isLinkLocalAddress() || addr6.isSiteLocalAddress()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] bytes = addr6.getAddress();
|
||||
if (isIpv4MappedAddress(bytes)) {
|
||||
String ipv4 =
|
||||
(bytes[12] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[13] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[14] & 0xff)
|
||||
+ "."
|
||||
+ (bytes[15] & 0xff);
|
||||
return isPrivateIPv4Range(ipv4);
|
||||
}
|
||||
|
||||
int firstByte = bytes[0] & 0xff;
|
||||
// Check for IPv6 unique local addresses (fc00::/7)
|
||||
if ((firstByte & 0xfe) == 0xfc) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isIpv4MappedAddress(byte[] addr) {
|
||||
if (addr.length != 16) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (addr[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// For IPv4-mapped IPv6 addresses, bytes 10 and 11 must be 0xff (i.e., address is
|
||||
// ::ffff:w.x.y.z)
|
||||
return addr[10] == (byte) 0xff && addr[11] == (byte) 0xff;
|
||||
}
|
||||
|
||||
private boolean isPrivateIPv4Range(String ip) {
|
||||
// Includes RFC1918, loopback, link-local, and unspecified addresses
|
||||
return ip.startsWith("10.")
|
||||
|| ip.startsWith("192.168.")
|
||||
|| (ip.startsWith("172.") && isInRange172(ip))
|
||||
|| ip.startsWith("169.254.")
|
||||
|| ip.startsWith("127.")
|
||||
|| "0.0.0.0".equals(ip);
|
||||
}
|
||||
@@ -192,17 +239,31 @@ public class SsrfProtectionService {
|
||||
int secondOctet = Integer.parseInt(parts[1]);
|
||||
return secondOctet >= 16 && secondOctet <= 31;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCloudMetadataAddress(String ip) {
|
||||
String normalizedIp = normalizeIpv4MappedAddress(ip);
|
||||
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||
return ip.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||
|| ip.startsWith("fd00:ec2::254") // AWS IPv6
|
||||
|| ip.startsWith("169.254.169.253") // Oracle Cloud
|
||||
|| ip.startsWith("169.254.169.250"); // IBM Cloud
|
||||
return normalizedIp.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||
|| normalizedIp.startsWith("fd00:ec2::254") // AWS IPv6
|
||||
|| normalizedIp.startsWith("169.254.169.253") // Oracle Cloud
|
||||
|| normalizedIp.startsWith("169.254.169.250"); // IBM Cloud
|
||||
}
|
||||
|
||||
private String normalizeIpv4MappedAddress(String ip) {
|
||||
if (ip == null) {
|
||||
return "";
|
||||
}
|
||||
if (ip.startsWith("::ffff:")) {
|
||||
return ip.substring(7);
|
||||
}
|
||||
int lastColon = ip.lastIndexOf(':');
|
||||
if (lastColon >= 0 && ip.indexOf('.') > lastColon) {
|
||||
return ip.substring(lastColon + 1);
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
@@ -61,8 +62,14 @@ public class TempFileCleanupService {
|
||||
// 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+")
|
||||
RegexPatternUtils.getInstance()
|
||||
.getSystemTempFile1Pattern()
|
||||
.matcher(fileName)
|
||||
.matches()
|
||||
|| RegexPatternUtils.getInstance()
|
||||
.getSystemTempFile2Pattern()
|
||||
.matcher(fileName)
|
||||
.matches()
|
||||
|| (fileName.startsWith("tmp") && !fileName.contains("jetty"))
|
||||
|| fileName.startsWith("OSL_PIPE_")
|
||||
|| (fileName.endsWith(".tmp") && !fileName.contains("jetty"));
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.github.junrar.Archive;
|
||||
import com.github.junrar.exception.CorruptHeaderException;
|
||||
import com.github.junrar.exception.RarException;
|
||||
import com.github.junrar.rarfile.FileHeader;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CbrUtils {
|
||||
|
||||
public byte[] convertCbrToPdf(
|
||||
MultipartFile cbrFile,
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
TempFileManager tempFileManager)
|
||||
throws IOException {
|
||||
return convertCbrToPdf(cbrFile, pdfDocumentFactory, tempFileManager, false);
|
||||
}
|
||||
|
||||
public byte[] convertCbrToPdf(
|
||||
MultipartFile cbrFile,
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
TempFileManager tempFileManager,
|
||||
boolean optimizeForEbook)
|
||||
throws IOException {
|
||||
|
||||
validateCbrFile(cbrFile);
|
||||
|
||||
try (TempFile tempFile = new TempFile(tempFileManager, ".cbr")) {
|
||||
cbrFile.transferTo(tempFile.getFile());
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.createNewDocument()) {
|
||||
|
||||
Archive archive;
|
||||
try {
|
||||
archive = new Archive(tempFile.getFile());
|
||||
} catch (CorruptHeaderException e) {
|
||||
log.warn(
|
||||
"Failed to open CBR/RAR archive due to corrupt header: {}",
|
||||
e.getMessage());
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat",
|
||||
"Invalid or corrupted CBR/RAR archive. "
|
||||
+ "The file may be corrupted, use an unsupported RAR format (RAR5+), "
|
||||
+ "or may not be a valid RAR archive. "
|
||||
+ "Please ensure the file is a valid RAR archive.");
|
||||
} catch (RarException e) {
|
||||
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
|
||||
String errorMessage;
|
||||
String exMessage = e.getMessage() != null ? e.getMessage() : "";
|
||||
|
||||
if (exMessage.contains("encrypted")) {
|
||||
errorMessage = "Encrypted CBR/RAR archives are not supported.";
|
||||
} else if (exMessage.isEmpty()) {
|
||||
errorMessage =
|
||||
"Invalid CBR/RAR archive. "
|
||||
+ "The file may be encrypted, corrupted, or use an unsupported format.";
|
||||
} else {
|
||||
errorMessage =
|
||||
"Invalid CBR/RAR archive: "
|
||||
+ exMessage
|
||||
+ ". The file may be encrypted, corrupted, or use an unsupported format.";
|
||||
}
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat", errorMessage);
|
||||
} catch (IOException e) {
|
||||
log.warn("IO error reading CBR/RAR archive: {}", e.getMessage());
|
||||
throw ExceptionUtils.createFileProcessingException("CBR extraction", e);
|
||||
}
|
||||
|
||||
List<ImageEntryData> imageEntries = new ArrayList<>();
|
||||
|
||||
try {
|
||||
for (FileHeader fileHeader : archive) {
|
||||
if (!fileHeader.isDirectory() && isImageFile(fileHeader.getFileName())) {
|
||||
try (InputStream is = archive.getInputStream(fileHeader)) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
is.transferTo(baos);
|
||||
imageEntries.add(
|
||||
new ImageEntryData(
|
||||
fileHeader.getFileName(), baos.toByteArray()));
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Error reading image {}: {}",
|
||||
fileHeader.getFileName(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
archive.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("Error closing CBR/RAR archive: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
imageEntries.sort(
|
||||
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
||||
|
||||
if (imageEntries.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.fileProcessing",
|
||||
"No valid images found in the CBR file. The archive may be empty or contain no supported image formats.");
|
||||
}
|
||||
|
||||
for (ImageEntryData imageEntry : imageEntries) {
|
||||
try {
|
||||
PDImageXObject pdImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
document, imageEntry.data(), imageEntry.name());
|
||||
PDPage page =
|
||||
new PDPage(
|
||||
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
|
||||
document.addPage(page);
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(document, page)) {
|
||||
contentStream.drawImage(pdImage, 0, 0);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn(
|
||||
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.fileProcessing",
|
||||
"No images could be processed from the CBR file. All images may be corrupted or in unsupported formats.");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
byte[] pdfBytes = baos.toByteArray();
|
||||
|
||||
// Apply Ghostscript optimization if requested
|
||||
if (optimizeForEbook) {
|
||||
try {
|
||||
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||
} catch (IOException e) {
|
||||
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||
return pdfBytes;
|
||||
}
|
||||
}
|
||||
|
||||
return pdfBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCbrFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a CBR or RAR archive");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCbrFile(MultipartFile file) {
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
return "cbr".equals(extension) || "rar".equals(extension);
|
||||
}
|
||||
|
||||
private boolean isImageFile(String filename) {
|
||||
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
|
||||
}
|
||||
|
||||
private record ImageEntryData(String name, byte[] data) {}
|
||||
|
||||
private class NaturalOrderComparator implements Comparator<String> {
|
||||
private static String getChunk(String s, int length, int marker) {
|
||||
StringBuilder chunk = new StringBuilder();
|
||||
char c = s.charAt(marker);
|
||||
chunk.append(c);
|
||||
marker++;
|
||||
|
||||
if (isDigit(c)) {
|
||||
while (marker < length && isDigit(s.charAt(marker))) {
|
||||
chunk.append(s.charAt(marker));
|
||||
marker++;
|
||||
}
|
||||
} else {
|
||||
while (marker < length && !isDigit(s.charAt(marker))) {
|
||||
chunk.append(s.charAt(marker));
|
||||
marker++;
|
||||
}
|
||||
}
|
||||
return chunk.toString();
|
||||
}
|
||||
|
||||
private static boolean isDigit(char ch) {
|
||||
return ch >= '0' && ch <= '9';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
int len1 = s1.length();
|
||||
int len2 = s2.length();
|
||||
int marker1 = 0, marker2 = 0;
|
||||
|
||||
while (marker1 < len1 && marker2 < len2) {
|
||||
String chunk1 = getChunk(s1, len1, marker1);
|
||||
marker1 += chunk1.length();
|
||||
|
||||
String chunk2 = getChunk(s2, len2, marker2);
|
||||
marker2 += chunk2.length();
|
||||
|
||||
int result;
|
||||
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
|
||||
int thisNumericValue = Integer.parseInt(chunk1);
|
||||
int thatNumericValue = Integer.parseInt(chunk2);
|
||||
result = Integer.compare(thisNumericValue, thatNumericValue);
|
||||
} else {
|
||||
result = chunk1.compareTo(chunk2);
|
||||
}
|
||||
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return Integer.compare(len1, len2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CbzUtils {
|
||||
|
||||
public byte[] convertCbzToPdf(
|
||||
MultipartFile cbzFile,
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
TempFileManager tempFileManager)
|
||||
throws IOException {
|
||||
return convertCbzToPdf(cbzFile, pdfDocumentFactory, tempFileManager, false);
|
||||
}
|
||||
|
||||
public byte[] convertCbzToPdf(
|
||||
MultipartFile cbzFile,
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
TempFileManager tempFileManager,
|
||||
boolean optimizeForEbook)
|
||||
throws IOException {
|
||||
|
||||
validateCbzFile(cbzFile);
|
||||
|
||||
try (TempFile tempFile = new TempFile(tempFileManager, ".cbz")) {
|
||||
cbzFile.transferTo(tempFile.getFile());
|
||||
|
||||
// Early ZIP validity check using ZipInputStream (fail fast on non-zip content)
|
||||
try (BufferedInputStream bis =
|
||||
new BufferedInputStream(
|
||||
new java.io.FileInputStream(tempFile.getFile()));
|
||||
ZipInputStream zis = new ZipInputStream(bis)) {
|
||||
if (zis.getNextEntry() == null) {
|
||||
throw new IllegalArgumentException("Archive is empty or invalid ZIP");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e);
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.createNewDocument();
|
||||
ZipFile zipFile = new ZipFile(tempFile.getFile())) {
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
List<ImageEntryData> imageEntries = new ArrayList<>();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (!entry.isDirectory() && isImageFile(entry.getName())) {
|
||||
try (InputStream is = zipFile.getInputStream(entry)) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
is.transferTo(baos);
|
||||
imageEntries.add(
|
||||
new ImageEntryData(entry.getName(), baos.toByteArray()));
|
||||
} catch (IOException e) {
|
||||
log.warn("Error reading image {}: {}", entry.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageEntries.sort(
|
||||
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
||||
|
||||
if (imageEntries.isEmpty()) {
|
||||
throw new IllegalArgumentException("No valid images found in the CBZ file");
|
||||
}
|
||||
|
||||
for (ImageEntryData imageEntry : imageEntries) {
|
||||
try {
|
||||
PDImageXObject pdImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
document, imageEntry.data(), imageEntry.name());
|
||||
PDPage page =
|
||||
new PDPage(
|
||||
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
|
||||
document.addPage(page);
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(document, page)) {
|
||||
contentStream.drawImage(pdImage, 0, 0);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn(
|
||||
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"No images could be processed from the CBZ file");
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
byte[] pdfBytes = baos.toByteArray();
|
||||
|
||||
// Apply Ghostscript optimization if requested
|
||||
if (optimizeForEbook) {
|
||||
try {
|
||||
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||
} catch (IOException e) {
|
||||
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||
return pdfBytes;
|
||||
}
|
||||
}
|
||||
|
||||
return pdfBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCbzFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCbzFile(MultipartFile file) {
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
return "cbz".equals(extension) || "zip".equals(extension);
|
||||
}
|
||||
|
||||
public static boolean isComicBookFile(MultipartFile file) {
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
return "cbz".equals(extension)
|
||||
|| "zip".equals(extension)
|
||||
|| "cbr".equals(extension)
|
||||
|| "rar".equals(extension);
|
||||
}
|
||||
|
||||
private boolean isImageFile(String filename) {
|
||||
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
|
||||
}
|
||||
|
||||
private record ImageEntryData(String name, byte[] data) {}
|
||||
|
||||
private class NaturalOrderComparator implements Comparator<String> {
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
int len1 = s1.length();
|
||||
int len2 = s2.length();
|
||||
int marker1 = 0, marker2 = 0;
|
||||
|
||||
while (marker1 < len1 && marker2 < len2) {
|
||||
String chunk1 = getChunk(s1, len1, marker1);
|
||||
marker1 += chunk1.length();
|
||||
|
||||
String chunk2 = getChunk(s2, len2, marker2);
|
||||
marker2 += chunk2.length();
|
||||
|
||||
int result;
|
||||
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
|
||||
int thisNumericValue = Integer.parseInt(chunk1);
|
||||
int thatNumericValue = Integer.parseInt(chunk2);
|
||||
result = Integer.compare(thisNumericValue, thatNumericValue);
|
||||
} else {
|
||||
result = chunk1.compareTo(chunk2);
|
||||
}
|
||||
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return Integer.compare(len1, len2);
|
||||
}
|
||||
|
||||
private static String getChunk(String s, int length, int marker) {
|
||||
StringBuilder chunk = new StringBuilder();
|
||||
char c = s.charAt(marker);
|
||||
chunk.append(c);
|
||||
marker++;
|
||||
|
||||
if (isDigit(c)) {
|
||||
while (marker < length && isDigit(s.charAt(marker))) {
|
||||
chunk.append(s.charAt(marker));
|
||||
marker++;
|
||||
}
|
||||
} else {
|
||||
while (marker < length && !isDigit(s.charAt(marker))) {
|
||||
chunk.append(s.charAt(marker));
|
||||
marker++;
|
||||
}
|
||||
}
|
||||
return chunk.toString();
|
||||
}
|
||||
|
||||
private static boolean isDigit(char ch) {
|
||||
return ch >= '0' && ch <= '9';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.zip.Adler32;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Checksum;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class ChecksumUtils {
|
||||
|
||||
/** Shared buffer size for streaming I/O. */
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
/** Mask to extract the lower 32 bits of a long value (unsigned int). */
|
||||
private static final long UNSIGNED_32_BIT_MASK = 0xFFFFFFFFL;
|
||||
|
||||
/**
|
||||
* Computes a checksum for the given file using the chosen algorithm and returns a lowercase hex
|
||||
* string.
|
||||
*
|
||||
* <p>For digest algorithms (e.g., SHA-256, SHA-1, MD5), this returns the digest as hex. For
|
||||
* 32-bit {@link Checksum} algorithms ("CRC32", "ADLER32"), this returns an 8-character
|
||||
* lowercase hex string of the unsigned 32-bit value.
|
||||
*
|
||||
* @param path file to read
|
||||
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||
* @return hex string of the checksum
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
public static String checksum(Path path, String algorithm) throws IOException {
|
||||
try (InputStream is = Files.newInputStream(path)) {
|
||||
return checksum(is, algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a checksum for the given stream using the chosen algorithm and returns a lowercase
|
||||
* hex string.
|
||||
*
|
||||
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||
*
|
||||
* @param is input stream (not closed by this method)
|
||||
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||
* @return hex string of the checksum
|
||||
* @throws IOException if reading from the stream fails
|
||||
*/
|
||||
public static String checksum(InputStream is, String algorithm) throws IOException {
|
||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32":
|
||||
return checksumChecksum(is, new CRC32());
|
||||
case "ADLER32":
|
||||
return checksumChecksum(is, new Adler32());
|
||||
default:
|
||||
return toHex(checksumBytes(is, algorithm));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a checksum for the given file using the chosen algorithm and returns a Base64
|
||||
* encoded string.
|
||||
*
|
||||
* <p>For digest algorithms this is the Base64 of the raw digest bytes. For 32-bit checksum
|
||||
* algorithms ("CRC32", "ADLER32"), this is the Base64 of the 4-byte big-endian unsigned value.
|
||||
*
|
||||
* @param path file to read
|
||||
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||
* @return Base64-encoded checksum bytes
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
public static String checksumBase64(Path path, String algorithm) throws IOException {
|
||||
try (InputStream is = Files.newInputStream(path)) {
|
||||
return checksumBase64(is, algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a checksum for the given stream using the chosen algorithm and returns a Base64
|
||||
* encoded string.
|
||||
*
|
||||
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||
*
|
||||
* @param is input stream (not closed by this method)
|
||||
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||
* @return Base64-encoded checksum bytes
|
||||
* @throws IOException if reading from the stream fails
|
||||
*/
|
||||
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
|
||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32":
|
||||
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
|
||||
case "ADLER32":
|
||||
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
|
||||
default:
|
||||
return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes multiple checksums for the given file in a single pass over the data.
|
||||
*
|
||||
* <p>Returns a map from algorithm name to lowercase hex string. Order of results follows the
|
||||
* order of the provided {@code algorithms}.
|
||||
*
|
||||
* @param path file to read
|
||||
* @param algorithms algorithm names (case-insensitive). Special: "CRC32", "ADLER32".
|
||||
* @return map of algorithm → hex string
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
public static Map<String, String> checksums(Path path, String... algorithms)
|
||||
throws IOException {
|
||||
try (InputStream is = Files.newInputStream(path)) {
|
||||
return checksums(is, algorithms);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes multiple checksums for the given stream in a single pass over the data.
|
||||
*
|
||||
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||
*
|
||||
* @param is input stream (not closed by this method)
|
||||
* @param algorithms algorithm names (case-insensitive). Special: "CRC32", "ADLER32".
|
||||
* @return map of algorithm → hex string
|
||||
* @throws IOException if reading from the stream fails
|
||||
*/
|
||||
public static Map<String, String> checksums(InputStream is, String... algorithms)
|
||||
throws IOException {
|
||||
// Use LinkedHashMap to preserve the order of requested algorithms in the result.
|
||||
Map<String, MessageDigest> digests = new LinkedHashMap<>();
|
||||
Map<String, Checksum> checksums = new LinkedHashMap<>();
|
||||
|
||||
for (String algorithm : algorithms) {
|
||||
String key = algorithm; // keep original key for output
|
||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32":
|
||||
checksums.put(key, new CRC32());
|
||||
break;
|
||||
case "ADLER32":
|
||||
checksums.put(key, new Adler32());
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
// For MessageDigest, pass the original name (case-insensitive per JCA)
|
||||
digests.put(key, MessageDigest.getInstance(algorithm));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Unsupported algorithm: " + algorithm, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) != -1) {
|
||||
for (MessageDigest digest : digests.values()) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
for (Checksum cs : checksums.values()) {
|
||||
cs.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> results = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, MessageDigest> entry : digests.entrySet()) {
|
||||
results.put(entry.getKey(), toHex(entry.getValue().digest()));
|
||||
}
|
||||
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
|
||||
// Keep value as long and mask to ensure unsigned hex formatting.
|
||||
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
|
||||
results.put(entry.getKey(), String.format("%08x", unsigned32));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the checksum of a file with an expected hex string (case-insensitive).
|
||||
*
|
||||
* @param path file to read
|
||||
* @param algorithm algorithm name (case-insensitive). Special: "CRC32", "ADLER32".
|
||||
* @param expected expected hex string (case-insensitive)
|
||||
* @return {@code true} if they match, otherwise {@code false}
|
||||
* @throws IOException if the file cannot be read
|
||||
*/
|
||||
public static boolean matches(Path path, String algorithm, String expected) throws IOException {
|
||||
try (InputStream is = Files.newInputStream(path)) {
|
||||
return matches(is, algorithm, expected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the checksum of a stream with an expected hex string (case-insensitive).
|
||||
*
|
||||
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||
*
|
||||
* @param is input stream (not closed by this method)
|
||||
* @param algorithm algorithm name (case-insensitive). Special: "CRC32", "ADLER32".
|
||||
* @param expected expected hex string (case-insensitive)
|
||||
* @return {@code true} if they match, otherwise {@code false}
|
||||
* @throws IOException if reading from the stream fails
|
||||
*/
|
||||
public static boolean matches(InputStream is, String algorithm, String expected)
|
||||
throws IOException {
|
||||
return checksum(is, algorithm).equalsIgnoreCase(expected);
|
||||
}
|
||||
|
||||
// ---------- Internal helpers ----------
|
||||
|
||||
/**
|
||||
* Computes a MessageDigest over a stream and returns the raw digest bytes.
|
||||
*
|
||||
* @param is input stream (not closed)
|
||||
* @param algorithm JCA MessageDigest algorithm (e.g., "SHA-256")
|
||||
* @return raw digest bytes
|
||||
* @throws IOException if reading fails
|
||||
* @throws IllegalStateException if the algorithm is unsupported
|
||||
*/
|
||||
private static byte[] checksumBytes(InputStream is, String algorithm) throws IOException {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance(algorithm);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
return digest.digest();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Keep the message explicit to aid debugging
|
||||
throw new IllegalStateException("Unsupported algorithm: " + algorithm, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a 32-bit {@link Checksum} over a stream and returns the lowercase 8-char hex of the
|
||||
* unsigned 32-bit value.
|
||||
*
|
||||
* @param is input stream (not closed)
|
||||
* @param checksum checksum implementation (CRC32, Adler32, etc.)
|
||||
* @return 8-character lowercase hex (big-endian representation)
|
||||
* @throws IOException if reading fails
|
||||
*/
|
||||
private static String checksumChecksum(InputStream is, Checksum checksum) throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) != -1) {
|
||||
checksum.update(buffer, 0, read);
|
||||
}
|
||||
// Keep as long and mask to ensure correct unsigned representation.
|
||||
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
|
||||
return String.format("%08x", unsigned32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a 32-bit {@link Checksum} over a stream and returns the raw 4-byte big-endian
|
||||
* representation of the unsigned 32-bit value.
|
||||
*
|
||||
* <p>Cast to int already truncates to the lower 32 bits; the sign is irrelevant because we
|
||||
* serialize the bit pattern directly into 4 bytes.
|
||||
*
|
||||
* @param is input stream (not closed)
|
||||
* @param checksum checksum implementation (CRC32, Adler32, etc.)
|
||||
* @return 4 bytes (big-endian)
|
||||
* @throws IOException if reading fails
|
||||
*/
|
||||
private static byte[] checksumChecksumBytes(InputStream is, Checksum checksum)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) != -1) {
|
||||
checksum.update(buffer, 0, read);
|
||||
}
|
||||
// Cast keeps only the lower 32 bits; mask is unnecessary here.
|
||||
int v = (int) checksum.getValue();
|
||||
return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(v).array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts bytes to a lowercase hex string.
|
||||
*
|
||||
* @param hash the byte array to convert
|
||||
* @return the lowercase hex string
|
||||
*/
|
||||
private static String toHex(byte[] hash) {
|
||||
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import org.owasp.html.AttributePolicy;
|
||||
import org.owasp.html.HtmlPolicyBuilder;
|
||||
import org.owasp.html.PolicyFactory;
|
||||
import org.owasp.html.Sanitizers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
@@ -16,7 +15,6 @@ public class CustomHtmlSanitizer {
|
||||
private final SsrfProtectionService ssrfProtectionService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
public CustomHtmlSanitizer(
|
||||
SsrfProtectionService ssrfProtectionService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
@@ -28,7 +26,7 @@ public class CustomHtmlSanitizer {
|
||||
new AttributePolicy() {
|
||||
@Override
|
||||
public String apply(String elementName, String attributeName, String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
if (value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@ import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@@ -25,11 +29,11 @@ public class EmlParser {
|
||||
private static volatile boolean mimeUtilityChecked = false;
|
||||
|
||||
private static final Pattern MIME_ENCODED_PATTERN =
|
||||
Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
||||
RegexPatternUtils.getInstance().getMimeEncodedWordPattern();
|
||||
|
||||
private static final String DISPOSITION_ATTACHMENT = "attachment";
|
||||
private static final String TEXT_PLAIN = "text/plain";
|
||||
private static final String TEXT_HTML = "text/html";
|
||||
private static final String TEXT_PLAIN = MediaType.TEXT_PLAIN_VALUE;
|
||||
private static final String TEXT_HTML = MediaType.TEXT_HTML_VALUE;
|
||||
private static final String MULTIPART_PREFIX = "multipart/";
|
||||
|
||||
private static final String HEADER_CONTENT_TYPE = "content-type:";
|
||||
@@ -69,12 +73,12 @@ public class EmlParser {
|
||||
if (isJakartaMailAvailable()) {
|
||||
return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer);
|
||||
} else {
|
||||
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
|
||||
return extractEmailContentBasic(emlBytes, customHtmlSanitizer);
|
||||
}
|
||||
}
|
||||
|
||||
private static EmailContent extractEmailContentBasic(
|
||||
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||
byte[] emlBytes, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||
String emlContent = new String(emlBytes, StandardCharsets.UTF_8);
|
||||
EmailContent content = new EmailContent();
|
||||
|
||||
@@ -121,7 +125,7 @@ public class EmlParser {
|
||||
return extractFromMimeMessage(message, request, customHtmlSanitizer);
|
||||
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
|
||||
return extractEmailContentBasic(emlBytes, customHtmlSanitizer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +147,11 @@ public class EmlParser {
|
||||
extractRecipients(message, messageClass, content);
|
||||
|
||||
Method getSentDate = messageClass.getMethod("getSentDate");
|
||||
content.setDate((Date) getSentDate.invoke(message));
|
||||
Date legacyDate = (Date) getSentDate.invoke(message);
|
||||
if (legacyDate != null) {
|
||||
content.setDate(
|
||||
ZonedDateTime.ofInstant(legacyDate.toInstant(), ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
Method getContent = messageClass.getMethod("getContent");
|
||||
Object messageContent = getContent.invoke(message);
|
||||
@@ -349,7 +357,11 @@ public class EmlParser {
|
||||
for (String contentIdHeader : contentIdHeaders) {
|
||||
if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) {
|
||||
attachment.setEmbedded(true);
|
||||
String contentId = contentIdHeader.trim().replaceAll("[<>]", "");
|
||||
String contentId =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getAngleBracketsPattern()
|
||||
.matcher(contentIdHeader.trim())
|
||||
.replaceAll("");
|
||||
attachment.setContentId(contentId);
|
||||
break;
|
||||
}
|
||||
@@ -406,7 +418,8 @@ public class EmlParser {
|
||||
|
||||
private static String extractBasicHeader(String emlContent, String headerName) {
|
||||
try {
|
||||
String[] lines = emlContent.split("\r?\n");
|
||||
String[] lines =
|
||||
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
|
||||
@@ -477,7 +490,10 @@ public class EmlParser {
|
||||
}
|
||||
|
||||
private static int findPartEnd(String content, int start) {
|
||||
String[] lines = content.substring(start).split("\r?\n");
|
||||
String[] lines =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getNewlineSplitPattern()
|
||||
.split(content.substring(start));
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (String line : lines) {
|
||||
@@ -491,7 +507,8 @@ public class EmlParser {
|
||||
private static List<EmailAttachment> extractAttachmentsBasic(String emlContent) {
|
||||
List<EmailAttachment> attachments = new ArrayList<>();
|
||||
try {
|
||||
String[] lines = emlContent.split("\r?\n");
|
||||
String[] lines =
|
||||
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
||||
boolean inHeaders = true;
|
||||
String currentContentType = "";
|
||||
String currentDisposition = "";
|
||||
@@ -554,7 +571,11 @@ public class EmlParser {
|
||||
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
||||
String extendedFilename =
|
||||
disposition.substring(filenameStarStart, filenameStarEnd).trim();
|
||||
extendedFilename = extendedFilename.replaceAll("^\"|\"$", "");
|
||||
extendedFilename =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getQuotesRemovalPattern()
|
||||
.matcher(extendedFilename)
|
||||
.replaceAll("");
|
||||
|
||||
if (extendedFilename.contains("'")) {
|
||||
String[] parts = extendedFilename.split("'", 3);
|
||||
@@ -569,7 +590,11 @@ public class EmlParser {
|
||||
int filenameEnd = disposition.indexOf(";", filenameStart);
|
||||
if (filenameEnd == -1) filenameEnd = disposition.length();
|
||||
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
||||
filename = filename.replaceAll("^\"|\"$", "");
|
||||
filename =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getQuotesRemovalPattern()
|
||||
.matcher(filename)
|
||||
.replaceAll("");
|
||||
return safeMimeDecode(filename);
|
||||
}
|
||||
|
||||
@@ -614,7 +639,7 @@ public class EmlParser {
|
||||
private String to;
|
||||
private String cc;
|
||||
private String bcc;
|
||||
private Date date;
|
||||
private ZonedDateTime date;
|
||||
private String dateString; // For basic parsing fallback
|
||||
private String htmlBody;
|
||||
private String textBody;
|
||||
@@ -622,11 +647,23 @@ public class EmlParser {
|
||||
private List<EmailAttachment> attachments = new ArrayList<>();
|
||||
|
||||
public void setHtmlBody(String htmlBody) {
|
||||
this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null;
|
||||
this.htmlBody =
|
||||
htmlBody != null
|
||||
? RegexPatternUtils.getInstance()
|
||||
.getCarriageReturnPattern()
|
||||
.matcher(htmlBody)
|
||||
.replaceAll("")
|
||||
: null;
|
||||
}
|
||||
|
||||
public void setTextBody(String textBody) {
|
||||
this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null;
|
||||
this.textBody =
|
||||
textBody != null
|
||||
? RegexPatternUtils.getInstance()
|
||||
.getCarriageReturnPattern()
|
||||
.matcher(textBody)
|
||||
.replaceAll("")
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
@@ -33,10 +35,10 @@ public class EmlProcessingUtils {
|
||||
// MIME type detection
|
||||
private static final Map<String, String> EXTENSION_TO_MIME_TYPE =
|
||||
Map.of(
|
||||
".png", "image/png",
|
||||
".jpg", "image/jpeg",
|
||||
".jpeg", "image/jpeg",
|
||||
".gif", "image/gif",
|
||||
".png", MediaType.IMAGE_PNG_VALUE,
|
||||
".jpg", MediaType.IMAGE_JPEG_VALUE,
|
||||
".jpeg", MediaType.IMAGE_JPEG_VALUE,
|
||||
".gif", MediaType.IMAGE_GIF_VALUE,
|
||||
".bmp", "image/bmp",
|
||||
".webp", "image/webp",
|
||||
".svg", "image/svg+xml",
|
||||
@@ -81,8 +83,8 @@ public class EmlProcessingUtils {
|
||||
|| lowerContent.contains("bcc:");
|
||||
boolean hasMimeStructure =
|
||||
lowerContent.contains("multipart/")
|
||||
|| lowerContent.contains("text/plain")
|
||||
|| lowerContent.contains("text/html")
|
||||
|| lowerContent.contains(MediaType.TEXT_PLAIN_VALUE)
|
||||
|| lowerContent.contains(MediaType.TEXT_HTML_VALUE)
|
||||
|| lowerContent.contains("boundary=");
|
||||
|
||||
int headerCount = 0;
|
||||
@@ -197,8 +199,16 @@ public class EmlProcessingUtils {
|
||||
String processed =
|
||||
customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody;
|
||||
|
||||
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", "");
|
||||
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", "");
|
||||
processed =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getFixedPositionCssPattern()
|
||||
.matcher(processed)
|
||||
.replaceAll("");
|
||||
processed =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getAbsolutePositionCssPattern()
|
||||
.matcher(processed)
|
||||
.replaceAll("");
|
||||
|
||||
if (emailContent != null && !emailContent.getAttachments().isEmpty()) {
|
||||
processed = PdfAttachmentHandler.processInlineImages(processed, emailContent);
|
||||
@@ -220,14 +230,18 @@ public class EmlProcessingUtils {
|
||||
html = html.replace("\n", "<br>\n");
|
||||
|
||||
html =
|
||||
html.replaceAll(
|
||||
"(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)",
|
||||
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||
RegexPatternUtils.getInstance()
|
||||
.getUrlLinkPattern()
|
||||
.matcher(html)
|
||||
.replaceAll(
|
||||
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||
|
||||
html =
|
||||
html.replaceAll(
|
||||
"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})",
|
||||
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||
RegexPatternUtils.getInstance()
|
||||
.getEmailLinkPattern()
|
||||
.matcher(html)
|
||||
.replaceAll(
|
||||
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||
|
||||
return html;
|
||||
}
|
||||
@@ -464,7 +478,7 @@ public class EmlProcessingUtils {
|
||||
}
|
||||
}
|
||||
|
||||
return "image/png";
|
||||
return MediaType.IMAGE_PNG_VALUE; // Default MIME type
|
||||
}
|
||||
|
||||
public static String decodeUrlEncoded(String encoded) {
|
||||
@@ -488,9 +502,13 @@ public class EmlProcessingUtils {
|
||||
Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText);
|
||||
String processedText =
|
||||
concatenatedMatcher.replaceAll(
|
||||
match -> match.group().replaceAll("\\s+(?==\\?)", ""));
|
||||
match ->
|
||||
RegexPatternUtils.getInstance()
|
||||
.getMimeHeaderWhitespacePattern()
|
||||
.matcher(match.group())
|
||||
.replaceAll(""));
|
||||
|
||||
Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
||||
Pattern mimePattern = RegexPatternUtils.getInstance().getMimeEncodedWordPattern();
|
||||
Matcher matcher = mimePattern.matcher(processedText);
|
||||
int lastEnd = 0;
|
||||
|
||||
@@ -505,7 +523,11 @@ public class EmlProcessingUtils {
|
||||
String decodedValue =
|
||||
switch (encoding) {
|
||||
case "B" -> {
|
||||
String cleanBase64 = encodedValue.replaceAll("\\s", "");
|
||||
String cleanBase64 =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getWhitespacePattern()
|
||||
.matcher(encodedValue)
|
||||
.replaceAll("");
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64);
|
||||
Charset targetCharset;
|
||||
try {
|
||||
@@ -594,8 +616,16 @@ public class EmlProcessingUtils {
|
||||
}
|
||||
|
||||
public static String simplifyHtmlContent(String htmlContent) {
|
||||
String simplified = htmlContent.replaceAll("(?i)<script[^>]*>.*?</script>", "");
|
||||
simplified = simplified.replaceAll("(?i)<style[^>]*>.*?</style>", "");
|
||||
String simplified =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getScriptTagPattern()
|
||||
.matcher(htmlContent)
|
||||
.replaceAll("");
|
||||
simplified =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getStyleTagPattern()
|
||||
.matcher(simplified)
|
||||
.replaceAll("");
|
||||
return simplified;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,4 +324,63 @@ public class ExceptionUtils {
|
||||
return createIllegalArgumentException(
|
||||
"error.argumentRequired", "{0} must not be null", argumentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RuntimeException for memory/image size errors when rendering PDF images with DPI.
|
||||
* Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that
|
||||
* result from images exceeding Java's array/memory limits.
|
||||
*
|
||||
* @param pageNumber the page number that caused the error
|
||||
* @param dpi the DPI value used
|
||||
* @param cause the original error/exception (e.g., OutOfMemoryError,
|
||||
* NegativeArraySizeException)
|
||||
* @return RuntimeException with user-friendly message
|
||||
*/
|
||||
public static RuntimeException createOutOfMemoryDpiException(
|
||||
int pageNumber, int dpi, Throwable cause) {
|
||||
String message =
|
||||
MessageFormat.format(
|
||||
"Out of memory or image-too-large error while rendering PDF page {0} at {1} DPI. "
|
||||
+ "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). "
|
||||
+ "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.",
|
||||
pageNumber, dpi);
|
||||
return new RuntimeException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RuntimeException for OutOfMemoryError when rendering PDF images with DPI.
|
||||
*
|
||||
* @param pageNumber the page number that caused the error
|
||||
* @param dpi the DPI value used
|
||||
* @param cause the original OutOfMemoryError
|
||||
* @return RuntimeException with user-friendly message
|
||||
*/
|
||||
public static RuntimeException createOutOfMemoryDpiException(
|
||||
int pageNumber, int dpi, OutOfMemoryError cause) {
|
||||
return createOutOfMemoryDpiException(pageNumber, dpi, (Throwable) cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RuntimeException for memory/image size errors when rendering PDF images with DPI.
|
||||
* Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that
|
||||
* result from images exceeding Java's array/memory limits.
|
||||
*
|
||||
* @param dpi the DPI value used
|
||||
* @param cause the original error/exception (e.g., OutOfMemoryError,
|
||||
* NegativeArraySizeException)
|
||||
* @return RuntimeException with user-friendly message
|
||||
*/
|
||||
public static RuntimeException createOutOfMemoryDpiException(int dpi, Throwable cause) {
|
||||
String message =
|
||||
MessageFormat.format(
|
||||
"Out of memory or image-too-large error while rendering PDF at {0} DPI. "
|
||||
+ "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). "
|
||||
+ "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.",
|
||||
dpi);
|
||||
return new RuntimeException(message, cause);
|
||||
}
|
||||
|
||||
public static RuntimeException createOutOfMemoryDpiException(int dpi, OutOfMemoryError cause) {
|
||||
return createOutOfMemoryDpiException(dpi, (Throwable) cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
@@ -124,20 +127,21 @@ public class FileToPdf {
|
||||
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
|
||||
try (ZipOutputStream zos =
|
||||
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
|
||||
Files.walk(sourceDir)
|
||||
.filter(path -> !Files.isDirectory(path))
|
||||
.forEach(
|
||||
path -> {
|
||||
ZipEntry zipEntry =
|
||||
new ZipEntry(sourceDir.relativize(path).toString());
|
||||
try {
|
||||
zos.putNextEntry(zipEntry);
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
try (Stream<Path> walk = Files.walk(sourceDir)) {
|
||||
walk.filter(path -> !Files.isDirectory(path))
|
||||
.forEach(
|
||||
path -> {
|
||||
ZipEntry zipEntry =
|
||||
new ZipEntry(sourceDir.relativize(path).toString());
|
||||
try {
|
||||
zos.putNextEntry(zipEntry);
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,15 +208,27 @@ public class FileToPdf {
|
||||
return "";
|
||||
}
|
||||
// Remove any drive letters (e.g., "C:\") and leading forward/backslashes
|
||||
entryName = entryName.replaceAll("^[a-zA-Z]:[\\\\/]+", "");
|
||||
entryName = entryName.replaceAll("^[\\\\/]+", "");
|
||||
entryName =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getDriveLetterPattern()
|
||||
.matcher(entryName)
|
||||
.replaceAll("");
|
||||
entryName =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getLeadingSlashesPattern()
|
||||
.matcher(entryName)
|
||||
.replaceAll("");
|
||||
|
||||
// Recursively remove path traversal sequences
|
||||
while (entryName.contains("../") || entryName.contains("..\\")) {
|
||||
entryName = entryName.replace("../", "").replace("..\\", "");
|
||||
}
|
||||
// Normalize all backslashes to forward slashes
|
||||
entryName = entryName.replaceAll("\\\\", "/");
|
||||
entryName =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getBackslashPattern()
|
||||
.matcher(entryName)
|
||||
.replaceAll("/");
|
||||
return entryName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.*;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public final class FormUtils {
|
||||
|
||||
private FormUtils() {}
|
||||
|
||||
public static boolean hasAnyRotatedPage(PDDocument document) {
|
||||
try {
|
||||
for (PDPage page : document.getPages()) {
|
||||
int rot = page.getRotation();
|
||||
int norm = ((rot % 360) + 360) % 360;
|
||||
if (norm != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to inspect page rotations: {}", e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void copyAndTransformFormFields(
|
||||
PDDocument sourceDocument,
|
||||
PDDocument newDocument,
|
||||
int totalPages,
|
||||
int pagesPerSheet,
|
||||
int cols,
|
||||
int rows,
|
||||
float cellWidth,
|
||||
float cellHeight)
|
||||
throws IOException {
|
||||
|
||||
PDDocumentCatalog sourceCatalog = sourceDocument.getDocumentCatalog();
|
||||
PDAcroForm sourceAcroForm = sourceCatalog.getAcroForm();
|
||||
|
||||
if (sourceAcroForm == null || sourceAcroForm.getFields().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PDDocumentCatalog newCatalog = newDocument.getDocumentCatalog();
|
||||
PDAcroForm newAcroForm = new PDAcroForm(newDocument);
|
||||
newCatalog.setAcroForm(newAcroForm);
|
||||
|
||||
PDResources dr = new PDResources();
|
||||
PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
PDType1Font zapfDingbats = new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS);
|
||||
dr.put(COSName.getPDFName("Helv"), helvetica);
|
||||
dr.put(COSName.getPDFName("ZaDb"), zapfDingbats);
|
||||
newAcroForm.setDefaultResources(dr);
|
||||
newAcroForm.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
|
||||
// Do not mutate the source AcroForm; skip bad widgets during copy
|
||||
newAcroForm.setNeedAppearances(true);
|
||||
|
||||
Map<String, Integer> fieldNameCounters = new HashMap<>();
|
||||
|
||||
// Build widget -> field map once for efficient lookups
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap = buildWidgetFieldMap(sourceAcroForm);
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
||||
List<PDAnnotation> annotations = sourcePage.getAnnotations();
|
||||
|
||||
if (annotations.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int destinationPageIndex = pageIndex / pagesPerSheet;
|
||||
int adjustedPageIndex = pageIndex % pagesPerSheet;
|
||||
int rowIndex = adjustedPageIndex / cols;
|
||||
int colIndex = adjustedPageIndex % cols;
|
||||
|
||||
if (destinationPageIndex >= newDocument.getNumberOfPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PDPage destinationPage = newDocument.getPage(destinationPageIndex);
|
||||
PDRectangle sourceRect = sourcePage.getMediaBox();
|
||||
|
||||
float scaleWidth = cellWidth / sourceRect.getWidth();
|
||||
float scaleHeight = cellHeight / sourceRect.getHeight();
|
||||
float scale = Math.min(scaleWidth, scaleHeight);
|
||||
|
||||
float x = colIndex * cellWidth + (cellWidth - sourceRect.getWidth() * scale) / 2;
|
||||
float y =
|
||||
destinationPage.getMediaBox().getHeight()
|
||||
- ((rowIndex + 1) * cellHeight
|
||||
- (cellHeight - sourceRect.getHeight() * scale) / 2);
|
||||
|
||||
copyBasicFormFields(
|
||||
sourceAcroForm,
|
||||
newAcroForm,
|
||||
sourcePage,
|
||||
destinationPage,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters,
|
||||
widgetFieldMap);
|
||||
}
|
||||
|
||||
// Refresh appearances to ensure widgets render correctly across viewers
|
||||
try {
|
||||
// Use reflection to avoid compile-time dependency on PDFBox version
|
||||
Method m = newAcroForm.getClass().getMethod("refreshAppearances");
|
||||
m.invoke(newAcroForm);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
log.warn(
|
||||
"AcroForm.refreshAppearances() not available in this PDFBox version; relying on NeedAppearances.");
|
||||
} catch (Throwable t) {
|
||||
log.warn("Failed to refresh field appearances via AcroForm: {}", t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyBasicFormFields(
|
||||
PDAcroForm sourceAcroForm,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage sourcePage,
|
||||
PDPage destinationPage,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters,
|
||||
Map<PDAnnotationWidget, PDField> widgetFieldMap) {
|
||||
|
||||
try {
|
||||
List<PDAnnotation> sourceAnnotations = sourcePage.getAnnotations();
|
||||
List<PDAnnotation> destinationAnnotations = destinationPage.getAnnotations();
|
||||
|
||||
for (PDAnnotation annotation : sourceAnnotations) {
|
||||
if (annotation instanceof PDAnnotationWidget widgetAnnotation) {
|
||||
if (widgetAnnotation.getRectangle() == null) {
|
||||
continue;
|
||||
}
|
||||
PDField sourceField =
|
||||
widgetFieldMap != null ? widgetFieldMap.get(widgetAnnotation) : null;
|
||||
if (sourceField == null) {
|
||||
continue; // skip widgets without a matching field
|
||||
}
|
||||
if (sourceField instanceof PDTextField pdtextfield) {
|
||||
createSimpleTextField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdtextfield,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDCheckBox pdCheckBox) {
|
||||
createSimpleCheckBoxField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdCheckBox,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDRadioButton pdRadioButton) {
|
||||
createSimpleRadioButtonField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdRadioButton,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDComboBox pdComboBox) {
|
||||
createSimpleComboBoxField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdComboBox,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDListBox pdlistbox) {
|
||||
createSimpleListBoxField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdlistbox,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDSignatureField pdSignatureField) {
|
||||
createSimpleSignatureField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdSignatureField,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
} else if (sourceField instanceof PDPushButton pdPushButton) {
|
||||
createSimplePushButtonField(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
pdPushButton,
|
||||
widgetAnnotation,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to copy basic form fields for page {}: {}",
|
||||
pageIndex,
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleTextField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDTextField sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDTextField newTextField = new PDTextField(newAcroForm);
|
||||
newTextField.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newTextField,
|
||||
sourceField.getPartialName(),
|
||||
"textField",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getValueAsString() != null) {
|
||||
newTextField.setValue(sourceField.getValueAsString());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create text field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleCheckBoxField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDCheckBox sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDCheckBox newCheckBox = new PDCheckBox(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newCheckBox,
|
||||
sourceField.getPartialName(),
|
||||
"checkBox",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.isChecked()) {
|
||||
newCheckBox.check();
|
||||
} else {
|
||||
newCheckBox.unCheck();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create checkbox field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleRadioButtonField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDRadioButton sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDRadioButton newRadioButton = new PDRadioButton(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newRadioButton,
|
||||
sourceField.getPartialName(),
|
||||
"radioButton",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getExportValues() != null) {
|
||||
newRadioButton.setExportValues(sourceField.getExportValues());
|
||||
}
|
||||
if (sourceField.getValue() != null) {
|
||||
newRadioButton.setValue(sourceField.getValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create radio button field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleComboBoxField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDComboBox sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDComboBox newComboBox = new PDComboBox(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newComboBox,
|
||||
sourceField.getPartialName(),
|
||||
"comboBox",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getOptions() != null) {
|
||||
newComboBox.setOptions(sourceField.getOptions());
|
||||
}
|
||||
if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) {
|
||||
newComboBox.setValue(sourceField.getValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create combo box field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleListBoxField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDListBox sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDListBox newListBox = new PDListBox(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newListBox,
|
||||
sourceField.getPartialName(),
|
||||
"listBox",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceField.getOptions() != null) {
|
||||
newListBox.setOptions(sourceField.getOptions());
|
||||
}
|
||||
if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) {
|
||||
newListBox.setValue(sourceField.getValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create list box field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimpleSignatureField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDSignatureField sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDSignatureField newSignatureField = new PDSignatureField(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newSignatureField,
|
||||
sourceField.getPartialName(),
|
||||
"signature",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create signature field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSimplePushButtonField(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
PDPushButton sourceField,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
try {
|
||||
PDPushButton newPushButton = new PDPushButton(newAcroForm);
|
||||
|
||||
boolean initialized =
|
||||
initializeFieldWithWidget(
|
||||
newAcroForm,
|
||||
destinationPage,
|
||||
destinationAnnotations,
|
||||
newPushButton,
|
||||
sourceField.getPartialName(),
|
||||
"pushButton",
|
||||
sourceWidget,
|
||||
offsetX,
|
||||
offsetY,
|
||||
scale,
|
||||
pageIndex,
|
||||
fieldNameCounters);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to create push button field '{}': {}",
|
||||
sourceField.getPartialName(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends PDTerminalField> boolean initializeFieldWithWidget(
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
List<PDAnnotation> destinationAnnotations,
|
||||
T newField,
|
||||
String originalName,
|
||||
String fallbackName,
|
||||
PDAnnotationWidget sourceWidget,
|
||||
float offsetX,
|
||||
float offsetY,
|
||||
float scale,
|
||||
int pageIndex,
|
||||
Map<String, Integer> fieldNameCounters) {
|
||||
|
||||
String baseName = (originalName != null) ? originalName : fallbackName;
|
||||
String newFieldName = generateUniqueFieldName(baseName, pageIndex, fieldNameCounters);
|
||||
newField.setPartialName(newFieldName);
|
||||
|
||||
PDAnnotationWidget newWidget = new PDAnnotationWidget();
|
||||
PDRectangle sourceRect = sourceWidget.getRectangle();
|
||||
if (sourceRect == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float newX = (sourceRect.getLowerLeftX() * scale) + offsetX;
|
||||
float newY = (sourceRect.getLowerLeftY() * scale) + offsetY;
|
||||
float newWidth = sourceRect.getWidth() * scale;
|
||||
float newHeight = sourceRect.getHeight() * scale;
|
||||
newWidget.setRectangle(new PDRectangle(newX, newY, newWidth, newHeight));
|
||||
newWidget.setPage(destinationPage);
|
||||
|
||||
newField.getWidgets().add(newWidget);
|
||||
newWidget.setParent(newField);
|
||||
newAcroForm.getFields().add(newField);
|
||||
destinationAnnotations.add(newWidget);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String generateUniqueFieldName(
|
||||
String originalName, int pageIndex, Map<String, Integer> fieldNameCounters) {
|
||||
String baseName = "page" + pageIndex + "_" + originalName;
|
||||
|
||||
Integer counter = fieldNameCounters.get(baseName);
|
||||
if (counter == null) {
|
||||
counter = 0;
|
||||
} else {
|
||||
counter++;
|
||||
}
|
||||
fieldNameCounters.put(baseName, counter);
|
||||
|
||||
return counter == 0 ? baseName : baseName + "_" + counter;
|
||||
}
|
||||
|
||||
private static Map<PDAnnotationWidget, PDField> buildWidgetFieldMap(PDAcroForm acroForm) {
|
||||
Map<PDAnnotationWidget, PDField> map = new HashMap<>();
|
||||
if (acroForm == null) {
|
||||
return map;
|
||||
}
|
||||
try {
|
||||
for (PDField field : acroForm.getFieldTree()) {
|
||||
List<PDAnnotationWidget> widgets = field.getWidgets();
|
||||
if (widgets != null) {
|
||||
for (PDAnnotationWidget w : widgets) {
|
||||
if (w != null) {
|
||||
map.put(w, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to build widget->field map: {}", e.getMessage(), e);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,9 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -28,25 +24,37 @@ import com.fathzer.soft.javaluator.DoubleEvaluator;
|
||||
import io.github.pixee.security.HostValidator;
|
||||
import io.github.pixee.security.Urls;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class GeneralUtils {
|
||||
|
||||
private static final Set<String> DEFAULT_VALID_SCRIPTS =
|
||||
Set.of("png_to_webp.py", "split_photos.py");
|
||||
private static final Set<String> DEFAULT_VALID_PIPELINE =
|
||||
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
|
||||
private final Set<String> DEFAULT_VALID_PIPELINE =
|
||||
Set.of(
|
||||
"OCR images.json",
|
||||
"Prepare-pdfs-for-email.json",
|
||||
"split-rotate-auto-rename.json");
|
||||
|
||||
private static final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs";
|
||||
private static final String PYTHON_SCRIPTS_DIR = "python";
|
||||
private final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs";
|
||||
private final String PYTHON_SCRIPTS_DIR = "python";
|
||||
private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance();
|
||||
// Valid size units used for convertSizeToBytes validation and parsing
|
||||
private final Set<String> VALID_SIZE_UNITS = Set.of("B", "KB", "MB", "GB", "TB");
|
||||
|
||||
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||
/*
|
||||
* Converts a MultipartFile to a regular File with improved performance and security.
|
||||
*
|
||||
* @param multipartFile the multipart file to convert
|
||||
* @return temporary File containing the multipart file data
|
||||
* @throws IOException if I/O error occurs during conversion
|
||||
* @throws IllegalArgumentException if file exceeds maximum allowed size
|
||||
*/
|
||||
public File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||
if (customTempDir == null || customTempDir.isEmpty()) {
|
||||
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
||||
@@ -81,10 +89,137 @@ public class GeneralUtils {
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
public static void deleteDirectory(Path path) throws IOException {
|
||||
/*
|
||||
* Gets the configured temporary directory, creating it if necessary.
|
||||
*
|
||||
* @return Path to the temporary directory
|
||||
* @throws IOException if directory creation fails
|
||||
*/
|
||||
private Path getTempDirectory() throws IOException {
|
||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||
if (customTempDir == null || customTempDir.isEmpty()) {
|
||||
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
||||
}
|
||||
|
||||
Path tempDir;
|
||||
if (customTempDir != null && !customTempDir.isEmpty()) {
|
||||
tempDir = Path.of(customTempDir);
|
||||
} else {
|
||||
tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
|
||||
}
|
||||
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
}
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove file extension
|
||||
*
|
||||
* <p>Uses fast string operations for common cases (valid extensions) and falls back to
|
||||
* optimized regex for edge cases (no extension, hidden files, etc.).
|
||||
*
|
||||
* <ul>
|
||||
* <li>String operations avoid regex engine overhead for common cases
|
||||
* <li>Cached pattern compilation eliminates recompilation costs
|
||||
* <li>Fresh Matcher instances ensure thread safety
|
||||
* </ul>
|
||||
*
|
||||
* @param filename the filename to process, may be null
|
||||
* @return filename without extension, or "default" if input is null
|
||||
*/
|
||||
public String removeExtension(String filename) {
|
||||
if (filename == null) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (filename.isEmpty()) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
int dotIndex = filename.lastIndexOf('.');
|
||||
if (dotIndex > 0 && dotIndex < filename.length() - 1) {
|
||||
return filename.substring(0, dotIndex);
|
||||
}
|
||||
|
||||
if (dotIndex == 0 || dotIndex == filename.length() - 1 || dotIndex == -1) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
Pattern pattern = patternCache.getPattern(RegexPatternUtils.getExtensionRegex());
|
||||
Matcher matcher = pattern.matcher(filename);
|
||||
return matcher.find() ? matcher.replaceFirst("") : filename;
|
||||
}
|
||||
|
||||
/*
|
||||
* Append suffix to base name with null safety.
|
||||
*
|
||||
* @param baseName the base filename, null becomes "default"
|
||||
* @param suffix the suffix to append, null becomes empty string
|
||||
* @return concatenated string with null safety
|
||||
*/
|
||||
public String appendSuffix(String baseName, String suffix) {
|
||||
return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : "");
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate a PDF filename by removing extension from first file and adding suffix.
|
||||
*
|
||||
* <p>High-level utility method for common PDF naming scenarios. Handles null safety and uses
|
||||
* extension removal.
|
||||
*
|
||||
* @param firstFilename the filename of the first file being, may be null
|
||||
* @param suffix the suffix to append (e.g., "_merged.pdf")
|
||||
* @return filename with suffix, or default name if input is null
|
||||
*/
|
||||
public String generateFilename(String firstFilename, String suffix) {
|
||||
String baseName = removeExtension(firstFilename);
|
||||
return appendSuffix(baseName, suffix);
|
||||
}
|
||||
|
||||
/*
|
||||
* Process a list of filenames by removing extensions and adding suffix.
|
||||
*
|
||||
* <p>Efficiently processes multiple filenames using streaming operations and bulk operations
|
||||
* where possible. Handles null safety for both input list and individual filenames.
|
||||
*
|
||||
* @param filenames the list of filenames to process, may be null
|
||||
* @param suffix the suffix to append to each processed filename
|
||||
* @param processor consumer to handle each processed filename, may be null
|
||||
*/
|
||||
public void processFilenames(
|
||||
List<String> filenames, String suffix, java.util.function.Consumer<String> processor) {
|
||||
if (filenames == null || processor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
filenames.stream()
|
||||
.map(filename -> appendSuffix(removeExtension(filename), suffix))
|
||||
.forEach(processor);
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract title from filename by removing extension, with fallback handling.
|
||||
*
|
||||
* <p>Returns "Untitled" for null or empty filenames, otherwise removes the extension using the
|
||||
* optimized removeExtension method.
|
||||
*
|
||||
* @param filename the filename to extract title from, may be null
|
||||
* @return the title without extension, or "Untitled" if input is null/empty
|
||||
*/
|
||||
public String getTitleFromFilename(String filename) {
|
||||
if (filename == null || filename.isEmpty()) {
|
||||
return "Untitled";
|
||||
}
|
||||
return removeExtension(filename);
|
||||
}
|
||||
|
||||
public void deleteDirectory(Path path) throws IOException {
|
||||
Files.walkFileTree(
|
||||
path,
|
||||
new SimpleFileVisitor<Path>() {
|
||||
new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
@@ -101,8 +236,18 @@ public class GeneralUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static String convertToFileName(String name) {
|
||||
String safeName = name.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
public String convertToFileName(String name) {
|
||||
if (name == null) return "_";
|
||||
StringBuilder safeNameBuilder = new StringBuilder(name.length());
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char c = name.charAt(i);
|
||||
if (Character.isLetterOrDigit(c)) {
|
||||
safeNameBuilder.append(c);
|
||||
} else {
|
||||
safeNameBuilder.append('_');
|
||||
}
|
||||
}
|
||||
String safeName = safeNameBuilder.toString();
|
||||
if (safeName.length() > 50) {
|
||||
safeName = safeName.substring(0, 50);
|
||||
}
|
||||
@@ -110,19 +255,20 @@ public class GeneralUtils {
|
||||
}
|
||||
|
||||
// Get resources from a location pattern
|
||||
public static Resource[] getResourcesFromLocationPattern(
|
||||
public Resource[] getResourcesFromLocationPattern(
|
||||
String locationPattern, ResourceLoader resourceLoader) throws Exception {
|
||||
// Normalize the path for file resources
|
||||
if (locationPattern.startsWith("file:")) {
|
||||
String rawPath = locationPattern.substring(5).replace("\\*", "").replace("/*", "");
|
||||
String pattern = locationPattern;
|
||||
if (pattern.startsWith("file:")) {
|
||||
String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", "");
|
||||
Path normalizePath = Paths.get(rawPath).normalize();
|
||||
locationPattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
||||
pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
||||
}
|
||||
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||
.getResources(locationPattern);
|
||||
.getResources(pattern);
|
||||
}
|
||||
|
||||
public static boolean isValidURL(String urlStr) {
|
||||
public boolean isValidURL(String urlStr) {
|
||||
try {
|
||||
Urls.create(
|
||||
urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS);
|
||||
@@ -132,7 +278,25 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isURLReachable(String urlStr) {
|
||||
/*
|
||||
* Checks if a URL is reachable with proper timeout configuration and error handling.
|
||||
*
|
||||
* @param urlStr the URL string to check
|
||||
* @return true if URL is reachable, false otherwise
|
||||
*/
|
||||
public boolean isURLReachable(String urlStr) {
|
||||
return isURLReachable(urlStr, 5000, 5000);
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if a URL is reachable with configurable timeouts.
|
||||
*
|
||||
* @param urlStr the URL string to check
|
||||
* @param connectTimeout connection timeout in milliseconds
|
||||
* @param readTimeout read timeout in milliseconds
|
||||
* @return true if URL is reachable, false otherwise
|
||||
*/
|
||||
public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) {
|
||||
try {
|
||||
// Parse the URL
|
||||
URL url = URI.create(urlStr).toURL();
|
||||
@@ -152,16 +316,19 @@ public class GeneralUtils {
|
||||
// Check if the URL is reachable
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("HEAD");
|
||||
// connection.setConnectTimeout(5000); // Set connection timeout
|
||||
// connection.setReadTimeout(5000); // Set read timeout
|
||||
connection.setConnectTimeout(connectTimeout);
|
||||
connection.setReadTimeout(readTimeout);
|
||||
connection.setInstanceFollowRedirects(false); // Security: prevent redirect loops
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
return (200 <= responseCode && responseCode <= 399);
|
||||
} catch (Exception e) {
|
||||
log.debug("URL {} is not reachable: {}", urlStr, e.getMessage());
|
||||
return false; // Return false in case of any exception
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLocalAddress(String host) {
|
||||
private boolean isLocalAddress(String host) {
|
||||
try {
|
||||
// Resolve DNS to IP address
|
||||
InetAddress address = InetAddress.getByName(host);
|
||||
@@ -181,7 +348,14 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static File multipartToFile(MultipartFile multipart) throws IOException {
|
||||
/*
|
||||
* Improved multipart file conversion using the shared helper method.
|
||||
*
|
||||
* @param multipart the multipart file to convert
|
||||
* @return temporary File containing the multipart file data
|
||||
* @throws IOException if I/O error occurs during conversion
|
||||
*/
|
||||
public File multipartToFile(MultipartFile multipart) throws IOException {
|
||||
Path tempFile = Files.createTempFile("overlay-", ".pdf");
|
||||
try (InputStream in = multipart.getInputStream();
|
||||
FileOutputStream out = new FileOutputStream(tempFile.toFile())) {
|
||||
@@ -194,54 +368,105 @@ public class GeneralUtils {
|
||||
return tempFile.toFile();
|
||||
}
|
||||
|
||||
public static Long convertSizeToBytes(String sizeStr) {
|
||||
/*
|
||||
* Supports TB/PB units and provides detailed error messages.
|
||||
*
|
||||
* @param sizeStr the size string to convert (e.g., "100MB", "1.5GB")
|
||||
* @param defaultUnit the default unit to assume if none specified ("MB", "GB", etc.)
|
||||
* @return size in bytes, or null if parsing fails
|
||||
* @throws IllegalArgumentException if defaultUnit is invalid
|
||||
*/
|
||||
public Long convertSizeToBytes(String sizeStr, String defaultUnit) {
|
||||
if (sizeStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (defaultUnit != null && !isValidSizeUnit(defaultUnit)) {
|
||||
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
|
||||
}
|
||||
|
||||
sizeStr = sizeStr.trim().toUpperCase();
|
||||
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
||||
|
||||
try {
|
||||
if (sizeStr.endsWith("KB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
if (sizeStr.endsWith("TB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024
|
||||
* 1024);
|
||||
* 1024L
|
||||
* 1024L
|
||||
* 1024L
|
||||
* 1024L);
|
||||
} else if (sizeStr.endsWith("GB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024
|
||||
* 1024
|
||||
* 1024);
|
||||
} else if (sizeStr.endsWith("B")) {
|
||||
* 1024L
|
||||
* 1024L
|
||||
* 1024L);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||
* 1024L
|
||||
* 1024L);
|
||||
} else if (sizeStr.endsWith("KB")) {
|
||||
return (long)
|
||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024L);
|
||||
} else if (!sizeStr.isEmpty() && sizeStr.charAt(sizeStr.length() - 1) == 'B') {
|
||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
} else {
|
||||
// Assume MB if no unit is specified
|
||||
return (long) (Double.parseDouble(sizeStr) * 1024 * 1024);
|
||||
// Use provided default unit or fall back to MB
|
||||
String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB";
|
||||
double value = Double.parseDouble(sizeStr);
|
||||
return switch (unit) {
|
||||
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
|
||||
case "GB" -> (long) (value * 1024L * 1024L * 1024L);
|
||||
case "MB" -> (long) (value * 1024L * 1024L);
|
||||
case "KB" -> (long) (value * 1024L);
|
||||
case "B" -> (long) value;
|
||||
default -> (long) (value * 1024L * 1024L); // Default to MB
|
||||
};
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// The numeric part of the input string cannot be parsed, handle this case
|
||||
log.warn("Failed to parse size string '{}': {}", sizeStr, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
/*
|
||||
* Converts size string to bytes using MB as default unit.
|
||||
*
|
||||
* @param sizeStr the size string to convert
|
||||
* @return size in bytes, or null if parsing fails
|
||||
*/
|
||||
public Long convertSizeToBytes(String sizeStr) {
|
||||
return convertSizeToBytes(sizeStr, "MB");
|
||||
}
|
||||
|
||||
/* Validates if a string represents a valid size unit. */
|
||||
private boolean isValidSizeUnit(String unit) {
|
||||
// Use a precomputed Set for O(1) lookup, normalize using a locale-safe toUpperCase
|
||||
return unit != null && VALID_SIZE_UNITS.contains(unit.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/* Enhanced byte formatting with TB/PB support and better precision. */
|
||||
public String formatBytes(long bytes) {
|
||||
if (bytes < 0) {
|
||||
return "Invalid size";
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
} else if (bytes < 1024L * 1024L) {
|
||||
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
} else if (bytes < 1024L * 1024L * 1024L) {
|
||||
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
|
||||
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) {
|
||||
public List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) {
|
||||
if (pages == null) {
|
||||
return List.of(1); // Default to first page if input is null
|
||||
}
|
||||
@@ -252,11 +477,11 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String[] pages, int totalPages) {
|
||||
public List<Integer> parsePageList(String[] pages, int totalPages) {
|
||||
return parsePageList(pages, totalPages, false);
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
||||
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
int offset = oneBased ? 1 : 0;
|
||||
for (String page : pages) {
|
||||
@@ -278,33 +503,72 @@ public class GeneralUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<Integer> evaluateNFunc(String expression, int maxValue) {
|
||||
/*
|
||||
* Enhanced mathematical expression evaluation with bounds checking and timeout protection.
|
||||
*
|
||||
* @param expression the mathematical expression containing 'n'
|
||||
* @param maxValue the maximum value for 'n' and result bounds
|
||||
* @return list of valid page numbers
|
||||
* @throws IllegalArgumentException if expression is invalid or unsafe
|
||||
*/
|
||||
public List<Integer> evaluateNFunc(String expression, int maxValue) {
|
||||
if (expression == null || expression.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Expression cannot be null or empty");
|
||||
}
|
||||
|
||||
if (maxValue <= 0 || maxValue > 10000) {
|
||||
throw new IllegalArgumentException("maxValue must be between 1 and 10000 for safety");
|
||||
}
|
||||
|
||||
List<Integer> results = new ArrayList<>();
|
||||
DoubleEvaluator evaluator = new DoubleEvaluator();
|
||||
|
||||
// Validate the expression
|
||||
if (!expression.matches("[0-9n+\\-*/() ]+")) {
|
||||
throw new IllegalArgumentException("Invalid expression");
|
||||
// Validate the expression format
|
||||
if (!RegexPatternUtils.getInstance()
|
||||
.getMathExpressionPattern()
|
||||
.matcher(expression.trim())
|
||||
.matches()) {
|
||||
throw new IllegalArgumentException("Invalid expression format: " + expression);
|
||||
}
|
||||
|
||||
for (int n = 1; n <= maxValue; n++) {
|
||||
// Replace 'n' with the current value of n, correctly handling numbers before
|
||||
// 'n'
|
||||
String sanitizedExpression = sanitizeNFunction(expression, n);
|
||||
Double result = evaluator.evaluate(sanitizedExpression);
|
||||
try {
|
||||
// Replace 'n' with the current value of n, correctly handling numbers before 'n'
|
||||
String sanitizedExpression = sanitizeNFunction(expression.trim(), n);
|
||||
Double result = evaluator.evaluate(sanitizedExpression);
|
||||
|
||||
// Check if the result is null or not within bounds
|
||||
if (result == null) break;
|
||||
// Check if the result is null or not within bounds
|
||||
if (result == null || !Double.isFinite(result)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.intValue() > 0 && result.intValue() <= maxValue)
|
||||
results.add(result.intValue());
|
||||
int intResult = result.intValue();
|
||||
if (intResult > 0 && intResult <= maxValue) {
|
||||
results.add(intResult);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"Failed to evaluate expression '{}' for n={}: {}",
|
||||
expression,
|
||||
n,
|
||||
e.getMessage());
|
||||
// Continue with next value instead of breaking
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static String sanitizeNFunction(String expression, int nValue) {
|
||||
String sanitizedExpression = expression.replace(" ", "");
|
||||
private String sanitizeNFunction(String expression, int nValue) {
|
||||
// Remove all spaces using a specialized character removal
|
||||
StringBuilder sb = new StringBuilder(expression.length());
|
||||
for (int i = 0; i < expression.length(); i++) {
|
||||
char c = expression.charAt(i);
|
||||
if (c != ' ') {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
String sanitizedExpression = sb.toString();
|
||||
String multiplyByOpeningRoundBracketPattern =
|
||||
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
|
||||
sanitizedExpression =
|
||||
@@ -319,23 +583,45 @@ public class GeneralUtils {
|
||||
return sanitizedExpression;
|
||||
}
|
||||
|
||||
private static String insertMultiplicationBeforeN(String expression, int nValue) {
|
||||
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
||||
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
||||
private String insertMultiplicationBeforeN(String expression, int nValue) {
|
||||
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") using a loop
|
||||
StringBuilder sb = new StringBuilder(expression.length() + 4); // +4 for possible extra '*'
|
||||
for (int i = 0; i < expression.length(); i++) {
|
||||
char c = expression.charAt(i);
|
||||
sb.append(c);
|
||||
if (Character.isDigit(c)
|
||||
&& i + 1 < expression.length()
|
||||
&& expression.charAt(i + 1) == 'n') {
|
||||
sb.append('*');
|
||||
}
|
||||
}
|
||||
String withMultiplication = sb.toString();
|
||||
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
||||
// Now replace 'n' with its current value
|
||||
return withMultiplication.replace("n", String.valueOf(nValue));
|
||||
}
|
||||
|
||||
private static String formatConsecutiveNsForNFunction(String expression) {
|
||||
private String formatConsecutiveNsForNFunction(String expression) {
|
||||
String text = expression;
|
||||
while (text.matches(".*n{2,}.*")) {
|
||||
text = text.replaceAll("(?<!n)n{2}", "n*n");
|
||||
// Replace all consecutive 'nn' with 'n*n' until no more 'nn' is found
|
||||
while (text.contains("nn")) {
|
||||
StringBuilder sb = new StringBuilder(text.length() + 2); // +2 for possible extra '*'
|
||||
int i = 0;
|
||||
while (i < text.length()) {
|
||||
if (i < text.length() - 1 && text.charAt(i) == 'n' && text.charAt(i + 1) == 'n') {
|
||||
sb.append("n*n");
|
||||
i += 2;
|
||||
} else {
|
||||
sb.append(text.charAt(i));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
text = sb.toString();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static List<Integer> handlePart(String part, int totalPages, int offset) {
|
||||
private List<Integer> handlePart(String part, int totalPages, int offset) {
|
||||
List<Integer> partResult = new ArrayList<>();
|
||||
|
||||
// First check for n-syntax because it should not be processed as a range
|
||||
@@ -361,7 +647,7 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Range is invalid, ignore this part
|
||||
log.debug("Invalid range: {}", part);
|
||||
}
|
||||
} else {
|
||||
// This is a single page number
|
||||
@@ -370,14 +656,14 @@ public class GeneralUtils {
|
||||
if (pageNum >= 1 && pageNum <= totalPages) {
|
||||
partResult.add(pageNum - 1 + offset);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Ignore invalid numbers
|
||||
} catch (NumberFormatException e) {
|
||||
log.debug("Invalid page number: {}", part);
|
||||
}
|
||||
}
|
||||
return partResult;
|
||||
}
|
||||
|
||||
public static boolean createDir(String path) {
|
||||
public boolean createDir(String path) {
|
||||
Path folder = Paths.get(path);
|
||||
if (!Files.exists(folder)) {
|
||||
try {
|
||||
@@ -390,7 +676,7 @@ public class GeneralUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isValidUUID(String uuid) {
|
||||
public boolean isValidUUID(String uuid) {
|
||||
if (uuid == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -406,7 +692,7 @@ public class GeneralUtils {
|
||||
* Internal Implementation Details *
|
||||
*------------------------------------------------------------------------*/
|
||||
|
||||
public static void saveKeyToSettings(String key, Object newValue) throws IOException {
|
||||
public void saveKeyToSettings(String key, Object newValue) throws IOException {
|
||||
String[] keyArray = key.split("\\.");
|
||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
||||
@@ -414,48 +700,67 @@ public class GeneralUtils {
|
||||
settingsYaml.saveOverride(settingsPath);
|
||||
}
|
||||
|
||||
public static String generateMachineFingerprint() {
|
||||
/*
|
||||
* Machine fingerprint generation with better error logging and fallbacks.
|
||||
*
|
||||
* @return unique machine fingerprint or "GenericID" if generation fails
|
||||
*/
|
||||
public String generateMachineFingerprint() {
|
||||
try {
|
||||
// Get the MAC address
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// Try to get MAC address from primary network interface
|
||||
InetAddress ip = InetAddress.getLocalHost();
|
||||
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
||||
|
||||
if (network == null) {
|
||||
if (network == null || network.getHardwareAddress() == null) {
|
||||
// Fallback: iterate through all network interfaces
|
||||
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
||||
while (networks.hasMoreElements()) {
|
||||
NetworkInterface net = networks.nextElement();
|
||||
byte[] mac = net.getHardwareAddress();
|
||||
if (mac != null) {
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X", mac[i]));
|
||||
if (net.isUp() && !net.isLoopback() && !net.isVirtual()) {
|
||||
byte[] mac = net.getHardwareAddress();
|
||||
if (mac != null && mac.length > 0) {
|
||||
for (byte b : mac) {
|
||||
sb.append(String.format("%02X", b));
|
||||
}
|
||||
break; // Use the first valid network interface
|
||||
}
|
||||
break; // Use the first network interface with a MAC address
|
||||
}
|
||||
}
|
||||
} else {
|
||||
byte[] mac = network.getHardwareAddress();
|
||||
if (mac != null) {
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X", mac[i]));
|
||||
for (byte b : mac) {
|
||||
sb.append(String.format("%02X", b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the MAC address for privacy and consistency
|
||||
// If no MAC address found, use hostname as fallback
|
||||
if (sb.length() == 0) {
|
||||
String hostname = InetAddress.getLocalHost().getHostName();
|
||||
sb.append(hostname != null ? hostname : "unknown-host");
|
||||
log.warn("No MAC address found, using hostname for fingerprint generation");
|
||||
}
|
||||
|
||||
// Hash the collected data for privacy and consistency
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder fingerprint = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
fingerprint.append(String.format("%02x", b));
|
||||
}
|
||||
|
||||
log.debug("Successfully generated machine fingerprint");
|
||||
return fingerprint.toString();
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to generate machine fingerprint: {}", e.getMessage());
|
||||
return "GenericID";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Extracts the default pipeline configurations from the classpath to the installation path.
|
||||
* Creates directories if needed and copies default JSON files.
|
||||
*
|
||||
@@ -464,7 +769,7 @@ public class GeneralUtils {
|
||||
*
|
||||
* @throws IOException if an I/O error occurs during file operations
|
||||
*/
|
||||
public static void extractPipeline() throws IOException {
|
||||
public void extractPipeline() throws IOException {
|
||||
Path pipelineDir =
|
||||
Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
|
||||
Files.createDirectories(pipelineDir);
|
||||
@@ -486,7 +791,7 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Extracts the specified Python script from the classpath to the installation path. Validates
|
||||
* name and copies file atomically when possible, overwriting existing.
|
||||
*
|
||||
@@ -497,7 +802,7 @@ public class GeneralUtils {
|
||||
* @throws IllegalArgumentException if the script name is invalid or not allowed
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static Path extractScript(String scriptName) throws IOException {
|
||||
public Path extractScript(String scriptName) throws IOException {
|
||||
// Validate input
|
||||
if (scriptName == null || scriptName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("scriptName must not be null or empty");
|
||||
@@ -530,15 +835,14 @@ public class GeneralUtils {
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
/*
|
||||
* Copies a resource from the classpath to a specified target file.
|
||||
*
|
||||
* @param resource the ClassPathResource to copy
|
||||
* @param target the target Path where the resource will be copied
|
||||
* @throws IOException if an I/O error occurs during the copy operation
|
||||
*/
|
||||
private static void copyResourceToFile(ClassPathResource resource, Path target)
|
||||
throws IOException {
|
||||
private void copyResourceToFile(ClassPathResource resource, Path target) throws IOException {
|
||||
Path dir = target.getParent();
|
||||
Path tmp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp");
|
||||
try (InputStream in = resource.getInputStream()) {
|
||||
@@ -573,7 +877,7 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
||||
public boolean isVersionHigher(String currentVersion, String compareVersion) {
|
||||
if (currentVersion == null || compareVersion == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -601,4 +905,67 @@ public class GeneralUtils {
|
||||
// If all components so far are equal, the longer version is considered higher
|
||||
return current.length > compare.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizes a PDF using Ghostscript with ebook settings for better e-reader compatibility. Uses
|
||||
* -dPDFSETTINGS=/ebook -dFastWebView=true settings to create an optimized PDF.
|
||||
*
|
||||
* @param inputPdfBytes Original PDF as byte array
|
||||
* @return Optimized PDF as byte array
|
||||
* @throws IOException if Ghostscript optimization fails
|
||||
*/
|
||||
public byte[] optimizePdfWithGhostscript(byte[] inputPdfBytes) throws IOException {
|
||||
Path tempInput = null;
|
||||
Path tempOutput = null;
|
||||
|
||||
try {
|
||||
tempInput = Files.createTempFile("gs_input_", ".pdf");
|
||||
tempOutput = Files.createTempFile("gs_output_", ".pdf");
|
||||
|
||||
Files.write(tempInput, inputPdfBytes);
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("gs");
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-dPDFSETTINGS=/ebook");
|
||||
command.add("-dFastWebView=true");
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dQUIET");
|
||||
command.add("-dBATCH");
|
||||
command.add("-sOutputFile=" + tempOutput.toString());
|
||||
command.add(tempInput.toString());
|
||||
|
||||
ProcessExecutor.ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
if (result.getRc() != 0) {
|
||||
log.warn(
|
||||
"Ghostscript ebook optimization failed with return code: {}",
|
||||
result.getRc());
|
||||
throw ExceptionUtils.createGhostscriptCompressionException();
|
||||
}
|
||||
|
||||
return Files.readAllBytes(tempOutput);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Ghostscript ebook optimization failed", e);
|
||||
throw ExceptionUtils.createGhostscriptCompressionException(e);
|
||||
} finally {
|
||||
if (tempInput != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempInput);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp input file: {}", tempInput, e);
|
||||
}
|
||||
}
|
||||
if (tempOutput != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempOutput);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp output file: {}", tempOutput, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,19 +85,16 @@ public class ImageProcessingUtils {
|
||||
return 0;
|
||||
}
|
||||
int orientationTag = directory.getInt(ExifSubIFDDirectory.TAG_ORIENTATION);
|
||||
switch (orientationTag) {
|
||||
case 1:
|
||||
return 0;
|
||||
case 6:
|
||||
return 90;
|
||||
case 3:
|
||||
return 180;
|
||||
case 8:
|
||||
return 270;
|
||||
default:
|
||||
return switch (orientationTag) {
|
||||
case 1 -> 0;
|
||||
case 6 -> 90;
|
||||
case 3 -> 180;
|
||||
case 8 -> 270;
|
||||
default -> {
|
||||
log.warn("Unknown orientation tag: {}", orientationTag);
|
||||
return 0;
|
||||
}
|
||||
yield 0;
|
||||
}
|
||||
};
|
||||
} catch (ImageProcessingException | MetadataException e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PDFService {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
/*
|
||||
* Merge multiple PDF documents into a single PDF document
|
||||
*
|
||||
* @param documents List of PDDocument to be merged
|
||||
* @return Merged PDDocument
|
||||
* @throws IOException If an error occurs during merging
|
||||
*/
|
||||
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||
PDDocument merged = pdfDocumentFactory.createNewDocument();
|
||||
PDFMergerUtility merger = new PDFMergerUtility();
|
||||
for (PDDocument doc : documents) {
|
||||
merger.appendDocument(merged, doc);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
@@ -25,18 +25,22 @@ import com.vladsch.flexmark.util.data.MutableDataSet;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
@Slf4j
|
||||
@NoArgsConstructor
|
||||
public class PDFToFile {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
public PDFToFile(TempFileManager tempFileManager) {
|
||||
this.tempFileManager = tempFileManager;
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -71,15 +75,12 @@ public class PDFToFile {
|
||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
String fileName = "temp.file";
|
||||
String fileName;
|
||||
|
||||
try {
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
|
||||
TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) {
|
||||
inputFile.transferTo(tempInputFile.getFile());
|
||||
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
@@ -88,14 +89,16 @@ public class PDFToFile {
|
||||
"-s",
|
||||
"-noframes",
|
||||
"-c",
|
||||
tempInputFile.toString(),
|
||||
tempInputFile.getAbsolutePath(),
|
||||
pdfBaseName));
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
|
||||
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
||||
.runCommandWithOutputHandling(
|
||||
command, tempOutputDir.getPath().toFile());
|
||||
// Process HTML files to Markdown
|
||||
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
|
||||
File[] outputFiles =
|
||||
Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles());
|
||||
List<File> markdownFiles = new ArrayList<>();
|
||||
|
||||
// Convert HTML files to Markdown
|
||||
@@ -105,7 +108,7 @@ public class PDFToFile {
|
||||
String markdown = htmlToMarkdownConverter.convert(html);
|
||||
|
||||
String mdFileName = outputFile.getName().replace(".html", ".md");
|
||||
File mdFile = new File(tempOutputDir.toFile(), mdFileName);
|
||||
File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName);
|
||||
Files.writeString(mdFile.toPath(), markdown);
|
||||
markdownFiles.add(mdFile);
|
||||
}
|
||||
@@ -142,10 +145,6 @@ public class PDFToFile {
|
||||
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
@@ -153,7 +152,7 @@ public class PDFToFile {
|
||||
|
||||
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -164,19 +163,18 @@ public class PDFToFile {
|
||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
String fileName = "temp.file";
|
||||
String fileName;
|
||||
|
||||
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
||||
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
||||
|
||||
Path tempInputFile = inputFileTemp.getPath();
|
||||
Path tempOutputDir = outputDirTemp.getPath();
|
||||
|
||||
try {
|
||||
// Save the uploaded file to a temporary location
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Prepare the output directory
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
// Run the pdftohtml command with complex output
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
@@ -208,11 +206,6 @@ public class PDFToFile {
|
||||
log.error("Exception writing zip", e);
|
||||
}
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
@@ -223,14 +216,14 @@ public class PDFToFile {
|
||||
MultipartFile inputFile, String outputFormat, String libreOfficeFilter)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Get the original PDF file name without the extension
|
||||
String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||
|
||||
if (originalPdfFileName == null || "".equals(originalPdfFileName.trim())) {
|
||||
if (originalPdfFileName == null || originalPdfFileName.trim().isEmpty()) {
|
||||
originalPdfFileName = "output.pdf";
|
||||
}
|
||||
// Assume file is pdf if no extension
|
||||
@@ -245,19 +238,18 @@ public class PDFToFile {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
String fileName = "temp.file";
|
||||
String fileName;
|
||||
|
||||
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
||||
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
||||
|
||||
Path tempInputFile = inputFileTemp.getPath();
|
||||
Path tempOutputDir = outputDirTemp.getPath();
|
||||
|
||||
try {
|
||||
// Save the uploaded file to a temporary location
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Prepare the output directory
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
// Run the LibreOffice command
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
@@ -308,11 +300,6 @@ public class PDFToFile {
|
||||
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
+144
-132
@@ -8,7 +8,9 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
@@ -18,7 +20,6 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -37,6 +38,7 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.Data;
|
||||
@@ -118,7 +120,7 @@ public class PdfAttachmentHandler {
|
||||
public String getContentType() {
|
||||
return attachment.getContentType() != null
|
||||
? attachment.getContentType()
|
||||
: "application/octet-stream";
|
||||
: MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -256,10 +258,7 @@ public class PdfAttachmentHandler {
|
||||
|
||||
if (contentIdMap.isEmpty()) return htmlContent;
|
||||
|
||||
Pattern cidPattern =
|
||||
Pattern.compile(
|
||||
"(?i)<img[^>]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
Pattern cidPattern = RegexPatternUtils.getInstance().getInlineCidImagePattern();
|
||||
Matcher matcher = cidPattern.matcher(htmlContent);
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
@@ -289,11 +288,15 @@ public class PdfAttachmentHandler {
|
||||
|
||||
public static String formatEmailDate(Date date) {
|
||||
if (date == null) return "";
|
||||
return formatEmailDate(ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
SimpleDateFormat formatter =
|
||||
new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH);
|
||||
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return formatter.format(date);
|
||||
public static String formatEmailDate(ZonedDateTime dateTime) {
|
||||
if (dateTime == null) return "";
|
||||
|
||||
DateTimeFormatter formatter =
|
||||
DateTimeFormatter.ofPattern("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH);
|
||||
return dateTime.withZoneSameInstant(ZoneId.of("UTC")).format(formatter);
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -313,121 +316,20 @@ public class PdfAttachmentHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public static class AttachmentMarkerPositionFinder extends PDFTextStripper {
|
||||
@Getter private final List<MarkerPosition> positions = new ArrayList<>();
|
||||
private int currentPageIndex;
|
||||
protected boolean sortByPosition;
|
||||
private boolean isInAttachmentSection;
|
||||
private boolean attachmentSectionFound;
|
||||
private final StringBuilder currentText = new StringBuilder();
|
||||
|
||||
private static final Pattern ATTACHMENT_SECTION_PATTERN =
|
||||
Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final Pattern FILENAME_PATTERN =
|
||||
Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)");
|
||||
|
||||
public AttachmentMarkerPositionFinder() {
|
||||
super();
|
||||
this.currentPageIndex = 0;
|
||||
this.sortByPosition = false; // Disable sorting to preserve document order
|
||||
this.isInAttachmentSection = false;
|
||||
this.attachmentSectionFound = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(PDDocument document) throws IOException {
|
||||
super.getText(document);
|
||||
|
||||
if (sortByPosition) {
|
||||
positions.sort(
|
||||
(a, b) -> {
|
||||
int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex());
|
||||
if (pageCompare != 0) return pageCompare;
|
||||
return Float.compare(
|
||||
b.getY(), a.getY()); // Descending Y per PDF coordinate system
|
||||
});
|
||||
}
|
||||
|
||||
return ""; // Return empty string as we only need positions
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startPage(PDPage page) throws IOException {
|
||||
super.startPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void endPage(PDPage page) throws IOException {
|
||||
currentPageIndex++;
|
||||
super.endPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeString(String string, List<TextPosition> textPositions)
|
||||
throws IOException {
|
||||
String lowerString = string.toLowerCase();
|
||||
|
||||
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
||||
isInAttachmentSection = true;
|
||||
attachmentSectionFound = true;
|
||||
}
|
||||
|
||||
if (isInAttachmentSection
|
||||
&& (lowerString.contains("</body>")
|
||||
|| lowerString.contains("</html>")
|
||||
|| (attachmentSectionFound
|
||||
&& lowerString.trim().isEmpty()
|
||||
&& string.length() > 50))) {
|
||||
isInAttachmentSection = false;
|
||||
}
|
||||
|
||||
if (isInAttachmentSection) {
|
||||
currentText.append(string);
|
||||
|
||||
for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) {
|
||||
if (i < textPositions.size()) {
|
||||
TextPosition textPosition = textPositions.get(i);
|
||||
|
||||
String filename = extractFilenameAfterMarker(string, i);
|
||||
|
||||
MarkerPosition position =
|
||||
new MarkerPosition(
|
||||
currentPageIndex,
|
||||
textPosition.getXDirAdj(),
|
||||
textPosition.getYDirAdj(),
|
||||
ATTACHMENT_MARKER,
|
||||
filename);
|
||||
positions.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.writeString(string, textPositions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSortByPosition(boolean sortByPosition) {
|
||||
this.sortByPosition = sortByPosition;
|
||||
}
|
||||
|
||||
private String extractFilenameAfterMarker(String text, int markerIndex) {
|
||||
String afterMarker = text.substring(markerIndex + 1);
|
||||
|
||||
Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
|
||||
String[] parts = afterMarker.split("[\\s\\(\\)]+");
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.length() > 3 && part.contains(".")) {
|
||||
return part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
private static String normalizeFilename(String filename) {
|
||||
if (filename == null) return "";
|
||||
String normalized = filename.toLowerCase().trim();
|
||||
normalized =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getWhitespacePattern()
|
||||
.matcher(normalized)
|
||||
.replaceAll(" ");
|
||||
normalized =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getPattern("[^a-zA-Z0-9._-]")
|
||||
.matcher(normalized)
|
||||
.replaceAll("");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static Map<Integer, String> addAttachmentsToDocumentWithMapping(
|
||||
@@ -607,12 +509,122 @@ public class PdfAttachmentHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeFilename(String filename) {
|
||||
if (filename == null) return "";
|
||||
return filename.toLowerCase()
|
||||
.trim()
|
||||
.replaceAll("\\s+", " ")
|
||||
.replaceAll("[^a-zA-Z0-9._-]", "");
|
||||
public static class AttachmentMarkerPositionFinder extends PDFTextStripper {
|
||||
private static final Pattern ATTACHMENT_SECTION_PATTERN =
|
||||
RegexPatternUtils.getInstance().getAttachmentSectionPattern();
|
||||
private static final Pattern FILENAME_PATTERN =
|
||||
RegexPatternUtils.getInstance().getAttachmentFilenamePattern();
|
||||
@Getter private final List<MarkerPosition> positions = new ArrayList<>();
|
||||
private final StringBuilder currentText = new StringBuilder();
|
||||
protected boolean sortByPosition;
|
||||
private int currentPageIndex;
|
||||
private boolean isInAttachmentSection;
|
||||
private boolean attachmentSectionFound;
|
||||
|
||||
public AttachmentMarkerPositionFinder() {
|
||||
super();
|
||||
this.currentPageIndex = 0;
|
||||
this.sortByPosition = false; // Disable sorting to preserve document order
|
||||
this.isInAttachmentSection = false;
|
||||
this.attachmentSectionFound = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(PDDocument document) throws IOException {
|
||||
super.getText(document);
|
||||
|
||||
if (sortByPosition) {
|
||||
positions.sort(
|
||||
(a, b) -> {
|
||||
int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex());
|
||||
if (pageCompare != 0) return pageCompare;
|
||||
return Float.compare(
|
||||
b.getY(), a.getY()); // Descending Y per PDF coordinate system
|
||||
});
|
||||
}
|
||||
|
||||
return ""; // Return empty string as we only need positions
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startPage(PDPage page) throws IOException {
|
||||
super.startPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void endPage(PDPage page) throws IOException {
|
||||
currentPageIndex++;
|
||||
super.endPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeString(String string, List<TextPosition> textPositions)
|
||||
throws IOException {
|
||||
String lowerString = string.toLowerCase();
|
||||
|
||||
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
||||
isInAttachmentSection = true;
|
||||
attachmentSectionFound = true;
|
||||
}
|
||||
|
||||
if (isInAttachmentSection
|
||||
&& (lowerString.contains("</body>")
|
||||
|| lowerString.contains("</html>")
|
||||
|| (attachmentSectionFound
|
||||
&& lowerString.trim().isEmpty()
|
||||
&& string.length() > 50))) {
|
||||
isInAttachmentSection = false;
|
||||
}
|
||||
|
||||
if (isInAttachmentSection) {
|
||||
currentText.append(string);
|
||||
|
||||
for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) {
|
||||
if (i < textPositions.size()) {
|
||||
TextPosition textPosition = textPositions.get(i);
|
||||
|
||||
String filename = extractFilenameAfterMarker(string, i);
|
||||
|
||||
MarkerPosition position =
|
||||
new MarkerPosition(
|
||||
currentPageIndex,
|
||||
textPosition.getXDirAdj(),
|
||||
textPosition.getYDirAdj(),
|
||||
ATTACHMENT_MARKER,
|
||||
filename);
|
||||
positions.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.writeString(string, textPositions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSortByPosition(boolean sortByPosition) {
|
||||
this.sortByPosition = sortByPosition;
|
||||
}
|
||||
|
||||
private String extractFilenameAfterMarker(String text, int markerIndex) {
|
||||
String afterMarker = text.substring(markerIndex + 1);
|
||||
|
||||
Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
|
||||
String[] parts =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getWhitespaceParenthesesSplitPattern()
|
||||
.split(afterMarker);
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.length() > 3 && part.contains(".")) {
|
||||
return part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addAttachmentAnnotationToPageWithMapping(
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
@Slf4j
|
||||
public class PdfToCbrUtils {
|
||||
|
||||
public static byte[] convertPdfToCbr(
|
||||
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
|
||||
throws IOException {
|
||||
|
||||
validatePdfFile(pdfFile);
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IllegalArgumentException("PDF file contains no pages");
|
||||
}
|
||||
|
||||
return createCbrFromPdf(document, dpi);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePdfFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
if (!"pdf".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a PDF");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
|
||||
Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-");
|
||||
List<Path> generatedImages = new ArrayList<>();
|
||||
try {
|
||||
int totalPages = document.getNumberOfPages();
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
try {
|
||||
BufferedImage image =
|
||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||
|
||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||
Path imagePath = tempDir.resolve(imageFilename);
|
||||
|
||||
ImageIO.write(image, "PNG", imagePath.toFile());
|
||||
generatedImages.add(imagePath);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (generatedImages.isEmpty()) {
|
||||
throw new IOException("Failed to render any pages to images for CBR conversion");
|
||||
}
|
||||
|
||||
return createRarArchive(tempDir, generatedImages);
|
||||
} finally {
|
||||
cleanupTempFiles(generatedImages, tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] createRarArchive(Path tempDir, List<Path> images) throws IOException {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("rar");
|
||||
command.add("a");
|
||||
command.add("-m5");
|
||||
command.add("-ep1");
|
||||
|
||||
Path rarFile = tempDir.resolve("output.cbr");
|
||||
command.add(rarFile.getFileName().toString());
|
||||
|
||||
for (Path image : images) {
|
||||
command.add(image.getFileName().toString());
|
||||
}
|
||||
|
||||
ProcessExecutor executor =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP);
|
||||
try {
|
||||
ProcessExecutorResult result =
|
||||
executor.runCommandWithOutputHandling(command, tempDir.toFile());
|
||||
if (result.getRc() != 0) {
|
||||
throw new IOException("RAR command failed: " + result.getMessages());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("RAR command interrupted", e);
|
||||
}
|
||||
|
||||
if (!Files.exists(rarFile)) {
|
||||
throw new IOException("RAR file was not created");
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
fis.transferTo(baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanupTempFiles(List<Path> images, Path tempDir) {
|
||||
for (Path image : images) {
|
||||
try {
|
||||
Files.deleteIfExists(image);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp image file {}: {}", image, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (tempDir != null) {
|
||||
try (var paths = Files.walk(tempDir)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
log.warn(
|
||||
"Failed to delete temp path {}: {}",
|
||||
path,
|
||||
e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to clean up temp directory {}: {}", tempDir, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPdfFile(MultipartFile file) {
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
return "pdf".equals(extension);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Slf4j
|
||||
public class PdfToCbzUtils {
|
||||
|
||||
public static byte[] convertPdfToCbz(
|
||||
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
|
||||
throws IOException {
|
||||
|
||||
validatePdfFile(pdfFile);
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IllegalArgumentException("PDF file contains no pages");
|
||||
}
|
||||
|
||||
return createCbzFromPdf(document, dpi);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePdfFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
if (!"pdf".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a PDF");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
|
||||
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
|
||||
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
|
||||
|
||||
int totalPages = document.getNumberOfPages();
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
try {
|
||||
BufferedImage image =
|
||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||
|
||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||
|
||||
ZipEntry zipEntry = new ZipEntry(imageFilename);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
ImageIO.write(image, "PNG", zipOut);
|
||||
zipOut.closeEntry();
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
}
|
||||
}
|
||||
|
||||
zipOut.finish();
|
||||
return cbzOutputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPdfFile(MultipartFile file) {
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
return "pdf".equals(extension);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@@ -29,19 +31,25 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class PdfUtils {
|
||||
|
||||
public static PDRectangle textToPageSize(String size) {
|
||||
private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance();
|
||||
|
||||
public PDRectangle textToPageSize(String size) {
|
||||
|
||||
switch (size.toUpperCase()) {
|
||||
case "A0" -> {
|
||||
return PDRectangle.A0;
|
||||
@@ -74,7 +82,7 @@ public class PdfUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
||||
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
||||
List<RenderedImage> images = new ArrayList<>();
|
||||
|
||||
for (COSName name : resources.getXObjectNames()) {
|
||||
@@ -91,7 +99,7 @@ public class PdfUtils {
|
||||
return images;
|
||||
}
|
||||
|
||||
public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException {
|
||||
public boolean hasImages(PDDocument document, String pagesToCheck) throws IOException {
|
||||
String[] pageOrderArr = pagesToCheck.split(",");
|
||||
List<Integer> pageList =
|
||||
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||
@@ -106,7 +114,7 @@ public class PdfUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase)
|
||||
public boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase)
|
||||
throws IOException {
|
||||
String[] pageOrderArr = pageNumbersToCheck.split(",");
|
||||
List<Integer> pageList =
|
||||
@@ -122,11 +130,11 @@ public class PdfUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasImagesOnPage(PDPage page) throws IOException {
|
||||
return getAllImages(page.getResources()).size() > 0;
|
||||
public boolean hasImagesOnPage(PDPage page) throws IOException {
|
||||
return !getAllImages(page.getResources()).isEmpty();
|
||||
}
|
||||
|
||||
public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
|
||||
public boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
|
||||
PDFTextStripper textStripper = new PDFTextStripper();
|
||||
PDDocument tempDoc = new PDDocument();
|
||||
tempDoc.addPage(page);
|
||||
@@ -135,14 +143,15 @@ public class PdfUtils {
|
||||
return pageText.contains(phrase);
|
||||
}
|
||||
|
||||
public static byte[] convertFromPdf(
|
||||
public byte[] convertFromPdf(
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
byte[] inputStream,
|
||||
String imageType,
|
||||
ImageType colorType,
|
||||
boolean singleImage,
|
||||
int DPI,
|
||||
String filename)
|
||||
String filename,
|
||||
boolean includeAnnotations)
|
||||
throws IOException, Exception {
|
||||
|
||||
// Validate and limit DPI to prevent excessive memory usage
|
||||
@@ -155,7 +164,8 @@ public class PdfUtils {
|
||||
if (DPI > maxSafeDpi) {
|
||||
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 value {0} exceeds maximum safe limit of {1}. High DPI values can cause"
|
||||
+ " memory issues and crashes. Please use a lower DPI value.",
|
||||
DPI,
|
||||
maxSafeDpi);
|
||||
}
|
||||
@@ -163,6 +173,9 @@ public class PdfUtils {
|
||||
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
if (!includeAnnotations) {
|
||||
pdfRenderer.setAnnotationsFilter(annotation -> false);
|
||||
}
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
// Create a ByteArrayOutputStream to save the image(s) to
|
||||
@@ -192,11 +205,17 @@ public class PdfUtils {
|
||||
.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).",
|
||||
"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;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||
}
|
||||
@@ -237,11 +256,18 @@ public class PdfUtils {
|
||||
.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).",
|
||||
"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;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
pdfSizeImageIndex = i;
|
||||
dimension =
|
||||
@@ -278,11 +304,17 @@ public class PdfUtils {
|
||||
.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).",
|
||||
"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;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,11 +343,16 @@ public class PdfUtils {
|
||||
&& 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).",
|
||||
"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;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, imageType, baosImage);
|
||||
@@ -349,23 +386,38 @@ public class PdfUtils {
|
||||
* @return converted document to PDF-Image
|
||||
* @throws IOException if conversion fails
|
||||
*/
|
||||
public static PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
|
||||
public PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
|
||||
PDDocument imageDocument = new PDDocument();
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
BufferedImage bim;
|
||||
|
||||
// Use global maximum DPI setting, fallback to 300 if not set
|
||||
int renderDpi = 300; // Default fallback
|
||||
ApplicationProperties properties =
|
||||
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
renderDpi = properties.getSystem().getMaxDPI();
|
||||
}
|
||||
|
||||
try {
|
||||
bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||
bim = pdfRenderer.renderImageWithDPI(page, renderDpi, 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.",
|
||||
"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;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
||||
}
|
||||
PDPage originalPage = document.getPage(page);
|
||||
|
||||
@@ -383,8 +435,7 @@ public class PdfUtils {
|
||||
return imageDocument;
|
||||
}
|
||||
|
||||
private static BufferedImage prepareImageForPdfToImage(
|
||||
int maxWidth, int height, String imageType) {
|
||||
private BufferedImage prepareImageForPdfToImage(int maxWidth, int height, String imageType) {
|
||||
BufferedImage combined;
|
||||
if ("png".equalsIgnoreCase(imageType)) {
|
||||
combined = new BufferedImage(maxWidth, height, BufferedImage.TYPE_INT_ARGB);
|
||||
@@ -400,7 +451,7 @@ public class PdfUtils {
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static byte[] imageToPdf(
|
||||
public byte[] imageToPdf(
|
||||
MultipartFile[] files,
|
||||
String fitOption,
|
||||
boolean autoRotate,
|
||||
@@ -431,7 +482,7 @@ public class PdfUtils {
|
||||
ImageProcessingUtils.convertColorType(image, colorType);
|
||||
// Use JPEGFactory if it's JPEG since JPEG is lossy
|
||||
PDImageXObject pdImage =
|
||||
(contentType != null && "image/jpeg".equals(contentType))
|
||||
(contentType != null && MediaType.IMAGE_JPEG_VALUE.equals(contentType))
|
||||
? JPEGFactory.createFromImage(doc, convertedImage)
|
||||
: LosslessFactory.createFromImage(doc, convertedImage);
|
||||
addImageToDocument(doc, pdImage, fitOption, autoRotate);
|
||||
@@ -444,7 +495,7 @@ public class PdfUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void addImageToDocument(
|
||||
public void addImageToDocument(
|
||||
PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate)
|
||||
throws IOException {
|
||||
boolean imageIsLandscape = image.getWidth() > image.getHeight();
|
||||
@@ -494,7 +545,7 @@ public class PdfUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] overlayImage(
|
||||
public byte[] overlayImage(
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
byte[] pdfBytes,
|
||||
byte[] imageBytes,
|
||||
@@ -536,13 +587,16 @@ public class PdfUtils {
|
||||
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
|
||||
throws IOException {
|
||||
PDFTextStripper textStripper = new PDFTextStripper();
|
||||
String pdfText = "";
|
||||
StringBuilder pdfText = new StringBuilder();
|
||||
|
||||
if (pagesToCheck == null || "all".equals(pagesToCheck)) {
|
||||
pdfText = textStripper.getText(pdfDocument);
|
||||
pdfText = new StringBuilder(textStripper.getText(pdfDocument));
|
||||
} else {
|
||||
// remove whitespaces
|
||||
pagesToCheck = pagesToCheck.replaceAll("\\s+", "");
|
||||
// remove whitespaces using cached pattern
|
||||
Pattern whitespacePattern =
|
||||
patternCache.getPattern(RegexPatternUtils.getWhitespaceRegex());
|
||||
Matcher whitespaceMatcher = whitespacePattern.matcher(pagesToCheck);
|
||||
pagesToCheck = whitespaceMatcher.replaceAll("");
|
||||
|
||||
String[] splitPoints = pagesToCheck.split(",");
|
||||
for (String splitPoint : splitPoints) {
|
||||
@@ -555,21 +609,21 @@ public class PdfUtils {
|
||||
for (int i = startPage; i <= endPage; i++) {
|
||||
textStripper.setStartPage(i);
|
||||
textStripper.setEndPage(i);
|
||||
pdfText += textStripper.getText(pdfDocument);
|
||||
pdfText.append(textStripper.getText(pdfDocument));
|
||||
}
|
||||
} else {
|
||||
// Handle individual page
|
||||
int page = Integer.parseInt(splitPoint);
|
||||
textStripper.setStartPage(page);
|
||||
textStripper.setEndPage(page);
|
||||
pdfText += textStripper.getText(pdfDocument);
|
||||
pdfText.append(textStripper.getText(pdfDocument));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pdfDocument.close();
|
||||
|
||||
return pdfText.contains(text);
|
||||
return pdfText.toString().contains(text);
|
||||
}
|
||||
|
||||
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator)
|
||||
@@ -577,16 +631,13 @@ public class PdfUtils {
|
||||
int actualPageCount = pdfDocument.getNumberOfPages();
|
||||
pdfDocument.close();
|
||||
|
||||
switch (comparator.toLowerCase()) {
|
||||
case "greater":
|
||||
return actualPageCount > pageCount;
|
||||
case "equal":
|
||||
return actualPageCount == pageCount;
|
||||
case "less":
|
||||
return actualPageCount < pageCount;
|
||||
default:
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
return switch (comparator.toLowerCase()) {
|
||||
case "greater" -> actualPageCount > pageCount;
|
||||
case "equal" -> actualPageCount == pageCount;
|
||||
case "less" -> actualPageCount < pageCount;
|
||||
default ->
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
};
|
||||
}
|
||||
|
||||
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
@@ -165,7 +167,7 @@ public class ProcessExecutor {
|
||||
semaphore.acquire();
|
||||
try {
|
||||
|
||||
log.info("Running command: " + String.join(" ", command));
|
||||
log.info("Running command: {}", String.join(" ", command));
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
|
||||
// Use the working directory if it's set
|
||||
@@ -250,7 +252,7 @@ public class ProcessExecutor {
|
||||
String outputMessage = String.join("\n", outputLines);
|
||||
messages += outputMessage;
|
||||
if (!liveUpdates) {
|
||||
log.info("Command output:\n" + outputMessage);
|
||||
log.info("Command output:\n{}", outputMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +260,7 @@ public class ProcessExecutor {
|
||||
String errorMessage = String.join("\n", errorLines);
|
||||
messages += errorMessage;
|
||||
if (!liveUpdates) {
|
||||
log.warn("Command error output:\n" + errorMessage);
|
||||
log.warn("Command error output:\n{}", errorMessage);
|
||||
}
|
||||
if (exitCode != 0) {
|
||||
if (isQpdf && exitCode == 3) {
|
||||
@@ -303,6 +305,8 @@ public class ProcessExecutor {
|
||||
OCR_MY_PDF
|
||||
}
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class ProcessExecutorResult {
|
||||
int rc;
|
||||
String messages;
|
||||
@@ -311,21 +315,5 @@ public class ProcessExecutor {
|
||||
this.rc = rc;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public int getRc() {
|
||||
return rc;
|
||||
}
|
||||
|
||||
public void setRc(int rc) {
|
||||
this.rc = rc;
|
||||
}
|
||||
|
||||
public String getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(String messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public final class RegexPatternUtils {
|
||||
|
||||
private static final RegexPatternUtils INSTANCE = new RegexPatternUtils();
|
||||
private final ConcurrentHashMap<PatternKey, Pattern> patternCache = new ConcurrentHashMap<>();
|
||||
|
||||
private static final String WHITESPACE_REGEX = "\\s++";
|
||||
private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$";
|
||||
|
||||
private RegexPatternUtils() {
|
||||
super();
|
||||
// Initialize with commonly used patterns for immediate availability
|
||||
precompileCommonPatterns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of the pattern cache.
|
||||
*
|
||||
* @return the singleton RegexPatternCache instance
|
||||
*/
|
||||
public static RegexPatternUtils getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a compiled pattern from cache, compiling and caching if not present.
|
||||
*
|
||||
* <p>This method is thread-safe and uses lazy initialization. Multiple threads calling with the
|
||||
* same regex will result in only one compilation, with all threads receiving the same cached
|
||||
* Pattern instance.
|
||||
*
|
||||
* <p>Performance: first call compiles and caches (expensive), subsequent calls return cached
|
||||
* pattern (fast O(1) lookup).
|
||||
*
|
||||
* @param regex the regular expression string to compile
|
||||
* @return compiled Pattern object, never null
|
||||
* @throws PatternSyntaxException if the regex syntax is invalid
|
||||
* @throws IllegalArgumentException if regex is null
|
||||
*/
|
||||
public Pattern getPattern(String regex) {
|
||||
if (regex == null) {
|
||||
throw new IllegalArgumentException("Regex pattern cannot be null");
|
||||
}
|
||||
|
||||
return patternCache.computeIfAbsent(new PatternKey(regex, 0), this::compilePattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a compiled pattern with flags.
|
||||
*
|
||||
* <p>Patterns with different flags are cached separately using a composite key. Common flags
|
||||
* include:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link Pattern#CASE_INSENSITIVE} - ignore case differences
|
||||
* <li>{@link Pattern#MULTILINE} - ^ and $ match line boundaries
|
||||
* <li>{@link Pattern#DOTALL} - . matches any character including newlines
|
||||
* </ul>
|
||||
*
|
||||
* @param regex the regular expression string
|
||||
* @param flags pattern flags (e.g., Pattern.CASE_INSENSITIVE)
|
||||
* @return compiled Pattern object with specified flags
|
||||
* @throws PatternSyntaxException if the regex syntax is invalid
|
||||
* @throws IllegalArgumentException if regex is null
|
||||
*/
|
||||
public Pattern getPattern(String regex, int flags) {
|
||||
if (regex == null) {
|
||||
throw new IllegalArgumentException("Regex pattern cannot be null");
|
||||
}
|
||||
|
||||
return patternCache.computeIfAbsent(new PatternKey(regex, flags), this::compilePattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a pattern is already cached.
|
||||
*
|
||||
* @param regex the regular expression string
|
||||
* @return true if pattern is cached, false otherwise
|
||||
*/
|
||||
public boolean isCached(String regex) {
|
||||
return isCached(regex, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a pattern with flags is already cached.
|
||||
*
|
||||
* @param regex the regular expression string
|
||||
* @param flags pattern flags
|
||||
* @return true if pattern is cached, false otherwise
|
||||
*/
|
||||
public boolean isCached(String regex, int flags) {
|
||||
return regex != null && patternCache.containsKey(new PatternKey(regex, flags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current cache size (number of cached patterns). Useful for monitoring and debugging.
|
||||
*
|
||||
* @return number of patterns currently cached
|
||||
*/
|
||||
public int getCacheSize() {
|
||||
return patternCache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached patterns. Use sparingly as it forces recompilation of all patterns. Mainly
|
||||
* useful for testing or memory cleanup in long-running applications.
|
||||
*/
|
||||
public void clearCache() {
|
||||
patternCache.clear();
|
||||
log.debug("Regex pattern cache cleared");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific pattern from cache.
|
||||
*
|
||||
* @param regex the regular expression string to remove
|
||||
* @return true if pattern was cached and removed, false otherwise
|
||||
*/
|
||||
public boolean removeFromCache(String regex) {
|
||||
return removeFromCache(regex, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific pattern with flags from cache.
|
||||
*
|
||||
* @param regex the regular expression string to remove
|
||||
* @param flags pattern flags
|
||||
* @return true if pattern was cached and removed, false otherwise
|
||||
*/
|
||||
public boolean removeFromCache(String regex, int flags) {
|
||||
if (regex == null) {
|
||||
return false;
|
||||
}
|
||||
PatternKey key = new PatternKey(regex, flags);
|
||||
boolean removed = patternCache.remove(key) != null;
|
||||
if (removed) {
|
||||
log.debug("Removed regex pattern from cache: {} (flags: {})", regex, flags);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to compile a pattern and handle errors consistently.
|
||||
*
|
||||
* @return compiled Pattern
|
||||
* @throws PatternSyntaxException if regex is invalid
|
||||
*/
|
||||
private Pattern compilePattern(PatternKey key) {
|
||||
String regex = key.regex;
|
||||
int flags = key.flags;
|
||||
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(regex, flags);
|
||||
log.trace("Compiled and cached regex pattern with flags {}: {}", flags, regex);
|
||||
return pattern;
|
||||
} catch (PatternSyntaxException e) {
|
||||
log.error(
|
||||
"Invalid regex pattern: '{}' with flags {} - {}", regex, flags, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getWhitespaceRegex() {
|
||||
return WHITESPACE_REGEX;
|
||||
}
|
||||
|
||||
/** Creates a case-insensitive pattern for text searching */
|
||||
public Pattern createSearchPattern(String regex, boolean caseInsensitive) {
|
||||
int flags = caseInsensitive ? (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) : 0;
|
||||
return getPattern(regex, flags);
|
||||
}
|
||||
|
||||
/** Pattern for matching trailing slashes (e.g., "/path/to/dir///") */
|
||||
public Pattern getTrailingSlashesPattern() {
|
||||
return getPattern("/+$");
|
||||
}
|
||||
|
||||
/** Pattern for removing drive letters from paths */
|
||||
public Pattern getDriveLetterPattern() {
|
||||
return getPattern("^[a-zA-Z]:[\\\\/]+");
|
||||
}
|
||||
|
||||
/** Pattern for removing leading slashes from paths */
|
||||
public Pattern getLeadingSlashesPattern() {
|
||||
return getPattern("^[\\\\/]+");
|
||||
}
|
||||
|
||||
/** Pattern for matching backslashes */
|
||||
public Pattern getBackslashPattern() {
|
||||
return getPattern("\\\\");
|
||||
}
|
||||
|
||||
/** Pattern for sanitizing filenames by removing problematic characters */
|
||||
public Pattern getSafeFilenamePattern() {
|
||||
return getPattern("[/\\\\?%*:|\"<>]");
|
||||
}
|
||||
|
||||
/** Pattern for sanitizing filenames (keeps only alphanumeric) */
|
||||
public Pattern getFilenameSafePattern() {
|
||||
return getPattern("[^a-zA-Z0-9]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern for replacing non-alphanumeric characters with underscore (explicit underscore
|
||||
* variant)
|
||||
*/
|
||||
public Pattern getNonAlnumUnderscorePattern() {
|
||||
return getPattern("[^A-Za-z0-9_]");
|
||||
}
|
||||
|
||||
/** Pattern for collapsing multiple underscores */
|
||||
public Pattern getMultipleUnderscoresPattern() {
|
||||
return getPattern("_+");
|
||||
}
|
||||
|
||||
/** Pattern for trimming leading underscores */
|
||||
public Pattern getLeadingUnderscoresPattern() {
|
||||
return getPattern("^_+");
|
||||
}
|
||||
|
||||
/** Pattern for trimming trailing underscores */
|
||||
public Pattern getTrailingUnderscoresPattern() {
|
||||
return getPattern("_+$");
|
||||
}
|
||||
|
||||
/** Pattern for matching upload/download paths (case insensitive) */
|
||||
public Pattern getUploadDownloadPathPattern() {
|
||||
return getPattern("(?i).*/(upload|download)/.*");
|
||||
}
|
||||
|
||||
/** Pattern for matching one or more whitespace characters */
|
||||
public Pattern getWhitespacePattern() {
|
||||
return getPattern("\\s+");
|
||||
}
|
||||
|
||||
/** Pattern for matching newlines (Windows and Unix style) */
|
||||
public Pattern getNewlinesPattern() {
|
||||
return getPattern("\\r?\\n");
|
||||
}
|
||||
|
||||
/** Pattern for splitting on newlines (Windows and Unix style) */
|
||||
public Pattern getNewlineSplitPattern() {
|
||||
return getPattern("\\r?\\n");
|
||||
}
|
||||
|
||||
/** Pattern for splitting text into words */
|
||||
public Pattern getWordSplitPattern() {
|
||||
return getPattern("\\s+");
|
||||
}
|
||||
|
||||
/** Pattern for removing carriage returns */
|
||||
public Pattern getCarriageReturnPattern() {
|
||||
return getPattern("\\r");
|
||||
}
|
||||
|
||||
/** Pattern for matching newline characters */
|
||||
public Pattern getNewlineCharsPattern() {
|
||||
return getPattern("[\n\r]");
|
||||
}
|
||||
|
||||
/** Pattern for multi-format newline splitting (Windows, Mac, Unix) */
|
||||
public Pattern getMultiFormatNewlinePattern() {
|
||||
return getPattern("\r\n|\r|\n");
|
||||
}
|
||||
|
||||
/** Pattern for encoded payload newline removal */
|
||||
public Pattern getEncodedPayloadNewlinePattern() {
|
||||
return getPattern("\\r?\\n");
|
||||
}
|
||||
|
||||
/** Pattern for escaped newlines in watermark text */
|
||||
public Pattern getEscapedNewlinePattern() {
|
||||
return getPattern("\\\\n");
|
||||
}
|
||||
|
||||
/** Pattern for input sanitization (allows only alphanumeric and spaces) */
|
||||
public Pattern getInputSanitizePattern() {
|
||||
return getPattern("[^a-zA-Z0-9 ]");
|
||||
}
|
||||
|
||||
/** Pattern for removing angle brackets */
|
||||
public Pattern getAngleBracketsPattern() {
|
||||
return getPattern("[<>]");
|
||||
}
|
||||
|
||||
/** Pattern for removing leading and trailing quotes */
|
||||
public Pattern getQuotesRemovalPattern() {
|
||||
return getPattern("^\"|\"$");
|
||||
}
|
||||
|
||||
/** Pattern for plus signs (URL encoding replacement) */
|
||||
public Pattern getPlusSignPattern() {
|
||||
return getPattern("\\+");
|
||||
}
|
||||
|
||||
/** Pattern for username validation */
|
||||
public Pattern getUsernameValidationPattern() {
|
||||
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
|
||||
}
|
||||
|
||||
public static String getExtensionRegex() {
|
||||
return EXTENSION_REGEX;
|
||||
}
|
||||
|
||||
/** Pattern for extracting non-numeric characters */
|
||||
public Pattern getNumericExtractionPattern() {
|
||||
return getPattern("\\D");
|
||||
}
|
||||
|
||||
/** Pattern for removing non-digit/dot characters (for timeout parsing) */
|
||||
public Pattern getNonDigitDotPattern() {
|
||||
return getPattern("[^\\d.]");
|
||||
}
|
||||
|
||||
/** Pattern for matching digit/dot characters (for timeout parsing) */
|
||||
public Pattern getDigitDotPattern() {
|
||||
return getPattern("[\\d.]");
|
||||
}
|
||||
|
||||
/** Pattern for detecting strings containing digits */
|
||||
public Pattern getContainsDigitsPattern() {
|
||||
return getPattern(".*\\d+.*");
|
||||
}
|
||||
|
||||
/** Pattern for matching 1-3 digit numbers */
|
||||
public Pattern getNumberRangePattern() {
|
||||
return getPattern("[1-9][0-9]{0,2}");
|
||||
}
|
||||
|
||||
/** Pattern for validating mathematical expressions */
|
||||
public Pattern getMathExpressionPattern() {
|
||||
return getPattern("[0-9n+\\-*/() ]+");
|
||||
}
|
||||
|
||||
/** Pattern for adding multiplication between numbers and 'n' */
|
||||
public Pattern getNumberBeforeNPattern() {
|
||||
return getPattern("(\\d)n");
|
||||
}
|
||||
|
||||
/** Pattern for detecting consecutive 'n' characters */
|
||||
public Pattern getConsecutiveNPattern() {
|
||||
return getPattern(".*n{2,}.*");
|
||||
}
|
||||
|
||||
/** Pattern for replacing consecutive 'n' characters */
|
||||
public Pattern getConsecutiveNReplacementPattern() {
|
||||
return getPattern("(?<!n)n{2}");
|
||||
}
|
||||
|
||||
/** Pattern for validating HTTP/HTTPS URLs */
|
||||
public Pattern getHttpUrlPattern() {
|
||||
return getPattern("^https?://.*");
|
||||
}
|
||||
|
||||
/** Pattern for matching URLs in text for link creation */
|
||||
public Pattern getUrlLinkPattern() {
|
||||
return getPattern("(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)");
|
||||
}
|
||||
|
||||
/** Pattern for matching email addresses in text for link creation */
|
||||
public Pattern getEmailLinkPattern() {
|
||||
return getPattern("([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})");
|
||||
}
|
||||
|
||||
/** Pattern for removing script tags from HTML */
|
||||
public Pattern getScriptTagPattern() {
|
||||
return getPattern("(?i)<script[^>]*>.*?</script>");
|
||||
}
|
||||
|
||||
/** Pattern for removing style tags from HTML */
|
||||
public Pattern getStyleTagPattern() {
|
||||
return getPattern("(?i)<style[^>]*>.*?</style>");
|
||||
}
|
||||
|
||||
/** Pattern for removing fixed position CSS */
|
||||
public Pattern getFixedPositionCssPattern() {
|
||||
return getPattern("(?i)\\s*position\\s*:\\s*fixed[^;]*;?");
|
||||
}
|
||||
|
||||
/** Pattern for removing absolute position CSS */
|
||||
public Pattern getAbsolutePositionCssPattern() {
|
||||
return getPattern("(?i)\\s*position\\s*:\\s*absolute[^;]*;?");
|
||||
}
|
||||
|
||||
/** Pattern for matching size unit suffixes (KB, MB, GB, etc.) */
|
||||
public Pattern getSizeUnitPattern() {
|
||||
return getPattern("[KMGkmg][Bb]");
|
||||
}
|
||||
|
||||
/** Pattern for system temp file type 1 */
|
||||
public Pattern getSystemTempFile1Pattern() {
|
||||
return getPattern("lu\\d+[a-z0-9]*\\.tmp");
|
||||
}
|
||||
|
||||
/** Pattern for system temp file type 2 (OCR processes) */
|
||||
public Pattern getSystemTempFile2Pattern() {
|
||||
return getPattern("ocr_process\\d+");
|
||||
}
|
||||
|
||||
/** Pattern for splitting on whitespace and parentheses */
|
||||
public Pattern getWhitespaceParenthesesSplitPattern() {
|
||||
return getPattern("[\\s\\(\\)]+");
|
||||
}
|
||||
|
||||
/** Pattern for MIME header whitespace cleanup before encoded sequences */
|
||||
public Pattern getMimeHeaderWhitespacePattern() {
|
||||
return getPattern("\\s+(?==\\?)");
|
||||
}
|
||||
|
||||
/** Pattern for font name validation (6 uppercase letters + plus + rest) */
|
||||
public Pattern getFontNamePattern() {
|
||||
return getPattern("^[A-Z]{6}\\+.*");
|
||||
}
|
||||
|
||||
/** Pattern for matching access="readOnly" attribute in XFA XML (with optional whitespace) */
|
||||
public Pattern getAccessReadOnlyPattern() {
|
||||
return getPattern("access\\s*=\\s*\"readOnly\"");
|
||||
}
|
||||
|
||||
/** Pattern for matching MIME encoded-word headers (RFC 2047) Example: =?charset?B?encoded?= */
|
||||
public Pattern getMimeEncodedWordPattern() {
|
||||
return getPattern("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
||||
}
|
||||
|
||||
/** Pattern for matching inline CID images in HTML (case-insensitive) */
|
||||
public Pattern getInlineCidImagePattern() {
|
||||
return getPattern(
|
||||
"(?i)<img[^>]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/** Pattern for matching image file extensions (case-insensitive) */
|
||||
public Pattern getImageFilePattern() {
|
||||
return getPattern(".*\\.(jpg|jpeg|png|gif|bmp|webp)$", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/** Pattern for matching attachment section headers (case-insensitive) */
|
||||
public Pattern getAttachmentSectionPattern() {
|
||||
return getPattern("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/** Pattern for matching filenames in attachment markers */
|
||||
public Pattern getAttachmentFilenamePattern() {
|
||||
return getPattern("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)");
|
||||
}
|
||||
|
||||
/** Pattern for matching pdfaid:part attribute in XMP metadata */
|
||||
public Pattern getPdfAidPartPattern() {
|
||||
return getPattern("pdfaid:part[\"\\s]*=[\"\\s]*([0-9]+)");
|
||||
}
|
||||
|
||||
/** Pattern for matching pdfaid:conformance attribute in XMP metadata */
|
||||
public Pattern getPdfAidConformancePattern() {
|
||||
return getPattern("pdfaid:conformance[\"\\s]*=[\"\\s]*([A-Za-z]+)");
|
||||
}
|
||||
|
||||
/** Pattern for matching slash in page mode description */
|
||||
public Pattern getPageModePattern() {
|
||||
return getPattern("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-compile commonly used patterns for immediate availability. This eliminates first-call
|
||||
* compilation overhead for frequent patterns.
|
||||
*/
|
||||
private void precompileCommonPatterns() {
|
||||
getPattern("\\.(?:[^.]*+)?$"); // Extension removal - possessive, optional, anchored
|
||||
getPattern("\\.[^.]+$"); // Simple extension match - anchored
|
||||
|
||||
getPattern("\\s+"); // One or more whitespace
|
||||
getPattern("\\s*"); // Zero or more whitespace
|
||||
|
||||
getPattern("/+$"); // Trailing slashes
|
||||
getPattern("\\D"); // Non-numeric characters
|
||||
getPattern("[/\\\\?%*:|\"<>]"); // Unsafe filename characters
|
||||
getPattern("[^a-zA-Z0-9 ]"); // Input sanitization
|
||||
getPattern("[^a-zA-Z0-9]"); // Filename sanitization
|
||||
// API doc patterns
|
||||
getPattern("Output:(\\w+)"); // precompiled single-escaped for runtime regex \w
|
||||
getPattern("Input:(\\w+)");
|
||||
getPattern("Type:(\\w+)");
|
||||
log.debug("Pre-compiled {} common regex patterns", patternCache.size());
|
||||
}
|
||||
|
||||
/** Pattern for email validation */
|
||||
public Pattern getEmailValidationPattern() {
|
||||
return getPattern(
|
||||
"^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*(?:\\.[A-Za-z]{2,})$");
|
||||
}
|
||||
|
||||
/* Pattern for matching Output:<TYPE> in API descriptions */
|
||||
public Pattern getApiDocOutputTypePattern() {
|
||||
return getPattern("Output:(\\w+)");
|
||||
}
|
||||
|
||||
/* Pattern for matching Input:<TYPE> in API descriptions */
|
||||
public Pattern getApiDocInputTypePattern() {
|
||||
return getPattern("Input:(\\w+)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern for matching Type:<CODE> in API descriptions
|
||||
*/
|
||||
public Pattern getApiDocTypePattern() {
|
||||
return getPattern("Type:(\\w+)");
|
||||
}
|
||||
|
||||
/* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */
|
||||
public Pattern getFileExtensionValidationPattern() {
|
||||
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
private record PatternKey(String regex, int flags) {
|
||||
// Record automatically provides equals, hashCode, and toString
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,10 @@ public class RequestUriUtils {
|
||||
|| requestURI.startsWith(contextPath + "/images/")
|
||||
|| requestURI.startsWith(contextPath + "/public/")
|
||||
|| requestURI.startsWith(contextPath + "/pdfjs/")
|
||||
|| requestURI.startsWith(contextPath + "/pdfjs-legacy/")
|
||||
|| requestURI.startsWith(contextPath + "/login")
|
||||
|| requestURI.startsWith(contextPath + "/error")
|
||||
|| requestURI.startsWith(contextPath + "/favicon")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -14,17 +15,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class TempFile implements AutoCloseable {
|
||||
|
||||
private final TempFileManager manager;
|
||||
private final File file;
|
||||
@Getter 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();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -24,8 +25,22 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class TempFileRegistry {
|
||||
|
||||
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* -- GETTER -- Get all registered third-party temporary files.
|
||||
*
|
||||
* @return Set of third-party file paths
|
||||
*/
|
||||
@Getter
|
||||
private final Set<Path> thirdPartyTempFiles =
|
||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
/**
|
||||
* -- GETTER -- Get all registered temporary directories.
|
||||
*
|
||||
* @return Set of temporary directory paths
|
||||
*/
|
||||
@Getter
|
||||
private final Set<Path> tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
/**
|
||||
@@ -133,24 +148,6 @@ public class TempFileRegistry {
|
||||
.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.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,8 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -11,9 +13,13 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class WebResponseUtils {
|
||||
|
||||
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||
@@ -44,7 +50,10 @@ public class WebResponseUtils {
|
||||
headers.setContentType(mediaType);
|
||||
headers.setContentLength(bytes.length);
|
||||
String encodedDocName =
|
||||
URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
RegexPatternUtils.getInstance()
|
||||
.getPlusSignPattern()
|
||||
.matcher(URLEncoder.encode(docName, StandardCharsets.UTF_8))
|
||||
.replaceAll("%20");
|
||||
headers.setContentDispositionFormData("attachment", encodedDocName);
|
||||
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
||||
}
|
||||
@@ -64,4 +73,59 @@ public class WebResponseUtils {
|
||||
|
||||
return baosToWebResponse(baos, docName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a File to a web response (PDF default).
|
||||
*
|
||||
* @param outputTempFile The temporary file to be sent as a response.
|
||||
* @param docName The name of the document.
|
||||
* @return A ResponseEntity containing the file as a resource.
|
||||
*/
|
||||
public static ResponseEntity<StreamingResponseBody> pdfFileToWebResponse(
|
||||
TempFile outputTempFile, String docName) throws IOException {
|
||||
return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_PDF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a File to a web response (ZIP default).
|
||||
*
|
||||
* @param outputTempFile The temporary file to be sent as a response.
|
||||
* @param docName The name of the document.
|
||||
* @return A ResponseEntity containing the file as a resource.
|
||||
*/
|
||||
public static ResponseEntity<StreamingResponseBody> zipFileToWebResponse(
|
||||
TempFile outputTempFile, String docName) throws IOException {
|
||||
return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a File to a web response with explicit media type (e.g., ZIP).
|
||||
*
|
||||
* @param outputTempFile The temporary file to be sent as a response.
|
||||
* @param docName The name of the document.
|
||||
* @param mediaType The content type to set on the response.
|
||||
* @return A ResponseEntity containing the file as a resource.
|
||||
*/
|
||||
public static ResponseEntity<StreamingResponseBody> fileToWebResponse(
|
||||
TempFile outputTempFile, String docName, MediaType mediaType) throws IOException {
|
||||
|
||||
Path path = outputTempFile.getFile().toPath().normalize();
|
||||
long len = Files.size(path);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(mediaType);
|
||||
headers.setContentLength(len);
|
||||
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + docName + "\"");
|
||||
|
||||
StreamingResponseBody body =
|
||||
os -> {
|
||||
try (os) {
|
||||
Files.copy(path, os);
|
||||
os.flush();
|
||||
} finally {
|
||||
outputTempFile.close();
|
||||
}
|
||||
};
|
||||
|
||||
return new ResponseEntity<>(body, headers, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package stirling.software.common.util.misc;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@Slf4j
|
||||
public class ColorSpaceConversionStrategy extends ReplaceAndInvertColorStrategy {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
public ColorSpaceConversionStrategy(
|
||||
MultipartFile file,
|
||||
ReplaceAndInvert replaceAndInvert,
|
||||
TempFileManager tempFileManager) {
|
||||
super(file, replaceAndInvert);
|
||||
this.tempFileManager = tempFileManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStreamResource replace() throws IOException {
|
||||
try (TempFile tempInput = new TempFile(tempFileManager, ".pdf");
|
||||
TempFile tempOutput = new TempFile(tempFileManager, ".pdf")) {
|
||||
|
||||
Path tempInputFile = tempInput.getPath();
|
||||
Path tempOutputFile = tempOutput.getPath();
|
||||
|
||||
Files.write(tempInputFile, getFileInput().getBytes());
|
||||
|
||||
log.info("Starting CMYK color space conversion");
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("gs");
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-dCompatibilityLevel=1.5");
|
||||
command.add("-dPDFSETTINGS=/prepress");
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dQUIET");
|
||||
command.add("-dBATCH");
|
||||
command.add("-sProcessColorModel=DeviceCMYK");
|
||||
command.add("-sColorConversionStrategy=CMYK");
|
||||
command.add("-sColorConversionStrategyForImages=CMYK");
|
||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
log.debug("Executing Ghostscript command for CMYK conversion: {}", command);
|
||||
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
if (result.getRc() != 0) {
|
||||
log.error(
|
||||
"Ghostscript CMYK conversion failed with return code: {}. Output: {}",
|
||||
result.getRc(),
|
||||
result.getMessages());
|
||||
throw new IOException(
|
||||
"CMYK color space conversion failed: " + result.getMessages());
|
||||
}
|
||||
|
||||
log.info("CMYK color space conversion completed successfully");
|
||||
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
return new InputStreamResource(new ByteArrayInputStream(pdfBytes));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("CMYK color space conversion failed", e);
|
||||
throw new IOException(
|
||||
"Failed to convert PDF to CMYK color space: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-2
@@ -19,7 +19,10 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
||||
|
||||
@@ -44,8 +47,25 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
||||
// Render each page and invert colors
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
for (int page = 0; page < document.getNumberOfPages(); page++) {
|
||||
BufferedImage image =
|
||||
pdfRenderer.renderImageWithDPI(page, 300); // Render page at 300 DPI
|
||||
BufferedImage image;
|
||||
|
||||
// Use global maximum DPI setting, fallback to 300 if not set
|
||||
int renderDpi = 300; // Default fallback
|
||||
ApplicationProperties properties =
|
||||
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
renderDpi = properties.getSystem().getMaxDPI();
|
||||
}
|
||||
|
||||
try {
|
||||
image =
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
page, renderDpi); // Render page with global DPI setting
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
||||
}
|
||||
|
||||
// Invert the colors
|
||||
invertImageColors(image);
|
||||
|
||||
+1
-5
@@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import stirling.software.common.aop.AutoJobAspect;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
import stirling.software.common.service.JobQueue;
|
||||
@@ -44,8 +43,6 @@ class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
@Mock private HttpServletRequest request;
|
||||
|
||||
@Mock private FileOrUploadService fileOrUploadService;
|
||||
|
||||
@Mock private FileStorage fileStorage;
|
||||
|
||||
@Mock private ResourceMonitor resourceMonitor;
|
||||
@@ -54,8 +51,7 @@ class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
autoJobAspect =
|
||||
new AutoJobAspect(jobExecutorService, request, fileOrUploadService, fileStorage);
|
||||
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
|
||||
}
|
||||
|
||||
@Mock private ProceedingJoinPoint joinPoint;
|
||||
|
||||
+9
-6
@@ -2,9 +2,11 @@ package stirling.software.common.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -31,22 +33,23 @@ class ApplicationPropertiesLogicTest {
|
||||
|
||||
@Test
|
||||
void tempFileManagement_defaults_and_overrides() {
|
||||
Function<String, String> normalize = s -> Paths.get(s).normalize().toString();
|
||||
ApplicationProperties.TempFileManagement tfm =
|
||||
new ApplicationProperties.TempFileManagement();
|
||||
|
||||
String expectedBase =
|
||||
java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
|
||||
+ "/stirling-pdf";
|
||||
Paths.get(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf")
|
||||
.toString();
|
||||
assertEquals(expectedBase, tfm.getBaseTmpDir());
|
||||
|
||||
String expectedLibre = expectedBase + "/libreoffice";
|
||||
String expectedLibre = Paths.get(expectedBase, "libreoffice").toString();
|
||||
assertEquals(expectedLibre, tfm.getLibreofficeDir());
|
||||
|
||||
tfm.setBaseTmpDir("/custom/base");
|
||||
assertEquals("/custom/base", tfm.getBaseTmpDir());
|
||||
assertEquals("/custom/base", normalize.apply(tfm.getBaseTmpDir()));
|
||||
|
||||
tfm.setLibreofficeDir("/opt/libre");
|
||||
assertEquals("/opt/libre", tfm.getLibreofficeDir());
|
||||
assertEquals("/opt/libre", normalize.apply(tfm.getLibreofficeDir()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -179,7 +182,7 @@ class ApplicationPropertiesLogicTest {
|
||||
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Deprecated(since = "0.45.0")
|
||||
@Test
|
||||
void enterprise_metadata_defaults() {
|
||||
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@ import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
@@ -23,7 +24,7 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
server.enqueue(
|
||||
new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.addHeader("Content-Type", "application/xml")
|
||||
.addHeader("Content-Type", MediaType.APPLICATION_XML_VALUE)
|
||||
.setBody("<EntityDescriptor/>"));
|
||||
server.start();
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
public class FileInfoTest {
|
||||
|
||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||
@CsvSource({
|
||||
"0, '0 Bytes'",
|
||||
"1023, '1023 Bytes'",
|
||||
"1024, '1.00 KB'",
|
||||
"1048575, '1024.00 KB'", // Do we really want this as result?
|
||||
"1048576, '1.00 MB'",
|
||||
"1073741823, '1024.00 MB'", // Do we really want this as result?
|
||||
"1073741824, '1.00 GB'"
|
||||
})
|
||||
void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) {
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
File.separator
|
||||
+ "path"
|
||||
+ File.separator
|
||||
+ "to"
|
||||
+ File.separator
|
||||
+ "example.txt",
|
||||
LocalDateTime.now(),
|
||||
fileSize,
|
||||
LocalDateTime.now().minusDays(1));
|
||||
|
||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFilePathAsPath() {
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"test.pdf",
|
||||
File.separator + "tmp" + File.separator + "test.pdf",
|
||||
LocalDateTime.now(),
|
||||
1234,
|
||||
LocalDateTime.now().minusDays(2));
|
||||
assertEquals(
|
||||
File.separator + "tmp" + File.separator + "test.pdf",
|
||||
fileInfo.getFilePathAsPath().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFormattedModificationDate() {
|
||||
LocalDateTime modDate = LocalDateTime.of(2024, 6, 1, 15, 30, 45);
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"file.txt",
|
||||
File.separator + "file.txt",
|
||||
modDate,
|
||||
100,
|
||||
LocalDateTime.of(2024, 5, 31, 10, 0, 0));
|
||||
assertEquals("2024-06-01 15:30:45", fileInfo.getFormattedModificationDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFormattedCreationDate() {
|
||||
LocalDateTime creationDate = LocalDateTime.of(2023, 12, 25, 8, 15, 0);
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"holiday.txt",
|
||||
File.separator + "holiday.txt",
|
||||
LocalDateTime.of(2024, 1, 1, 0, 0, 0),
|
||||
500,
|
||||
creationDate);
|
||||
assertEquals("2023-12-25 08:15:00", fileInfo.getFormattedCreationDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGettersAndSetters() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"doc.pdf",
|
||||
File.separator + "docs" + File.separator + "doc.pdf",
|
||||
now,
|
||||
2048,
|
||||
now.minusDays(1));
|
||||
// Test getters
|
||||
assertEquals("doc.pdf", fileInfo.getFileName());
|
||||
assertEquals(File.separator + "docs" + File.separator + "doc.pdf", fileInfo.getFilePath());
|
||||
assertEquals(now, fileInfo.getModificationDate());
|
||||
assertEquals(2048, fileInfo.getFileSize());
|
||||
assertEquals(now.minusDays(1), fileInfo.getCreationDate());
|
||||
|
||||
// Test setters
|
||||
fileInfo.setFileName("new.pdf");
|
||||
fileInfo.setFilePath(File.separator + "new" + File.separator + "new.pdf");
|
||||
fileInfo.setModificationDate(now.plusDays(1));
|
||||
fileInfo.setFileSize(4096);
|
||||
fileInfo.setCreationDate(now.minusDays(2));
|
||||
|
||||
assertEquals("new.pdf", fileInfo.getFileName());
|
||||
assertEquals(File.separator + "new" + File.separator + "new.pdf", fileInfo.getFilePath());
|
||||
assertEquals(now.plusDays(1), fileInfo.getModificationDate());
|
||||
assertEquals(4096, fileInfo.getFileSize());
|
||||
assertEquals(now.minusDays(2), fileInfo.getCreationDate());
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class InputStreamTemplateResourceTest {
|
||||
|
||||
@Test
|
||||
void gettersReturnProvidedFields() {
|
||||
byte[] data = {1, 2, 3};
|
||||
InputStream is = new ByteArrayInputStream(data);
|
||||
String encoding = "UTF-8";
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, encoding);
|
||||
|
||||
assertSame(is, resource.getInputStream());
|
||||
assertEquals(encoding, resource.getCharacterEncoding());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldsAreFinal() throws NoSuchFieldException {
|
||||
Field inputStreamField = InputStreamTemplateResource.class.getDeclaredField("inputStream");
|
||||
Field characterEncodingField =
|
||||
InputStreamTemplateResource.class.getDeclaredField("characterEncoding");
|
||||
|
||||
assertTrue(Modifier.isFinal(inputStreamField.getModifiers()));
|
||||
assertTrue(Modifier.isFinal(characterEncodingField.getModifiers()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSetterMethodsPresent() {
|
||||
long setterCount =
|
||||
Arrays.stream(InputStreamTemplateResource.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().startsWith("set"))
|
||||
.count();
|
||||
|
||||
assertEquals(0, setterCount, "InputStreamTemplateResource should not have setter methods");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readerReturnsCorrectContent() throws Exception {
|
||||
String content = "Hello, world!";
|
||||
InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
|
||||
try (Reader reader = resource.reader()) {
|
||||
char[] buffer = new char[content.length()];
|
||||
int read = reader.read(buffer);
|
||||
assertEquals(content.length(), read);
|
||||
assertEquals(content, new String(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void relativeThrowsUnsupportedOperationException() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertThrows(UnsupportedOperationException.class, () -> resource.relative("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDescriptionReturnsExpectedString() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertEquals("InputStream resource [Stream]", resource.getDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBaseNameReturnsExpectedString() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertEquals("streamResource", resource.getBaseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsReturnsTrueWhenInputStreamNotNull() {
|
||||
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||
assertTrue(resource.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsReturnsFalseWhenInputStreamIsNull() {
|
||||
InputStreamTemplateResource resource = new InputStreamTemplateResource(null, "UTF-8");
|
||||
assertFalse(resource.exists());
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,12 +1,12 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
@@ -18,9 +18,11 @@ import org.junit.jupiter.api.parallel.Execution;
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.SpyPDFDocumentFactory.StrategyType;
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@@ -73,7 +75,7 @@ class CustomPDFDocumentFactoryTest {
|
||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||
try (PDDocument doc = factory.load(multipart)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
@@ -84,7 +86,7 @@ class CustomPDFDocumentFactoryTest {
|
||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileInput(multipart);
|
||||
try (PDDocument doc = factory.load(pdfFile)) {
|
||||
|
||||
@@ -2,6 +2,8 @@ package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.AdditionalAnswers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -15,6 +17,7 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -36,7 +39,7 @@ class FileStorageTest {
|
||||
// Create a mock MultipartFile
|
||||
mockFile = mock(MultipartFile.class);
|
||||
when(mockFile.getOriginalFilename()).thenReturn("test.pdf");
|
||||
when(mockFile.getContentType()).thenReturn("application/pdf");
|
||||
when(mockFile.getContentType()).thenReturn(MediaType.APPLICATION_PDF_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
@@ -77,7 +78,7 @@ class TaskManagerTest {
|
||||
taskManager.createTask(jobId);
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
String contentType = "application/pdf";
|
||||
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||
long fileSize = 1024L;
|
||||
|
||||
// Mock the fileStorage.getFileSize() call
|
||||
@@ -185,7 +186,8 @@ class TaskManagerTest {
|
||||
// 2. Create completed successful job with file
|
||||
String successFileJobId = "success-file-job";
|
||||
taskManager.createTask(successFileJobId);
|
||||
taskManager.setFileResult(successFileJobId, "file-id", "test.pdf", "application/pdf");
|
||||
taskManager.setFileResult(
|
||||
successFileJobId, "file-id", "test.pdf", MediaType.APPLICATION_PDF_VALUE);
|
||||
|
||||
// 3. Create completed successful job without file
|
||||
String successJobId = "success-job";
|
||||
@@ -235,7 +237,7 @@ class TaskManagerTest {
|
||||
ResultFile.builder()
|
||||
.fileId("file-id")
|
||||
.fileName("test.pdf")
|
||||
.contentType("application/pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF_VALUE)
|
||||
.fileSize(1024L)
|
||||
.build();
|
||||
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ChecksumUtilsTest {
|
||||
|
||||
@Test
|
||||
void computeChecksums_basic() throws Exception {
|
||||
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// MD5 (hex)
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
assertEquals("5d41402abc4b2a76b9719d911017c592", ChecksumUtils.checksum(is, "MD5"));
|
||||
}
|
||||
|
||||
// MD5 (Base64)
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
assertEquals("XUFAKrxLKna5cZ2REBfFkg==", ChecksumUtils.checksumBase64(is, "MD5"));
|
||||
}
|
||||
|
||||
// MD5 + CRC32 (hex map)
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
Map<String, String> map = ChecksumUtils.checksums(is, "MD5", "CRC32");
|
||||
assertEquals("5d41402abc4b2a76b9719d911017c592", map.get("MD5"));
|
||||
assertEquals("3610a686", map.get("CRC32"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void crc32_base64_bigEndianBytes_forHello() throws Exception {
|
||||
// CRC32("hello") = 0x3610A686 → bytes: 36 10 A6 86 → Base64: "NhCmhg=="
|
||||
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
assertEquals("NhCmhg==", ChecksumUtils.checksumBase64(is, "CRC32"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void crc32_unsignedFormatting_highBitSet() throws Exception {
|
||||
// CRC32 of single zero byte (0x00) is 0xD202EF8D (>= 0x8000_0000)
|
||||
byte[] data = new byte[] {0x00};
|
||||
|
||||
// Hex (unsigned, 8 chars, lowercase)
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
assertEquals("d202ef8d", ChecksumUtils.checksum(is, "CRC32"));
|
||||
}
|
||||
|
||||
// Base64 of the 4-byte big-endian representation
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
assertEquals("0gLvjQ==", ChecksumUtils.checksumBase64(is, "CRC32"));
|
||||
}
|
||||
|
||||
// matches(..) must be case-insensitive for hex
|
||||
try (InputStream is = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))) {
|
||||
assertTrue(ChecksumUtils.matches(is, "CRC32", "3610A686")); // uppercase expected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,7 +586,10 @@ class EmlToPdfTest {
|
||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf =
|
||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
||||
mockStatic(
|
||||
FileToPdf.class,
|
||||
org.mockito.Mockito.withSettings()
|
||||
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
@@ -657,7 +660,10 @@ class EmlToPdfTest {
|
||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf =
|
||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
||||
mockStatic(
|
||||
FileToPdf.class,
|
||||
org.mockito.Mockito.withSettings()
|
||||
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
@@ -724,7 +730,10 @@ class EmlToPdfTest {
|
||||
String errorMessage = "Conversion failed";
|
||||
|
||||
try (MockedStatic<FileToPdf> fileToPdf =
|
||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
||||
mockStatic(
|
||||
FileToPdf.class,
|
||||
org.mockito.Mockito.withSettings()
|
||||
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||
fileToPdf
|
||||
.when(
|
||||
() ->
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import stirling.software.common.model.FileInfo;
|
||||
|
||||
public class FileInfoTest {
|
||||
|
||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||
@CsvSource({
|
||||
"0, '0 Bytes'",
|
||||
"1023, '1023 Bytes'",
|
||||
"1024, '1.00 KB'",
|
||||
"1048575, '1024.00 KB'", // Do we really want this as result?
|
||||
"1048576, '1.00 MB'",
|
||||
"1073741823, '1024.00 MB'", // Do we really want this as result?
|
||||
"1073741824, '1.00 GB'"
|
||||
})
|
||||
void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) {
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
LocalDateTime.now(),
|
||||
fileSize,
|
||||
LocalDateTime.now().minusDays(1));
|
||||
|
||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -154,4 +161,223 @@ public class GeneralUtilsTest {
|
||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, false);
|
||||
assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveExtension() {
|
||||
// Test common cases (should use fast string operations)
|
||||
assertEquals("document", GeneralUtils.removeExtension("document.pdf"));
|
||||
assertEquals("image", GeneralUtils.removeExtension("image.jpg"));
|
||||
assertEquals("file.backup", GeneralUtils.removeExtension("file.backup.zip"));
|
||||
assertEquals("complex.file.name", GeneralUtils.removeExtension("complex.file.name.txt"));
|
||||
|
||||
// Test edge cases (should fall back to regex)
|
||||
assertEquals("default", GeneralUtils.removeExtension(null));
|
||||
assertEquals("noextension", GeneralUtils.removeExtension("noextension"));
|
||||
assertEquals(
|
||||
".hidden", GeneralUtils.removeExtension(".hidden")); // Hidden file, no extension
|
||||
assertEquals("trailing.", GeneralUtils.removeExtension("trailing.")); // Trailing dot
|
||||
assertEquals("", GeneralUtils.removeExtension(""));
|
||||
assertEquals("a", GeneralUtils.removeExtension("a"));
|
||||
|
||||
// Test multiple dots
|
||||
assertEquals("file.with.multiple", GeneralUtils.removeExtension("file.with.multiple.dots"));
|
||||
assertEquals("path/to/file", GeneralUtils.removeExtension("path/to/file.ext"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAppendSuffix() {
|
||||
// Normal cases
|
||||
assertEquals("document_processed", GeneralUtils.appendSuffix("document", "_processed"));
|
||||
assertEquals("file.txt", GeneralUtils.appendSuffix("file", ".txt"));
|
||||
|
||||
// Null handling
|
||||
assertEquals("default_suffix", GeneralUtils.appendSuffix(null, "_suffix"));
|
||||
assertEquals("basename", GeneralUtils.appendSuffix("basename", null));
|
||||
assertEquals("default", GeneralUtils.appendSuffix(null, null));
|
||||
|
||||
// Empty strings
|
||||
assertEquals("_suffix", GeneralUtils.appendSuffix("", "_suffix"));
|
||||
assertEquals("basename", GeneralUtils.appendSuffix("basename", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessFilenames() {
|
||||
List<String> filenames = new ArrayList<>();
|
||||
filenames.add("document.pdf");
|
||||
filenames.add("image.jpg");
|
||||
filenames.add("spreadsheet.xlsx");
|
||||
filenames.add("presentation.pptx");
|
||||
filenames.add(null); // Should handle null gracefully
|
||||
filenames.add("noextension");
|
||||
|
||||
List<String> results = new ArrayList<>();
|
||||
GeneralUtils.processFilenames(filenames, "_processed", results::add);
|
||||
|
||||
List<String> expected =
|
||||
List.of(
|
||||
"document_processed",
|
||||
"image_processed",
|
||||
"spreadsheet_processed",
|
||||
"presentation_processed",
|
||||
"default_processed",
|
||||
"noextension_processed");
|
||||
|
||||
assertEquals(expected, results);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessFilenamesNullHandling() {
|
||||
List<String> results = new ArrayList<>();
|
||||
|
||||
// Null filenames list
|
||||
GeneralUtils.processFilenames(null, "_suffix", results::add);
|
||||
assertTrue(results.isEmpty(), "Should handle null filenames list");
|
||||
|
||||
// Null processor
|
||||
List<String> filenames = List.of("test.txt");
|
||||
GeneralUtils.processFilenames(filenames, "_suffix", null); // Should not throw
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveExtensionThreadSafety() throws InterruptedException {
|
||||
final int threadCount = 50;
|
||||
final int operationsPerThread = 100;
|
||||
final String[] testFilenames = {
|
||||
"document.pdf", "image.jpg", "data.csv", "presentation.pptx",
|
||||
"archive.zip", "music.mp3", "video.mp4", "text.txt"
|
||||
};
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
executor.submit(
|
||||
() -> {
|
||||
try {
|
||||
for (int j = 0; j < operationsPerThread; j++) {
|
||||
String filename = testFilenames[j % testFilenames.length];
|
||||
String result = GeneralUtils.removeExtension(filename);
|
||||
|
||||
// Verify result is correct
|
||||
assertFalse(
|
||||
result.contains("."),
|
||||
"Result should not contain extension: " + result);
|
||||
assertTrue(
|
||||
filename.startsWith(result),
|
||||
"Original should start with result: "
|
||||
+ filename
|
||||
+ " -> "
|
||||
+ result);
|
||||
}
|
||||
successCount.incrementAndGet();
|
||||
} catch (Exception e) {
|
||||
exceptions.add(e);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS), "All threads should complete");
|
||||
|
||||
if (!exceptions.isEmpty()) {
|
||||
fail("Thread safety test failed with exceptions: " + exceptions);
|
||||
}
|
||||
|
||||
assertEquals(threadCount, successCount.get(), "All threads should succeed");
|
||||
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchProcessingPerformance() {
|
||||
List<String> filenames = new ArrayList<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
filenames.add("file" + i + ".pdf");
|
||||
filenames.add("document" + i + ".docx");
|
||||
filenames.add("image" + i + ".jpg");
|
||||
}
|
||||
|
||||
List<String> results = new ArrayList<>();
|
||||
|
||||
GeneralUtils.processFilenames(filenames, "_processed", results::add);
|
||||
|
||||
assertEquals(filenames.size(), results.size(), "Should process all filenames");
|
||||
|
||||
assertTrue(results.contains("file0_processed"), "Should contain processed filename");
|
||||
assertTrue(results.contains("document500_processed"), "Should contain processed filename");
|
||||
assertTrue(results.contains("image999_processed"), "Should contain processed filename");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHybridStringRegexApproach() {
|
||||
|
||||
String[] edgeCases = {
|
||||
"", // Empty string
|
||||
".", // Just a dot
|
||||
"..", // Two dots
|
||||
"...", // Three dots
|
||||
".hidden", // Hidden file
|
||||
"file.", // Trailing dot
|
||||
"a.b.c.d.e.f.g", // Many extensions
|
||||
"no-extension-here", // No extension
|
||||
"file..double.dot" // Double dots
|
||||
};
|
||||
|
||||
for (String edgeCase : edgeCases) {
|
||||
String result = GeneralUtils.removeExtension(edgeCase);
|
||||
assertNotNull(result, "Result should not be null for: " + edgeCase);
|
||||
|
||||
// For specific edge cases, verify expected behavior
|
||||
switch (edgeCase) {
|
||||
case "" -> assertEquals("", result, "Empty string should remain empty");
|
||||
case "." -> assertEquals(".", result, "Single dot should remain unchanged");
|
||||
case ".." -> assertEquals("..", result, "Double dots should remain unchanged");
|
||||
case "..." -> assertEquals("...", result, "Triple dots should remain unchanged");
|
||||
case ".hidden" ->
|
||||
assertEquals(".hidden", result, "Hidden file should remain unchanged");
|
||||
case "file." ->
|
||||
assertEquals("file.", result, "Trailing dot should remain unchanged");
|
||||
case "no-extension-here" ->
|
||||
assertEquals(
|
||||
"no-extension-here",
|
||||
result,
|
||||
"No extension should remain unchanged");
|
||||
case "a.b.c.d.e.f.g" ->
|
||||
assertEquals(
|
||||
"a.b.c.d.e.f",
|
||||
result,
|
||||
"Multiple extensions should remove last one");
|
||||
case "file..double.dot" ->
|
||||
assertEquals(
|
||||
"file..double",
|
||||
result,
|
||||
"Double dot case should remove last extension");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTitleFromFilename() {
|
||||
// Test normal cases
|
||||
assertEquals("document", GeneralUtils.getTitleFromFilename("document.pdf"));
|
||||
assertEquals("presentation", GeneralUtils.getTitleFromFilename("presentation.pptx"));
|
||||
assertEquals("file.backup", GeneralUtils.getTitleFromFilename("file.backup.zip"));
|
||||
|
||||
// Test null and empty handling
|
||||
assertEquals("Untitled", GeneralUtils.getTitleFromFilename(null));
|
||||
assertEquals("Untitled", GeneralUtils.getTitleFromFilename(""));
|
||||
|
||||
// Test edge cases
|
||||
assertEquals(".hidden", GeneralUtils.getTitleFromFilename(".hidden"));
|
||||
assertEquals("file.", GeneralUtils.getTitleFromFilename("file."));
|
||||
assertEquals("noextension", GeneralUtils.getTitleFromFilename("noextension"));
|
||||
|
||||
// Test complex cases
|
||||
assertEquals(
|
||||
"complex.file.name", GeneralUtils.getTitleFromFilename("complex.file.name.txt"));
|
||||
assertEquals("path/to/file", GeneralUtils.getTitleFromFilename("path/to/file.ext"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -25,6 +27,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -46,10 +49,21 @@ class PDFToFileTest {
|
||||
|
||||
@Mock private ProcessExecutor mockProcessExecutor;
|
||||
@Mock private ProcessExecutorResult mockExecutorResult;
|
||||
@Mock private TempFileManager mockTempFileManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
pdfToFile = new PDFToFile();
|
||||
void setUp() throws IOException {
|
||||
// Mock the TempFileManager to return real temp files
|
||||
lenient()
|
||||
.when(mockTempFileManager.createTempFile(anyString()))
|
||||
.thenAnswer(
|
||||
invocation ->
|
||||
Files.createTempFile("test", invocation.getArgument(0)).toFile());
|
||||
lenient()
|
||||
.when(mockTempFileManager.createTempDirectory())
|
||||
.thenAnswer(invocation -> Files.createTempDirectory("test"));
|
||||
|
||||
pdfToFile = new PDFToFile(mockTempFileManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,7 +71,10 @@ class PDFToFileTest {
|
||||
// Prepare
|
||||
MultipartFile nonPdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
||||
"file",
|
||||
"test.txt",
|
||||
MediaType.TEXT_PLAIN_VALUE,
|
||||
"This is not a PDF".getBytes());
|
||||
|
||||
// Execute
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
|
||||
@@ -71,7 +88,10 @@ class PDFToFileTest {
|
||||
// Prepare
|
||||
MultipartFile nonPdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
||||
"file",
|
||||
"test.txt",
|
||||
MediaType.TEXT_PLAIN_VALUE,
|
||||
"This is not a PDF".getBytes());
|
||||
|
||||
// Execute
|
||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(nonPdfFile);
|
||||
@@ -86,7 +106,10 @@ class PDFToFileTest {
|
||||
// Prepare
|
||||
MultipartFile nonPdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
||||
"file",
|
||||
"test.txt",
|
||||
MediaType.TEXT_PLAIN_VALUE,
|
||||
"This is not a PDF".getBytes());
|
||||
|
||||
// Execute
|
||||
ResponseEntity<byte[]> response =
|
||||
@@ -102,7 +125,10 @@ class PDFToFileTest {
|
||||
// Prepare
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
||||
"file",
|
||||
"test.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Execute with invalid format
|
||||
ResponseEntity<byte[]> response =
|
||||
@@ -120,7 +146,10 @@ class PDFToFileTest {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
||||
"file",
|
||||
"test.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Create a mock HTML output file
|
||||
Path htmlOutputFile = tempDir.resolve("test.html");
|
||||
@@ -168,7 +197,7 @@ class PDFToFileTest {
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"multipage.pdf",
|
||||
"application/pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
@@ -245,7 +274,10 @@ class PDFToFileTest {
|
||||
// Create a mock PDF file
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
||||
"file",
|
||||
"test.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
@@ -324,7 +356,7 @@ class PDFToFileTest {
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
@@ -386,7 +418,7 @@ class PDFToFileTest {
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
@@ -472,7 +504,7 @@ class PDFToFileTest {
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
"application/pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
@@ -531,7 +563,10 @@ class PDFToFileTest {
|
||||
// Create a mock PDF file with no filename
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file", "", "application/pdf", "Fake PDF content".getBytes());
|
||||
"file",
|
||||
"",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
// Setup ProcessExecutor mock
|
||||
mockedStaticProcessExecutor
|
||||
|
||||
@@ -65,23 +65,22 @@ public class PdfUtilsTest {
|
||||
doc1.addPage(new PDPage());
|
||||
doc1.addPage(new PDPage());
|
||||
doc1.addPage(new PDPage());
|
||||
PdfUtils utils = new PdfUtils();
|
||||
assertTrue(utils.pageCount(doc1, 2, "greater"));
|
||||
assertTrue(PdfUtils.pageCount(doc1, 2, "greater"));
|
||||
|
||||
PDDocument doc2 = new PDDocument();
|
||||
doc2.addPage(new PDPage());
|
||||
doc2.addPage(new PDPage());
|
||||
doc2.addPage(new PDPage());
|
||||
assertTrue(utils.pageCount(doc2, 3, "equal"));
|
||||
assertTrue(PdfUtils.pageCount(doc2, 3, "equal"));
|
||||
|
||||
PDDocument doc3 = new PDDocument();
|
||||
doc3.addPage(new PDPage());
|
||||
doc3.addPage(new PDPage());
|
||||
assertTrue(utils.pageCount(doc3, 5, "less"));
|
||||
assertTrue(PdfUtils.pageCount(doc3, 5, "less"));
|
||||
|
||||
PDDocument doc4 = new PDDocument();
|
||||
doc4.addPage(new PDPage());
|
||||
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
|
||||
assertThrows(IllegalArgumentException.class, () -> PdfUtils.pageCount(doc4, 1, "bad"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,8 +90,7 @@ public class PdfUtilsTest {
|
||||
doc.addPage(page);
|
||||
PDRectangle rect = page.getMediaBox();
|
||||
String expected = rect.getWidth() + "x" + rect.getHeight();
|
||||
PdfUtils utils = new PdfUtils();
|
||||
assertTrue(utils.pageSize(doc, expected));
|
||||
assertTrue(PdfUtils.pageSize(doc, expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class RegexPatternUtilsTest {
|
||||
|
||||
private RegexPatternUtils utils;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
utils = RegexPatternUtils.getInstance();
|
||||
utils.clearCache(); // Start with clean cache for each test
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPatternCaching() {
|
||||
String regex = "test\\d+";
|
||||
|
||||
Pattern pattern1 = utils.getPattern(regex);
|
||||
assertNotNull(pattern1);
|
||||
assertTrue(utils.isCached(regex));
|
||||
assertEquals(
|
||||
1, utils.getCacheSize()); // Should have at least 1 pattern (plus precompiled ones
|
||||
// are cleared)
|
||||
|
||||
Pattern pattern2 = utils.getPattern(regex);
|
||||
assertSame(pattern1, pattern2); // Should be the exact same object
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPatternWithFlags() {
|
||||
String regex = "test";
|
||||
int flags = Pattern.CASE_INSENSITIVE;
|
||||
|
||||
Pattern pattern1 = utils.getPattern(regex, flags);
|
||||
Pattern pattern2 = utils.getPattern(regex); // No flags
|
||||
|
||||
assertNotSame(pattern1, pattern2); // Different flags = different cached patterns
|
||||
assertTrue(utils.isCached(regex, flags));
|
||||
assertTrue(utils.isCached(regex, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCacheEviction() {
|
||||
String regex = "evict\\d+";
|
||||
|
||||
utils.getPattern(regex);
|
||||
assertTrue(utils.isCached(regex));
|
||||
|
||||
boolean removed = utils.removeFromCache(regex);
|
||||
assertTrue(removed);
|
||||
assertFalse(utils.isCached(regex));
|
||||
|
||||
boolean removedAgain = utils.removeFromCache(regex);
|
||||
assertFalse(removedAgain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullRegexHandling() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> {
|
||||
utils.getPattern(null);
|
||||
});
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> {
|
||||
utils.getPattern(null, Pattern.CASE_INSENSITIVE);
|
||||
});
|
||||
|
||||
assertFalse(utils.isCached(null));
|
||||
assertFalse(utils.removeFromCache(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCommonPatterns() {
|
||||
Pattern whitespace = utils.getWhitespacePattern();
|
||||
assertTrue(whitespace.matcher(" \t ").matches());
|
||||
|
||||
Pattern trailing = utils.getTrailingSlashesPattern();
|
||||
assertTrue(trailing.matcher("/path/to/dir///").find());
|
||||
|
||||
Pattern filename = utils.getSafeFilenamePattern();
|
||||
assertTrue(filename.matcher("bad<file>name").find());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateSearchPattern() {
|
||||
String regex = "Hello";
|
||||
|
||||
Pattern caseSensitive = utils.createSearchPattern(regex, false);
|
||||
Pattern caseInsensitive = utils.createSearchPattern(regex, true);
|
||||
|
||||
assertTrue(caseSensitive.matcher("Hello").matches());
|
||||
assertFalse(caseSensitive.matcher("hello").matches());
|
||||
|
||||
assertTrue(caseInsensitive.matcher("Hello").matches());
|
||||
assertTrue(caseInsensitive.matcher("hello").matches());
|
||||
assertTrue(caseInsensitive.matcher("HELLO").matches());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingletonBehavior() {
|
||||
RegexPatternUtils instance1 = RegexPatternUtils.getInstance();
|
||||
RegexPatternUtils instance2 = RegexPatternUtils.getInstance();
|
||||
|
||||
assertSame(instance1, instance2);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user