diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 5ee241863..2f4f242cb 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -11,11 +11,12 @@ openapi: &openapi - app/(common|core|proprietary)/src/main/java/** project: &project - - app/** + - app/(common|core|proprietary)/src/(main|test)/java/** + - app/(common|core|proprietary)/build.gradle + - 'app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*' - exampleYmlFiles/** - gradle/** - libs/** - - scripts/** - testing/** - build.gradle - Dockerfile diff --git a/.github/labeler-config-srvaroa.yml b/.github/labeler-config-srvaroa.yml index bdc48da53..44a234e3b 100644 --- a/.github/labeler-config-srvaroa.yml +++ b/.github/labeler-config-srvaroa.yml @@ -76,8 +76,8 @@ labels: - 'app/core/src/main/resources/settings.yml.template' - 'app/core/src/main/resources/application.properties' - 'app/core/src/main/resources/banner.txt' - - 'scripts/png_to_webp.py' - - 'split_photos.py' + - 'app/core/src/main/resources/static/python/png_to_webp.py' + - 'app/core/src/main/resources/static/python/split_photos.py' - 'application.properties' - label: 'Security' @@ -95,8 +95,8 @@ labels: - 'app/core/src/main/java/stirling/software/SPDF/model/api/.*' - 'app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java' - 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*' - - 'scripts/png_to_webp.py' - - 'split_photos.py' + - 'app/core/src/main/resources/static/python/png_to_webp.py' + - 'app/core/src/main/resources/static/python/split_photos.py' - '.github/workflows/swagger.yml' - label: 'Documentation' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 094e9b2fa..ec2d837e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,10 +6,10 @@ repos: args: - --fix - --line-length=127 - files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$ + files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$ exclude: (split_photos.py) - id: ruff-format - files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$ + files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$ exclude: (split_photos.py) - repo: https://github.com/codespell-project/codespell rev: v2.4.1 diff --git a/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java b/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java index d087f2a7a..08618329d 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java @@ -14,6 +14,7 @@ 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; // Config paths private static final String SETTINGS_PATH; @@ -36,6 +37,7 @@ public class InstallationPathConfig { // Initialize config paths SETTINGS_PATH = CONFIG_PATH + "settings.yml"; CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml"; + SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator; // Initialize custom file paths STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator; @@ -89,6 +91,10 @@ public class InstallationPathConfig { return CLIENT_WEBUI_PATH; } + public static String getScriptsPath() { + return SCRIPTS_PATH; + } + public static String getSettingsPath() { return SETTINGS_PATH; } diff --git a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java index ddbec92e0..a164faec2 100644 --- a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Locale; import java.util.UUID; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternUtils; @@ -33,6 +34,9 @@ import stirling.software.common.configuration.InstallationPathConfig; @Slf4j public class GeneralUtils { + private static final List DEFAULT_VALID_SCRIPTS = + List.of("png_to_webp.py", "split_photos.py"); + public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY"); if (customTempDir == null || customTempDir.isEmpty()) { @@ -442,6 +446,40 @@ public class GeneralUtils { } } + /** + * Extracts a file from classpath:/static/python to a temporary directory and returns the path. + */ + public static Path extractScript(String scriptName) throws IOException { + // Validate input + if (scriptName == null || scriptName.trim().isEmpty()) { + throw new IllegalArgumentException("scriptName must not be null or empty"); + } + if (scriptName.contains("..") || scriptName.contains("/")) { + throw new IllegalArgumentException( + "scriptName must not contain path traversal characters"); + } + + if (!DEFAULT_VALID_SCRIPTS.contains(scriptName)) { + throw new IllegalArgumentException( + "scriptName must be either 'png_to_webp.py' or 'split_photos.py'"); + } + + Path scriptsDir = Paths.get(InstallationPathConfig.getScriptsPath(), "python"); + Files.createDirectories(scriptsDir); + + Path scriptFile = scriptsDir.resolve(scriptName); + if (!Files.exists(scriptFile)) { + ClassPathResource resource = new ClassPathResource("static/python/" + scriptName); + try (InputStream in = resource.getInputStream()) { + Files.copy(in, scriptFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + log.error("Failed to extract Python script", e); + throw e; + } + } + return scriptFile; + } + public static boolean isVersionHigher(String currentVersion, String compareVersion) { if (currentVersion == null || compareVersion == null) { return false; diff --git a/app/core/.gitignore b/app/core/.gitignore index f85be51d5..2b3880de6 100644 --- a/app/core/.gitignore +++ b/app/core/.gitignore @@ -194,3 +194,5 @@ id_ed25519.pub # node_modules node_modules/ + +scripts/**/* diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index 82dcc2bc5..8efd983b0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -56,8 +56,8 @@ public class ConvertImgPDFController { summary = "Convert PDF to image(s)", description = "This endpoint converts a PDF file to image(s) with the specified image format," - + " color type, and DPI. Users can choose to get a single image or multiple" - + " images. Input:PDF Output:Image Type:SI-Conditional") + + " color type, and DPI. Users can choose to get a single image or multiple" + + " images. Input:PDF Output:Image Type:SI-Conditional") public ResponseEntity convertToImage(@ModelAttribute ConvertToImageRequest request) throws Exception { MultipartFile file = request.getFileInput(); @@ -117,10 +117,14 @@ public class ConvertImgPDFController { } String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); + Path pngToWebpScript = GeneralUtils.extractScript("png_to_webp.py"); List command = new ArrayList<>(); command.add(pythonVersion); - command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion + command.add( + pngToWebpScript + .toAbsolutePath() + .toString()); // Python script to handle the conversion // Create a temporary directory for the output WebP files tempOutputDir = Files.createTempDirectory("webp_output"); @@ -232,7 +236,8 @@ public class ConvertImgPDFController { PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory); return WebResponseUtils.bytesToWebResponse( bytes, - new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); + new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "") + + "_converted.pdf"); } private String getMediaType(String imageFormat) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index 7ecc04e1a..1a0ae7516 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -34,6 +34,7 @@ import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.WebResponseUtils; @@ -54,9 +55,9 @@ public class ExtractImageScansController { summary = "Extract image scans from an input file", description = "This endpoint extracts image scans from a given file based on certain" - + " parameters. Users can specify angle threshold, tolerance, minimum area," - + " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP" - + " Type:SIMO") + + " parameters. Users can specify angle threshold, tolerance, minimum area," + + " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP" + + " Type:SIMO") public ResponseEntity extractImageScans( @ModelAttribute ExtractImageScansRequest request) throws IOException, InterruptedException { @@ -78,6 +79,7 @@ public class ExtractImageScansController { } String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); + Path splitPhotosScript = GeneralUtils.extractScript("split_photos.py"); try { // Check if input file is a PDF if ("pdf".equalsIgnoreCase(extension)) { @@ -120,7 +122,7 @@ public class ExtractImageScansController { new ArrayList<>( Arrays.asList( pythonVersion, - "./scripts/split_photos.py", + splitPhotosScript.toAbsolutePath().toString(), images.get(i), tempDir.toString(), "--angle_threshold", diff --git a/scripts/png_to_webp.py b/app/core/src/main/resources/static/python/png_to_webp.py similarity index 100% rename from scripts/png_to_webp.py rename to app/core/src/main/resources/static/python/png_to_webp.py diff --git a/scripts/split_photos.py b/app/core/src/main/resources/static/python/split_photos.py similarity index 100% rename from scripts/split_photos.py rename to app/core/src/main/resources/static/python/split_photos.py