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
CLEAR_AT_RESTART=1
SKIP_IHDR_FILL=0
REMOVAL_MIN_AGE_SECONDS=300
# Flask configuration
FLASK_DEBUG=0
+3 -3
View File
@@ -1,7 +1,7 @@
# Python
.env
.venv/
__pycache__/
.mypy_cache/
.vscode/
__pycache__/
results/
.vscode
removed_images/
+95 -1
View File
@@ -6,6 +6,7 @@
import hashlib
import json
import os
import shutil
import time
from datetime import datetime, timezone
from pathlib import Path
@@ -20,7 +21,13 @@ from .utils.sentry import 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
@@ -217,6 +224,7 @@ def create_app() -> Flask:
"last_submission_date": image.last_submission_date,
"upload_count": image.upload_count,
"passwords": passwords,
"removal_min_age_seconds": REMOVAL_MIN_AGE_SECONDS,
}
) # type: ignore
@@ -251,6 +259,92 @@ def create_app() -> Flask:
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/<hash_val>/<img_name>")
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"]
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_STORE_TIME = int(getenv("MAX_STORE_TIME", "259200")) # 3 days by default
CLEAR_AT_RESTART = int(getenv("CLEAR_AT_RESTART", "0"))
+29
View File
@@ -162,6 +162,35 @@ form {
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 ======== */
input,
textarea,
+102
View File
@@ -230,6 +230,108 @@ async function fetchImageInfo(submission_hash) {
.join(", ");
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-infos" class="mb-3"></div>
<div id="result-analyzers" class="mb-3"></div>
<div id="result-removal" class="mb-3"></div>
</div>
{% endblock %}
{% endblock %}
+2 -1
View File
@@ -3,10 +3,11 @@
<div id="result" class="mt-5">
<div id="result-infos" class="mb-3"></div>
<div id="result-analyzers" class="mb-3"></div>
<div id="result-removal" class="mb-3"></div>
</div>
{% endblock %}
{% block footer_content %}
<script>
pollStatus("{{ hash_val }}");
</script>
{% endblock %}
{% endblock %}