From 1b0c6e6d4bfcaf2f26f9ed0a820b5305f7faa47d Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 10:40:13 +0100 Subject: [PATCH 01/18] fix_114 --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 403b06d..9a7fe73 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,15 @@ Table of contents - Visualize each bit layer (LSB and other layers) per image channel (R/G/B/Alpha). - Browse and download each bit-layer image. - Integrates and displays outputs from: - - zsteg (LSB text/data extraction) - - steghide (extraction with password) - - outguess (extraction with password) - - openstego (extraction with password) - - exiftool (metadata and geolocation) - - binwalk (embedded archives) - - foremost (carved files) - - pngcheck - - strings + - [zsteg](https://github.com/zed-0xff/zsteg) (LSB text/data extraction) + - [steghide](https://steghide.sourceforge.net/) (extraction with password) + - [outguess](https://www.rbcafe.com/software/outguess/) (extraction with password) + - [openstego](https://www.openstego.com/) (extraction with password) + - [exiftool](https://exiftool.org/) (metadata and geolocation) + - [binwalk](https://github.com/ReFirmLabs/binwalk) (embedded archives) + - [foremost](https://foremost.sourceforge.net/) (carved files) + - [pngcheck](https://www.libpng.org/pub/png/apps/pngcheck.html) + - [strings](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/strings.html) - Worker queue architecture for offloading heavy/slow analyzers (Redis + background workers). - Results stored for later browsing and download. @@ -198,4 +198,4 @@ CI will run these checks on each PR. ## Credits Acknowledgements: -- Thanks to contributors and the open-source community for the tools integrated (zsteg, steghide, openstego, binwalk, foremost, exiftool, ...). +- Thanks to contributors and the open-source community for the tools integrated ([zsteg](https://github.com/zed-0xff/zsteg), [steghide](https://steghide.sourceforge.net/), [outguess](https://www.rbcafe.com/software/outguess/), [openstego](https://www.openstego.com/), [exiftool](https://exiftool.org/), [binwalk](https://github.com/ReFirmLabs/binwalk), [foremost](https://foremost.sourceforge.net/), [pngcheck](https://www.libpng.org/pub/png/apps/pngcheck.html), [strings](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/strings.html), ...). From ac1427ddac3a8aef85dfe6729c431f39d32af960 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 10:51:07 +0100 Subject: [PATCH 02/18] fix_113 --- aperisolve/analyzers/image_resize.py | 11 +++++++++++ aperisolve/analyzers/utils.py | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py index b7b4589..9761635 100644 --- a/aperisolve/analyzers/image_resize.py +++ b/aperisolve/analyzers/image_resize.py @@ -146,6 +146,17 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None: logs.append(f"Target CRC found: 0x{png.crc:08x}") + if png.crc == png.compute_crc(): + logs.append( + f"PNG is already valid with dimensions {png.width}x{png.height} " + f"and crc 0x{png.crc:08x}." + ) + output_data: dict[str, Any] = { + "image_resize": {"status": "ok", "output": logs} + } + update_data(output_dir, output_data) + return + saved_img_urls = [] db_matches = lookup_crc(png) diff --git a/aperisolve/analyzers/utils.py b/aperisolve/analyzers/utils.py index d17adf7..da86987 100644 --- a/aperisolve/analyzers/utils.py +++ b/aperisolve/analyzers/utils.py @@ -180,17 +180,24 @@ class PNG: self.height = size[1] self.bit_depth = bit_depth self.color_type = color_type - self.crc = ( - crc - or zlib.crc32( + self.interlace = interlace + self.crc = crc or self.compute_crc() + + def compute_crc(self) -> int: + """Compute the CRC checksum for the PNG IHDR chunk. + + Returns: + int: The computed CRC checksum. + """ + return ( + zlib.crc32( b"IHDR" + self.width.to_bytes(4, "big") + self.height.to_bytes(4, "big") - + bytes((bit_depth, color_type, 0, 0, interlace)) + + bytes((self.bit_depth, self.color_type, 0, 0, self.interlace)) ) & 0xFFFFFFFF ) - self.interlace = interlace @classmethod def from_packed(cls, packed: int, crc: int | None = None) -> Self: From b096d1334485a167795fb9ae1c79d5efb1ad0829 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 10:54:42 +0100 Subject: [PATCH 03/18] lint --- aperisolve/analyzers/image_resize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py index 9761635..4e35e15 100644 --- a/aperisolve/analyzers/image_resize.py +++ b/aperisolve/analyzers/image_resize.py @@ -151,10 +151,10 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None: f"PNG is already valid with dimensions {png.width}x{png.height} " f"and crc 0x{png.crc:08x}." ) - output_data: dict[str, Any] = { + output_no_compute: dict[str, Any] = { "image_resize": {"status": "ok", "output": logs} } - update_data(output_dir, output_data) + update_data(output_dir, output_no_compute) return saved_img_urls = [] From a2987ed90b47b8013e0b9454fb66fe20ca7f6175 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 17:25:05 +0100 Subject: [PATCH 04/18] Add github markdown alerts --- README.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9a7fe73..b2b812b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ Support Aperi'Solve: - --- Aperi'Solve is an open-source steganalysis web platform that performs automated analysis on images to detect and extract hidden data using common steganography tools and techniques. @@ -87,10 +86,12 @@ docker exec -it aperisolve-web bash docker exec -it postgres psql -U aperiuser -d aperisolve ``` -Note: If switching between dev and production compose files, remove the `results` directory or mounted volume to avoid conflicts: -```bash -rm -rf aperisolve/results -``` +> [!WARNING] +> If switching between dev and production compose files, remove the `results` directory or mounted volume to avoid conflicts: +> ```bash +> rm -rf aperisolve/results +> ``` + ### Adding a new analyzer Adding a custom analyzer is straightforward: @@ -129,9 +130,9 @@ Adding a custom analyzer is straightforward: 4. Test locally: run the worker and submit jobs to ensure outputs are produced and displayed. -Tips: -- Keep analyzers idempotent and write outputs to the provided `results_dir`. -- Return structured JSON so the frontend can render links/downloads automatically. +> [!TIP] +> - Keep analyzers idempotent and write outputs to the provided `results_dir`. +> - Return structured JSON so the frontend can render links/downloads automatically. ## Configuration & environment variables @@ -147,7 +148,8 @@ Main environment variables (examples): - SECRET_KEY=change_me - FLASK_ENV=production/development -If using Docker Compose, defaults are set in the compose files. For production deployments, set secure secrets via your orchestrator or environment. +> [!NOTE] +> If using Docker Compose, defaults are set in the compose files. For production deployments, set secure secrets via your orchestrator or environment. ## Architecture @@ -182,18 +184,19 @@ Contributions are welcome! Please: 2. Create a feature branch 3. Open a pull request describing your change -Please follow the code style and run linters before submitting. -The project adheres to: -- Black -- Flake8 (ignoring E203, E501, W503) -- Pylint (ignoring W0718, R0903, R0801) -- Mypy (ignoring unused-awaitable) +> [!IMPORTANT] +> Please follow the code style and run linters before submitting. The project adheres to: +> - Black +> - Flake8 (ignoring E203, E501, W503) +> - Pylint (ignoring W0718, R0903, R0801) +> - Mypy (ignoring unused-awaitable) CI will run these checks on each PR. ## Security -- If you discover a security issue, please report it privately to the repository owner instead of opening a public issue. +> [!WARNING] +> If you discover a security issue, please report it privately to the repository owner instead of opening a public issue. ## Credits From db212312e78b97b56dcb7f4538b63039aae7b970 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 17:29:21 +0100 Subject: [PATCH 05/18] Improve roadmap --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b2b812b..467109a 100644 --- a/README.md +++ b/README.md @@ -171,11 +171,17 @@ This separation keeps heavy tools (binwalk, foremost, zsteg, etc.) isolated and ## Roadmap -- [ ] zsteg: full extraction (--all) and download of discovered files (mp3, etc.) -- [ ] Mobile-friendly UI / test on mobile -- [ ] i18n (internationalization) -- [ ] Rootless / unprivileged analyzers -- [ ] Improve analyzer sandboxing (e.g., per-analyzer containers) +- [ ] **[Bug]** Error 500 when file already exist on disc +- [ ] **[Bug]** Multiple bug on buggy Jpeg filebugSomething isn't working +- [ ] **[Bug]** Binwalk Extractor errorbugSomething isn't working +- [ ] **[Bug]** Grayscale on a RGB image ?bugSomething isn't working +- [ ] **[Feature]** Implement healthcheck and auto reboot +- [ ] **[Feature]** Implement Sentry for better error handling +- [ ] **[Feature]** Zsteg: full extraction (--all) and download of discovered files (mp3, etc.) +- [ ] **[Feature]** Mobile-friendly UI / test on mobile +- [ ] **[Feature]** i18n (internationalization) +- [ ] **[Feature]** Rootless / unprivileged analyzers +- [ ] **[Feature]** Improve analyzer sandboxing (e.g., per-analyzer containers) ## Contributing From ac07266bff650de611ebf03984165d413ed118e5 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 17:44:13 +0100 Subject: [PATCH 06/18] Enhance error handling in exiftool and pngcheck analyzers --- aperisolve/analyzers/exiftool.py | 21 ++++++++++++--------- aperisolve/analyzers/pngcheck.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/aperisolve/analyzers/exiftool.py b/aperisolve/analyzers/exiftool.py index 23ec7bd..e13e2e9 100644 --- a/aperisolve/analyzers/exiftool.py +++ b/aperisolve/analyzers/exiftool.py @@ -18,21 +18,24 @@ def analyze_exiftool(input_img: Path, output_dir: Path) -> None: timeout=MAX_PENDING_TIME, ) - if data.stderr: - update_data( - output_dir, {"exiftool": {"status": "error", "error": data.stderr}} - ) - return - metadata: dict[str, str] = {} for line in data.stdout.split("\n"): if ":" in line: key, value = line.split(":", 1) metadata[key.strip()] = value.strip() - # exiftool_dir = output_dir / "exiftool" - # exiftool_dir.mkdir(parents=True, exist_ok=True) - # update_data(exiftool_dir, metadata, "data.json") + if data.stderr: + update_data( + output_dir, + { + "exiftool": { + "status": "error", + "output": metadata, + "error": data.stderr, + } + }, + ) + return update_data( output_dir, diff --git a/aperisolve/analyzers/pngcheck.py b/aperisolve/analyzers/pngcheck.py index 5c9a945..a398bd5 100644 --- a/aperisolve/analyzers/pngcheck.py +++ b/aperisolve/analyzers/pngcheck.py @@ -24,6 +24,19 @@ def analyze_pngcheck(input_img: Path, output_dir: Path) -> None: # ) # return + if "this is neither a PNG or JNG image nor a MNG stream" in data.stdout: + update_data( + output_dir, + { + "pngcheck": { + "status": "error", + "error": "ERRORS DETECTED: this is neither a PNG or JNG image nor a " + "MNG stream.", + } + }, + ) + return + data_pngcheck = data.stdout.split("\n") if data else [] data_pngcheck = [s for s in data_pngcheck if s] From 190ac9346d64f1b5fe97215687bfe2be4e54e7b8 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 18:31:14 +0100 Subject: [PATCH 07/18] fix 69 --- aperisolve/analyzers/decomposer.py | 27 ++++++++++++++++++--------- aperisolve/analyzers/image_resize.py | 4 ++-- aperisolve/static/js/aperisolve.js | 8 +++++++- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/aperisolve/analyzers/decomposer.py b/aperisolve/analyzers/decomposer.py index b923fed..e6440d3 100644 --- a/aperisolve/analyzers/decomposer.py +++ b/aperisolve/analyzers/decomposer.py @@ -11,6 +11,12 @@ from .utils import update_data def analyze_decomposer(input_img: Path, output_dir: Path) -> None: """Analyze an image submission using bits decomposition.""" img = Image.open(input_img) + converted = False + + if img.mode == "P": + img = img.convert("RGB") + converted = True + img_np = np.array(img) # Handle grayscale, RGB, RGBA, etc. @@ -58,12 +64,15 @@ def analyze_decomposer(input_img: Path, output_dir: Path) -> None: bit_img.save(out_path) image_json[channel_label] = channel_json - update_data( - output_dir, - { - "decomposer": { - "status": "ok", - "images": image_json, - } - }, - ) + output = { + "decomposer": { + "status": "ok", + "images": image_json, + } + } + if converted: + output["decomposer"][ + "note" + ] = "Image contains a color palette and was converted to RGB for processing." + + update_data(output_dir, output) diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py index 4e35e15..bf30897 100644 --- a/aperisolve/analyzers/image_resize.py +++ b/aperisolve/analyzers/image_resize.py @@ -147,12 +147,12 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None: logs.append(f"Target CRC found: 0x{png.crc:08x}") if png.crc == png.compute_crc(): - logs.append( + note = ( f"PNG is already valid with dimensions {png.width}x{png.height} " f"and crc 0x{png.crc:08x}." ) output_no_compute: dict[str, Any] = { - "image_resize": {"status": "ok", "output": logs} + "image_resize": {"status": "ok", "note": note} } update_data(output_dir, output_no_compute) return diff --git a/aperisolve/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js index a093ce7..54ecf94 100644 --- a/aperisolve/static/js/aperisolve.js +++ b/aperisolve/static/js/aperisolve.js @@ -327,9 +327,15 @@ function parseResult(result) { result[tool]["download"] )}" target="_blank" class="btn btn-primary mt-2"> Download file`; } - } else if (result[tool]["status"] === "error") { + } + + if (result[tool]["error"] && (/[^\s]/.test(result[tool]["error"]))) { showDanger(result[tool]["error"]); } + + if (result[tool]["note"] && (/[^\s]/.test(result[tool]["note"]))) { + showInfo(result[tool]["note"]); + } } } From c0e95bd2ced3293b16a44a0750cc546d9eaccf20 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 18:57:33 +0100 Subject: [PATCH 08/18] Update readme roadmap --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 467109a..929dbc2 100644 --- a/README.md +++ b/README.md @@ -172,9 +172,8 @@ This separation keeps heavy tools (binwalk, foremost, zsteg, etc.) isolated and ## Roadmap - [ ] **[Bug]** Error 500 when file already exist on disc -- [ ] **[Bug]** Multiple bug on buggy Jpeg filebugSomething isn't working -- [ ] **[Bug]** Binwalk Extractor errorbugSomething isn't working -- [ ] **[Bug]** Grayscale on a RGB image ?bugSomething isn't working +- [ ] **[Bug]** Duplicate key value violates unique constraint "submission_pkey" +- [ ] **[Bug]** Binwalk Extractor error - [ ] **[Feature]** Implement healthcheck and auto reboot - [ ] **[Feature]** Implement Sentry for better error handling - [ ] **[Feature]** Zsteg: full extraction (--all) and download of discovered files (mp3, etc.) From 566055e5eb39314a9adeedd752bfe762978070d7 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Tue, 30 Dec 2025 06:49:05 +0530 Subject: [PATCH 09/18] Fix Dockerfile to install default-jdk instead of default-jre to get jar --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fe69aa9..41e4c21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR / # Install build tools and dependencies RUN apt-get update && apt-get install -y \ wget \ - default-jre \ + default-jdk \ ruby \ zip \ 7zip From 54adcbcbfa61faf02dceb9e72db37153ebcdb267 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Tue, 30 Dec 2025 06:49:34 +0530 Subject: [PATCH 10/18] Fix error handling in binwalk analyzer to check for zip existence before reporting errors --- aperisolve/analyzers/binwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperisolve/analyzers/binwalk.py b/aperisolve/analyzers/binwalk.py index 32d6fec..c3d1f2e 100644 --- a/aperisolve/analyzers/binwalk.py +++ b/aperisolve/analyzers/binwalk.py @@ -44,7 +44,7 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: if extracted_dir.exists(): shutil.rmtree(extracted_dir) - if len(stderr) > 0: + if len(stderr) > 0 and not zip_exist: err = { "binwalk": { "status": "error", From a2f6579380c310e89dcd032001947029c7884b3e Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Tue, 30 Dec 2025 06:50:41 +0530 Subject: [PATCH 11/18] Clarify error reporting logic in binwalk analyzer comments --- aperisolve/analyzers/binwalk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aperisolve/analyzers/binwalk.py b/aperisolve/analyzers/binwalk.py index c3d1f2e..69920e7 100644 --- a/aperisolve/analyzers/binwalk.py +++ b/aperisolve/analyzers/binwalk.py @@ -44,6 +44,8 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: if extracted_dir.exists(): shutil.rmtree(extracted_dir) + # Only report error if stderr exists AND extraction failed. + # If zip_exist is True, binwalk worked (despite warnings), so we proceed. if len(stderr) > 0 and not zip_exist: err = { "binwalk": { From ea04b57dcea011a7bbfc8ecd8d631b66676f4ff0 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Tue, 30 Dec 2025 11:48:05 +0530 Subject: [PATCH 12/18] Refactor upload_image function to streamline submission and image handling logic --- aperisolve/app.py | 116 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 30 deletions(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index e81de53..9705156 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -112,21 +112,26 @@ def upload_image() -> tuple[Response, int]: submission_hash = hashlib.md5(submission_data).hexdigest() submission_path = RESULT_FOLDER / img_hash / submission_hash - # If submission already exists, return submission hash - if submission_path.exists(): - submission: Submission = Submission.query.filter_by( - hash=submission_hash - ).first() - return jsonify({"submission_hash": submission.hash}), 200 + submission: Submission = Submission.query.filter_by( + hash=submission_hash + ).first() - # If image is new, create a new Image entry + if submission_path.exists() and submission is not None: + return jsonify({"submission_hash": submission.hash}),200 + new_img_path = RESULT_FOLDER / img_hash / image_name - if not new_img_path.parent.exists(): - new_img_path.parent.mkdir(parents=True, exist_ok=True) - with open(new_img_path, "wb") as f: # Write file to disk - f.write(image_data) - new_img = Image( + sub_img = Image.query.filter_by( + hash=img_hash + ).first() + + if sub_img is None: + if not new_img_path.parent.exists() or not new_img_path.exists(): + new_img_path.parent.mkdir(parents=True, exist_ok=True) + with open(new_img_path,"wb") as f: + f.write(image_data) + + sub_img = Image( file=str(new_img_path), hash=img_hash, size=len(image_data), @@ -134,31 +139,82 @@ def upload_image() -> tuple[Response, int]: first_submission_date=datetime.now(timezone.utc), last_submission_date=datetime.now(timezone.utc), ) - db.session.add(new_img) # pylint: disable=no-member - db.session.commit() # pylint: disable=no-member + db.session.add(sub_img) + db.session.commit() - sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore + sub_img.upload_count += 1 - db.session.commit() # pylint: disable=no-member + db.session.commit() - # Create new Submission entry submission_path.mkdir(parents=True, exist_ok=True) - submission = Submission( - filename=image.filename, - password=password, - deep_analysis=deep_analysis, - hash=submission_hash, - status="pending", - date=time.time(), - image_hash=sub_img.hash, # type: ignore - ) - db.session.add(submission) # pylint: disable=no-member - db.session.commit() # pylint: disable=no-member - # Start the analysis jobs + if not submission: + submission = Submission( + filename=image.filename, + password=password, + deep_analysis=deep_analysis, + hash=submission_hash, + status="pending", + date=time.time(), + image_hash=sub_img.hash, + ) + db.session.add(submission) + db.session.commit() + queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) - return jsonify({"submission_hash": submission.hash}), 200 + return jsonify({"submission_hash": submission.hash}), 20 + + + + + # # If submission already exists, return submission hash + # if submission_path.exists(): + # submission: Submission = Submission.query.filter_by( + # hash=submission_hash + # ).first() + # return jsonify({"submission_hash": submission.hash}), 200 + + # # If image is new, create a new Image entry + # new_img_path = RESULT_FOLDER / img_hash / image_name + # if not new_img_path.parent.exists(): + # new_img_path.parent.mkdir(parents=True, exist_ok=True) + # with open(new_img_path, "wb") as f: # Write file to disk + # f.write(image_data) + + # new_img = Image( + # file=str(new_img_path), + # hash=img_hash, + # size=len(image_data), + # upload_count=0, + # first_submission_date=datetime.now(timezone.utc), + # last_submission_date=datetime.now(timezone.utc), + # ) + # db.session.add(new_img) # pylint: disable=no-member + # db.session.commit() # pylint: disable=no-member + + # sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore + # sub_img.upload_count += 1 + # db.session.commit() # pylint: disable=no-member + + # # Create new Submission entry + # submission_path.mkdir(parents=True, exist_ok=True) + # submission = Submission( + # filename=image.filename, + # password=password, + # deep_analysis=deep_analysis, + # hash=submission_hash, + # status="pending", + # date=time.time(), + # image_hash=sub_img.hash, # type: ignore + # ) + # db.session.add(submission) # pylint: disable=no-member + + # db.session.commit() # pylint: disable=no-member + # # Start the analysis jobs + # queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) + + # return jsonify({"submission_hash": submission.hash}), 200 @app.route("/status/", methods=["GET"]) From 8e72911f54beec57b3abf04a9641e3b220808b38 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Tue, 30 Dec 2025 11:48:20 +0530 Subject: [PATCH 13/18] Fix upload_image response status code from 20 to 200 --- aperisolve/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index 9705156..94b3f54 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -163,7 +163,7 @@ def upload_image() -> tuple[Response, int]: queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) - return jsonify({"submission_hash": submission.hash}), 20 + return jsonify({"submission_hash": submission.hash}), 200 From 4a380d277e883163ae304f83d152810af2de76c6 Mon Sep 17 00:00:00 2001 From: aradhyacp Date: Tue, 30 Dec 2025 11:59:02 +0530 Subject: [PATCH 14/18] simple documentation comments --- aperisolve/app.py | 65 ++++++++++------------------------------------- 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index 94b3f54..0ebc831 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -112,25 +112,33 @@ def upload_image() -> tuple[Response, int]: submission_hash = hashlib.md5(submission_data).hexdigest() submission_path = RESULT_FOLDER / img_hash / submission_hash + # Check if hash is present on the DB submission: Submission = Submission.query.filter_by( hash=submission_hash ).first() + # Only return if BOTH the file exists AND the DB entry exists if submission_path.exists() and submission is not None: return jsonify({"submission_hash": submission.hash}),200 + + # Self-Healing Logic new_img_path = RESULT_FOLDER / img_hash / image_name + # Try to find image in DB by hash sub_img = Image.query.filter_by( hash=img_hash ).first() + # If DB entry is missing (even if file exists on disk), create it! if sub_img is None: if not new_img_path.parent.exists() or not new_img_path.exists(): new_img_path.parent.mkdir(parents=True, exist_ok=True) with open(new_img_path,"wb") as f: f.write(image_data) + + # Recreate the missing Database Entry sub_img = Image( file=str(new_img_path), hash=img_hash, @@ -142,12 +150,15 @@ def upload_image() -> tuple[Response, int]: db.session.add(sub_img) db.session.commit() - + # Recreate the missing Database Entry sub_img.upload_count += 1 db.session.commit() + # Create new Submission entry submission_path.mkdir(parents=True, exist_ok=True) + # Only create a new submission if it does not already exist + # New submission for new image if not submission: submission = Submission( filename=image.filename, @@ -161,62 +172,14 @@ def upload_image() -> tuple[Response, int]: db.session.add(submission) db.session.commit() + # Start the analysis jobs queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) + # Return submission identifier return jsonify({"submission_hash": submission.hash}), 200 - - # # If submission already exists, return submission hash - # if submission_path.exists(): - # submission: Submission = Submission.query.filter_by( - # hash=submission_hash - # ).first() - # return jsonify({"submission_hash": submission.hash}), 200 - - # # If image is new, create a new Image entry - # new_img_path = RESULT_FOLDER / img_hash / image_name - # if not new_img_path.parent.exists(): - # new_img_path.parent.mkdir(parents=True, exist_ok=True) - # with open(new_img_path, "wb") as f: # Write file to disk - # f.write(image_data) - - # new_img = Image( - # file=str(new_img_path), - # hash=img_hash, - # size=len(image_data), - # upload_count=0, - # first_submission_date=datetime.now(timezone.utc), - # last_submission_date=datetime.now(timezone.utc), - # ) - # db.session.add(new_img) # pylint: disable=no-member - # db.session.commit() # pylint: disable=no-member - - # sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore - # sub_img.upload_count += 1 - # db.session.commit() # pylint: disable=no-member - - # # Create new Submission entry - # submission_path.mkdir(parents=True, exist_ok=True) - # submission = Submission( - # filename=image.filename, - # password=password, - # deep_analysis=deep_analysis, - # hash=submission_hash, - # status="pending", - # date=time.time(), - # image_hash=sub_img.hash, # type: ignore - # ) - # db.session.add(submission) # pylint: disable=no-member - - # db.session.commit() # pylint: disable=no-member - # # Start the analysis jobs - # queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) - - # return jsonify({"submission_hash": submission.hash}), 200 - - @app.route("/status/", methods=["GET"]) def get_status(hash_val: str) -> Response: """Get the processing status of a submission.""" From a0c616bc78622d4e4ee572d307e90af0a97994d5 Mon Sep 17 00:00:00 2001 From: Aradhya C P <135510032+aradhyacp@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:00:05 +0530 Subject: [PATCH 15/18] Update aperisolve/app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aperisolve/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index 0ebc831..55f321f 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -150,7 +150,7 @@ def upload_image() -> tuple[Response, int]: db.session.add(sub_img) db.session.commit() - # Recreate the missing Database Entry + # Increment upload count for the image sub_img.upload_count += 1 db.session.commit() From 9f21ac429dceb403cf19314a2e66a37ca055d6d9 Mon Sep 17 00:00:00 2001 From: Aradhya C P <135510032+aradhyacp@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:00:12 +0530 Subject: [PATCH 16/18] Update aperisolve/app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aperisolve/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index 55f321f..f820226 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -134,7 +134,7 @@ def upload_image() -> tuple[Response, int]: if sub_img is None: if not new_img_path.parent.exists() or not new_img_path.exists(): new_img_path.parent.mkdir(parents=True, exist_ok=True) - with open(new_img_path,"wb") as f: + with open(new_img_path, "wb") as f: f.write(image_data) From 87df3cc629be451e7afb2cc17f447ee4a99480f2 Mon Sep 17 00:00:00 2001 From: Aradhya C P <135510032+aradhyacp@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:00:31 +0530 Subject: [PATCH 17/18] Update aperisolve/app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aperisolve/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index f820226..75359d2 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -119,7 +119,7 @@ def upload_image() -> tuple[Response, int]: # Only return if BOTH the file exists AND the DB entry exists if submission_path.exists() and submission is not None: - return jsonify({"submission_hash": submission.hash}),200 + return jsonify({"submission_hash": submission.hash}), 200 # Self-Healing Logic From b15dca9c537d418dba39567761b38198207ed5d1 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Tue, 30 Dec 2025 16:57:08 +0100 Subject: [PATCH 18/18] Fix with fs remove --- aperisolve/app.py | 80 ++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/aperisolve/app.py b/aperisolve/app.py index 75359d2..f9e2ff9 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -6,6 +6,7 @@ import os import time from datetime import datetime, timezone from pathlib import Path +from shutil import rmtree from typing import Any, Optional from flask import Flask, Response, abort, jsonify, render_template, request, send_file @@ -112,34 +113,33 @@ def upload_image() -> tuple[Response, int]: submission_hash = hashlib.md5(submission_data).hexdigest() submission_path = RESULT_FOLDER / img_hash / submission_hash - # Check if hash is present on the DB - submission: Submission = Submission.query.filter_by( - hash=submission_hash - ).first() + # If submission already exists, return submission hash + if submission_path.exists(): + submission: Submission = Submission.query.filter_by( + hash=submission_hash + ).first() - # Only return if BOTH the file exists AND the DB entry exists - if submission_path.exists() and submission is not None: - return jsonify({"submission_hash": submission.hash}), 200 - + if submission is not None: # File exist but not in DB (should not happen), + rmtree(submission_path) # Removing file and continue processing as new + db.session.delete(submission) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + else: + return jsonify({"submission_hash": submission.hash}), 200 - # Self-Healing Logic + # If image is new, create a new Image entry new_img_path = RESULT_FOLDER / img_hash / image_name + sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore - # Try to find image in DB by hash - sub_img = Image.query.filter_by( - hash=img_hash - ).first() + if new_img_path.parent.exists() and sub_img is None: + # Inconsistent state: image file exists but no DB entry + rmtree(new_img_path.parent) # Remove the existing files and continue - # If DB entry is missing (even if file exists on disk), create it! - if sub_img is None: - if not new_img_path.parent.exists() or not new_img_path.exists(): - new_img_path.parent.mkdir(parents=True, exist_ok=True) - with open(new_img_path, "wb") as f: - f.write(image_data) + if not new_img_path.parent.exists(): + new_img_path.parent.mkdir(parents=True, exist_ok=True) + with open(new_img_path, "wb") as f: # Write file to disk + f.write(image_data) - - # Recreate the missing Database Entry - sub_img = Image( + new_img = Image( file=str(new_img_path), hash=img_hash, size=len(image_data), @@ -147,39 +147,33 @@ def upload_image() -> tuple[Response, int]: first_submission_date=datetime.now(timezone.utc), last_submission_date=datetime.now(timezone.utc), ) - db.session.add(sub_img) - db.session.commit() + db.session.add(new_img) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member - # Increment upload count for the image + sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore sub_img.upload_count += 1 - db.session.commit() + db.session.commit() # pylint: disable=no-member # Create new Submission entry submission_path.mkdir(parents=True, exist_ok=True) + submission = Submission( + filename=image.filename, + password=password, + deep_analysis=deep_analysis, + hash=submission_hash, + status="pending", + date=time.time(), + image_hash=sub_img.hash, # type: ignore + ) + db.session.add(submission) # pylint: disable=no-member - # Only create a new submission if it does not already exist - # New submission for new image - if not submission: - submission = Submission( - filename=image.filename, - password=password, - deep_analysis=deep_analysis, - hash=submission_hash, - status="pending", - date=time.time(), - image_hash=sub_img.hash, - ) - db.session.add(submission) - db.session.commit() - + db.session.commit() # pylint: disable=no-member # Start the analysis jobs queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) - # Return submission identifier return jsonify({"submission_hash": submission.hash}), 200 - @app.route("/status/", methods=["GET"]) def get_status(hash_val: str) -> Response: """Get the processing status of a submission."""