Fix PATH order for python3 calls, improve merge memory

This commit is contained in:
Anthony Stirling 2025-03-25 17:36:01 +00:00
parent 905fb67766
commit dfd567b803
11 changed files with 142 additions and 67 deletions

View File

@ -34,7 +34,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \ UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=/opt/venv/bin:$PATH PATH=$PATH:/opt/venv/bin
# JDK for app # JDK for app

View File

@ -43,7 +43,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \ UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=/opt/venv/bin:$PATH PATH=$PATH:/opt/venv/bin
# JDK for app # JDK for app

View File

@ -78,7 +78,6 @@ public class KeygenLicenseVerifier {
// Remove all newlines // Remove all newlines
encodedPayload = encodedPayload.replaceAll("\\r?\\n", ""); encodedPayload = encodedPayload.replaceAll("\\r?\\n", "");
byte[] payloadBytes = Base64.getDecoder().decode(encodedPayload); byte[] payloadBytes = Base64.getDecoder().decode(encodedPayload);
String payload = new String(payloadBytes); String payload = new String(payloadBytes);

View File

@ -290,7 +290,7 @@ public class EndpointConfiguration {
disableGroup("enterprise"); disableGroup("enterprise");
} }
if(!applicationProperties.getSystem().getEnableUrlToPDF()) { if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
disableEndpoint("url-to-pdf"); disableEndpoint("url-to-pdf");
} }
} }

View File

@ -125,8 +125,7 @@ public class MergeController {
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form) public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException { throws IOException {
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
ByteArrayOutputStream docOutputstream = File mergedTempFile = null;
new ByteArrayOutputStream(); // Stream for the merged document
PDDocument mergedDocument = null; PDDocument mergedDocument = null;
boolean removeCertSign = form.isRemoveCertSign(); boolean removeCertSign = form.isRemoveCertSign();
@ -139,21 +138,24 @@ public class MergeController {
form.getSortType())); // Sort files based on the given sort type form.getSortType())); // Sort files based on the given sort type
PDFMergerUtility mergerUtility = new PDFMergerUtility(); PDFMergerUtility mergerUtility = new PDFMergerUtility();
long totalSize = 0;
for (MultipartFile multipartFile : files) { for (MultipartFile multipartFile : files) {
totalSize += multipartFile.getSize();
File tempFile = File tempFile =
GeneralUtils.convertMultipartFileToFile( GeneralUtils.convertMultipartFileToFile(
multipartFile); // Convert MultipartFile to File multipartFile); // Convert MultipartFile to File
filesToDelete.add(tempFile); // Add temp file to the list for later deletion filesToDelete.add(tempFile); // Add temp file to the list for later deletion
mergerUtility.addSource(tempFile); // Add source file to the merger utility mergerUtility.addSource(tempFile); // Add source file to the merger utility
} }
mergerUtility.setDestinationStream(
docOutputstream); // Set the output stream for the merged document
mergerUtility.mergeDocuments(null); // Merge the documents
byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes mergedTempFile = File.createTempFile("merged-", ".pdf");
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
// Load the merged PDF document // Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedPdfBytes); mergedDocument = pdfDocumentFactory.load(mergedTempFile);
// Remove signatures if removeCertSign is true // Remove signatures if removeCertSign is true
if (removeCertSign) { if (removeCertSign) {
@ -180,21 +182,23 @@ public class MergeController {
String mergedFileName = String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf"; + "_merged_unsigned.pdf";
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.boasToWebResponse(
baos.toByteArray(), mergedFileName); // Return the modified PDF baos, mergedFileName); // Return the modified PDF
} catch (Exception ex) { } catch (Exception ex) {
log.error("Error in merge pdf process", ex); log.error("Error in merge pdf process", ex);
throw ex; throw ex;
} finally { } finally {
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
for (File file : filesToDelete) { for (File file : filesToDelete) {
if (file != null) { if (file != null) {
Files.deleteIfExists(file.toPath()); // Delete temporary files Files.deleteIfExists(file.toPath()); // Delete temporary files
} }
} }
docOutputstream.close(); if (mergedTempFile != null) {
if (mergedDocument != null) { Files.deleteIfExists(mergedTempFile.toPath());
mergedDocument.close(); // Close the merged document
} }
} }
} }

View File

@ -37,9 +37,12 @@ public class ConvertWebsiteToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig; private final RuntimePathConfig runtimePathConfig;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@Autowired @Autowired
public ConvertWebsiteToPDF( public ConvertWebsiteToPDF(
CustomPDFDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig, ApplicationProperties applicationProperties) { CustomPDFDocumentFactory pdfDocumentFactory,
RuntimePathConfig runtimePathConfig,
ApplicationProperties applicationProperties) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
this.runtimePathConfig = runtimePathConfig; this.runtimePathConfig = runtimePathConfig;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
@ -55,8 +58,7 @@ public class ConvertWebsiteToPDF {
throws IOException, InterruptedException { throws IOException, InterruptedException {
String URL = request.getUrlInput(); String URL = request.getUrlInput();
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
if(!applicationProperties.getSystem().getEnableUrlToPDF()) {
throw new IllegalArgumentException("This endpoint has been disabled by the admin."); throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
} }
// Validate the URL format // Validate the URL format

View File

@ -22,6 +22,7 @@ import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointInspector;
import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -32,15 +33,17 @@ import stirling.software.SPDF.model.ApplicationProperties;
public class MetricsController { public class MetricsController {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
private final EndpointInspector endpointInspector;
private boolean metricsEnabled; private boolean metricsEnabled;
public MetricsController( public MetricsController(
ApplicationProperties applicationProperties, MeterRegistry meterRegistry) { ApplicationProperties applicationProperties,
MeterRegistry meterRegistry,
EndpointInspector endpointInspector) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
this.endpointInspector = endpointInspector;
} }
@PostConstruct @PostConstruct
@ -208,16 +211,40 @@ public class MetricsController {
} }
private double getRequestCount(String method, Optional<String> endpoint) { private double getRequestCount(String method, Optional<String> endpoint) {
double count = return meterRegistry.find("http.requests").tag("method", method).counters().stream()
meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter( .filter(
counter -> counter -> {
!endpoint.isPresent() String uri = counter.getId().getTag("uri");
|| endpoint.get()
.equals(counter.getId().getTag("uri"))) // Apply filtering logic - Skip if uri is null
if (uri == null) {
return false;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return false;
}
if (uri.contains(".txt")) {
return false;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return false;
}
// Filter for specific endpoint if provided
return !endpoint.isPresent() || endpoint.get().equals(uri);
})
.mapToDouble(Counter::count) .mapToDouble(Counter::count)
.sum(); .sum();
return count;
} }
private List<EndpointCount> getEndpointCounts(String method) { private List<EndpointCount> getEndpointCounts(String method) {
@ -229,23 +256,72 @@ public class MetricsController {
.forEach( .forEach(
counter -> { counter -> {
String uri = counter.getId().getTag("uri"); String uri = counter.getId().getTag("uri");
// Skip if uri is null
if (uri == null) {
return;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return;
}
if (uri.contains(".txt")) {
return;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return;
}
counts.merge(uri, counter.count(), Double::sum); counts.merge(uri, counter.count(), Double::sum);
}); });
List<EndpointCount> result =
counts.entrySet().stream() return counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
return result;
} }
private double getUniqueUserCount(String method, Optional<String> endpoint) { private double getUniqueUserCount(String method, Optional<String> endpoint) {
Set<String> uniqueUsers = new HashSet<>(); Set<String> uniqueUsers = new HashSet<>();
meterRegistry.find("http.requests").tag("method", method).counters().stream() meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter( .filter(
counter -> counter -> {
!endpoint.isPresent() String uri = counter.getId().getTag("uri");
|| endpoint.get().equals(counter.getId().getTag("uri")))
// Skip if uri is null
if (uri == null) {
return false;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return false;
}
if (uri.contains(".txt")) {
return false;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return false;
}
return !endpoint.isPresent() || endpoint.get().equals(uri);
})
.forEach( .forEach(
counter -> { counter -> {
String session = counter.getId().getTag("session"); String session = counter.getId().getTag("session");
@ -270,12 +346,10 @@ public class MetricsController {
uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session); uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session);
} }
}); });
List<EndpointCount> result = return uniqueUsers.entrySet().stream()
uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size())) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
return result;
} }
@GetMapping("/uptime") @GetMapping("/uptime")

View File

@ -139,7 +139,7 @@ public class CustomPDFDocumentFactory {
* Determine the appropriate caching strategy based on file size and available memory. This * Determine the appropriate caching strategy based on file size and available memory. This
* common method is used by both password and non-password loading paths. * common method is used by both password and non-password loading paths.
*/ */
private StreamCacheCreateFunction getStreamCacheFunction(long contentSize) { public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
long maxMemory = Runtime.getRuntime().maxMemory(); long maxMemory = Runtime.getRuntime().maxMemory();
long freeMemory = Runtime.getRuntime().freeMemory(); long freeMemory = Runtime.getRuntime().freeMemory();
long totalMemory = Runtime.getRuntime().totalMemory(); long totalMemory = Runtime.getRuntime().totalMemory();

View File

@ -72,10 +72,6 @@ premium:
author: username author: username
creator: Stirling-PDF creator: Stirling-PDF
producer: Stirling-PDF producer: Stirling-PDF
enterpriseFeatures:
persistentMetrics:
enabled: false
retentionDays: 90
legal: legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder

View File

@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -29,13 +28,14 @@ public class ConvertWebsiteToPdfTest {
MockitoAnnotations.openMocks(this); MockitoAnnotations.openMocks(this);
applicationProperties = new ApplicationProperties(); applicationProperties = new ApplicationProperties();
applicationProperties.getSystem().setEnableUrlToPDF(true); applicationProperties.getSystem().setEnableUrlToPDF(true);
convertWebsiteToPDF = new ConvertWebsiteToPDF(mockPdfDocumentFactory, runtimePathConfig, applicationProperties); convertWebsiteToPDF =
new ConvertWebsiteToPDF(
mockPdfDocumentFactory, runtimePathConfig, applicationProperties);
} }
@Test @Test
public void test_exemption_is_thrown_when_invalid_url_format_provided() { public void test_exemption_is_thrown_when_invalid_url_format_provided() {
String invalid_format_Url = "invalid-url"; String invalid_format_Url = "invalid-url";
UrlToPdfRequest request = new UrlToPdfRequest(); UrlToPdfRequest request = new UrlToPdfRequest();