Removing image is working

This commit is contained in:
Zeecka
2026-01-09 16:28:11 +01:00
parent d106b1bb07
commit 1c0186fb9d
8 changed files with 236 additions and 6 deletions
+1
View File
@@ -4,6 +4,7 @@ MAX_PENDING_TIME=600
MAX_STORE_TIME=259200 MAX_STORE_TIME=259200
CLEAR_AT_RESTART=1 CLEAR_AT_RESTART=1
SKIP_IHDR_FILL=0 SKIP_IHDR_FILL=0
REMOVAL_MIN_AGE_SECONDS=300
# Flask configuration # Flask configuration
FLASK_DEBUG=0 FLASK_DEBUG=0
+3 -3
View File
@@ -1,7 +1,7 @@
# Python
.env .env
.venv/ .venv/
__pycache__/
.mypy_cache/ .mypy_cache/
.vscode/
__pycache__/
results/ results/
.vscode removed_images/
+95 -1
View File
@@ -6,6 +6,7 @@
import hashlib import hashlib
import json import json
import os import os
import shutil
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@@ -20,7 +21,13 @@ from .utils.sentry import initialize_sentry
initialize_sentry() initialize_sentry()
from .config import IMAGE_EXTENSIONS, RESULT_FOLDER, WORKER_FILES from .config import (
IMAGE_EXTENSIONS,
REMOVAL_MIN_AGE_SECONDS,
REMOVED_IMAGES_FOLDER,
RESULT_FOLDER,
WORKER_FILES,
)
from .models import Image, Submission, UploadLog, cleanup_old_entries, db from .models import Image, Submission, UploadLog, cleanup_old_entries, db
@@ -217,6 +224,7 @@ def create_app() -> Flask:
"last_submission_date": image.last_submission_date, "last_submission_date": image.last_submission_date,
"upload_count": image.upload_count, "upload_count": image.upload_count,
"passwords": passwords, "passwords": passwords,
"removal_min_age_seconds": REMOVAL_MIN_AGE_SECONDS,
} }
) # type: ignore ) # type: ignore
@@ -251,6 +259,92 @@ def create_app() -> Flask:
return send_file(output_file, as_attachment=True) return send_file(output_file, as_attachment=True)
@app.route("/remove/<hash_val>", methods=["POST"])
def remove_image(hash_val: str) -> tuple[Response, int]:
"""Remove an image and associated results if criteria are met."""
submission = Submission.query.get_or_404(hash_val) # type: ignore
image = Image.query.get_or_404(submission.image_hash) # type: ignore
# Check if image is at least 5 minutes old
current_time = time.time()
age_seconds = current_time - submission.date
if age_seconds < REMOVAL_MIN_AGE_SECONDS:
return (
jsonify(
{
"error": "Image must be at least 5 minutes old. "
f"Current age: {int(age_seconds)}s"
}
),
403,
)
# Get all IPs that uploaded this image
upload_logs = UploadLog.query.filter_by(image_hash=image.hash).all()
unique_ips = set()
for log in upload_logs:
if log.ip_address:
unique_ips.add(log.ip_address)
# Check if only one IP has uploaded this image
if len(unique_ips) > 1:
return (
jsonify(
{
"error": "Image uploaded from multiple IP addresses. Removal is not "
"allowed.",
"ip_count": len(unique_ips),
}
),
403,
)
# Create archive directory if it doesn't exist
REMOVED_IMAGES_FOLDER.mkdir(parents=True, exist_ok=True)
# Archive the original image (copy to removed_images folder with metadata)
original_image_path = Path(image.file)
if original_image_path.exists():
dt = datetime.now(timezone.utc).isoformat()
archive_filename = f"{image.hash}_{submission.hash}_{dt}{original_image_path.suffix}"
archive_path = REMOVED_IMAGES_FOLDER / archive_filename
try:
shutil.copy2(original_image_path, archive_path)
except Exception as e:
sentry_sdk.capture_exception(e)
return jsonify({"error": "Failed to archive image"}), 500
# Delete the results folder
result_path = RESULT_FOLDER / str(image.hash) / str(submission.hash)
try:
if result_path.exists():
shutil.rmtree(result_path)
except Exception as e:
sentry_sdk.capture_exception(e)
return jsonify({"error": "Failed to delete results"}), 500
# Delete submission from database
try:
db.session.delete(submission) # pylint: disable=no-member
# If this was the last submission for this image, delete the image entry too
if len(image.submissions) <= 1:
# Also delete the original image file
if original_image_path.exists():
original_image_path.unlink()
db.session.delete(image) # pylint: disable=no-member
db.session.commit() # pylint: disable=no-member
except Exception as e:
db.session.rollback() # pylint: disable=no-member
sentry_sdk.capture_exception(e)
return jsonify({"error": "Failed to update database"}), 500
# Note: UploadLog entries are intentionally kept for audit purposes
return jsonify({"message": "Image successfully removed"}), 200
@app.route("/image/<img_name>") @app.route("/image/<img_name>")
@app.route("/image/<hash_val>/<img_name>") @app.route("/image/<hash_val>/<img_name>")
def get_image(hash_val: Optional[str] = None, img_name: Optional[str] = None) -> Response: def get_image(hash_val: Optional[str] = None, img_name: Optional[str] = None) -> Response:
+2
View File
@@ -10,6 +10,8 @@ IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"]
WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize", "openstego", "pcrt"] WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize", "openstego", "pcrt"]
RESULT_FOLDER = Path(__file__).parent.resolve() / "results" RESULT_FOLDER = Path(__file__).parent.resolve() / "results"
REMOVED_IMAGES_FOLDER = Path(__file__).parent.resolve() / "removed_images"
REMOVAL_MIN_AGE_SECONDS = int(getenv("REMOVAL_MIN_AGE_SECONDS", "300")) # 5 minutes
MAX_PENDING_TIME = int(getenv("MAX_PENDING_TIME", "600")) # 10 minutes by default MAX_PENDING_TIME = int(getenv("MAX_PENDING_TIME", "600")) # 10 minutes by default
MAX_STORE_TIME = int(getenv("MAX_STORE_TIME", "259200")) # 3 days by default MAX_STORE_TIME = int(getenv("MAX_STORE_TIME", "259200")) # 3 days by default
CLEAR_AT_RESTART = int(getenv("CLEAR_AT_RESTART", "0")) CLEAR_AT_RESTART = int(getenv("CLEAR_AT_RESTART", "0"))
+29
View File
@@ -162,6 +162,35 @@ form {
margin-top: 25px; margin-top: 25px;
} }
/* Removal card */
.removal-card {
padding: 20px;
margin-bottom: 20px;
border: 2px solid var(--theme-green);
background-color: rgba(159, 239, 0, 0.05);
}
.removal-card h3 {
margin-top: 0;
margin-bottom: 15px;
}
.removal-card p {
margin-bottom: 15px;
color: #e0e0e0;
}
.removal-card button:disabled {
background-color: #666;
cursor: not-allowed;
opacity: 0.6;
}
.removal-card #removal-status {
margin-top: 10px;
font-size: 0.95rem;
}
/* ======== Forms & Inputs ======== */ /* ======== Forms & Inputs ======== */
input, input,
textarea, textarea,
+102
View File
@@ -230,6 +230,108 @@ async function fetchImageInfo(submission_hash) {
.join(", "); .join(", ");
tableInfos.innerHTML += `<tr><td><i class="fa fa-key"></i> Common password(s):</td><td>${passwordList}</td></tr>`; tableInfos.innerHTML += `<tr><td><i class="fa fa-key"></i> Common password(s):</td><td>${passwordList}</td></tr>`;
} }
// Fetch removal status and update removal UI
await updateRemovalUI(submission_hash, infoData);
}
/**
* Image removal functionality
*/
let removalTimerIntervals = {};
async function updateRemovalUI(submission_hash, infoData) {
console.log("Updating removal UI...");
const resultRemovalDiv = document.getElementById("result-removal");
resultRemovalDiv.innerHTML = "";
try {
// Extract upload timestamp from first_submission_date
const uploadTime = new Date(infoData.first_submission_date).getTime();
const currentTime = new Date().getTime();
const ageSeconds = Math.floor((currentTime - uploadTime) / 1000);
const REMOVAL_MIN_AGE_SECONDS = infoData.removal_min_age_seconds || 300;
const removalContainer = document.createElement("div");
removalContainer.className = "card removal-card";
resultRemovalDiv.appendChild(removalContainer);
if (ageSeconds < REMOVAL_MIN_AGE_SECONDS) {
const remainingSeconds = REMOVAL_MIN_AGE_SECONDS - ageSeconds;
removalContainer.innerHTML = `
<h3><i class="fa fa-trash"></i> Remove Image</h3>
<p>Available in <span id="removal-countdown">${remainingSeconds}</span> seconds</p>
<button class="btn btn-primary" disabled id="remove-btn">Remove Image</button>
`;
// Clear existing interval if any
if (removalTimerIntervals[submission_hash]) {
clearInterval(removalTimerIntervals[submission_hash]);
}
// Start countdown timer
removalTimerIntervals[submission_hash] = setInterval(() => {
const countdownEl = document.getElementById("removal-countdown");
if (countdownEl) {
const current = parseInt(countdownEl.textContent);
if (current > 1) {
countdownEl.textContent = current - 1;
} else {
clearInterval(removalTimerIntervals[submission_hash]);
updateRemovalUI(submission_hash, infoData);
}
}
}, 1000);
} else {
// Check for multiple IPs - we'll do this by attempting removal
removalContainer.innerHTML = `
<h3><i class="fa fa-trash"></i> Remove Image</h3>
<button class="btn btn-primary" id="remove-btn">Remove Image</button>
<p id="removal-status"></p>
`;
const removeBtn = document.getElementById("remove-btn");
removeBtn.addEventListener("click", async () => {
removeBtn.disabled = true;
const statusEl = document.getElementById("removal-status");
try {
const response = await fetch(`/remove/${submission_hash}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (response.ok) {
statusEl.innerHTML = `<span class="text-success"><i class="fa fa-check"></i> Image successfully removed</span>`;
removeBtn.style.display = "none";
setTimeout(() => { document.location = '/'; }, 2000);
} else if (response.status === 403) {
if (data.error.includes("multiple IP")) {
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-warning"></i> ${escapeHtml(data.error)}</span>`;
} else {
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-warning"></i> ${escapeHtml(data.error)}</span>`;
}
removeBtn.disabled = false;
} else {
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-exclamation-circle"></i> Error: ${escapeHtml(data.error)}</span>`;
removeBtn.disabled = false;
}
} catch (error) {
statusEl.innerHTML = `<span class="text-danger"><i class="fa fa-exclamation-circle"></i> Network error occurred</span>`;
removeBtn.disabled = false;
console.error("Removal error:", error);
}
});
}
} catch (error) {
console.error("Error updating removal UI:", error);
}
} }
/** /**
+2 -1
View File
@@ -34,5 +34,6 @@
<div id="result" class="mt-5"> <div id="result" class="mt-5">
<div id="result-infos" class="mb-3"></div> <div id="result-infos" class="mb-3"></div>
<div id="result-analyzers" class="mb-3"></div> <div id="result-analyzers" class="mb-3"></div>
<div id="result-removal" class="mb-3"></div>
</div> </div>
{% endblock %} {% endblock %}
+2 -1
View File
@@ -3,10 +3,11 @@
<div id="result" class="mt-5"> <div id="result" class="mt-5">
<div id="result-infos" class="mb-3"></div> <div id="result-infos" class="mb-3"></div>
<div id="result-analyzers" class="mb-3"></div> <div id="result-analyzers" class="mb-3"></div>
<div id="result-removal" class="mb-3"></div>
</div> </div>
{% endblock %} {% endblock %}
{% block footer_content %} {% block footer_content %}
<script> <script>
pollStatus("{{ hash_val }}"); pollStatus("{{ hash_val }}");
</script> </script>
{% endblock %} {% endblock %}