Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e5ee7b5ea | |||
| 2528e79895 | |||
| 542caa520f | |||
| 5136e40587 |
+3
-1
@@ -5484,7 +5484,7 @@ def upload_item():
|
||||
fs = get_gridfs()
|
||||
|
||||
if cfg.MODULES.is_enabled('library') and sanitize_form_value(request.form.get('item_type_input', '')) != "other":
|
||||
success_redirect_endpoint = 'library'
|
||||
success_redirect_endpoint = 'library_view'
|
||||
else:
|
||||
success_redirect_endpoint = 'home_admin'
|
||||
|
||||
@@ -5828,7 +5828,9 @@ def upload_item():
|
||||
|
||||
if item_id:
|
||||
success_msg = f'Element wurde erfolgreich hinzugefügt ({len(created_item_ids)} erstellt)'
|
||||
fs = get_gridfs() # Deine GridFS Verbindung
|
||||
|
||||
it.cleanup_orphaned_images(fs, dry_run=True)
|
||||
if upload_mode == 'library':
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
|
||||
@@ -25,6 +25,7 @@ import datetime
|
||||
import Web.modules.database.settings as cfg
|
||||
from Web.modules.database.settings import MongoClient
|
||||
import Web.modules.inventarsystem.data_protection as dp
|
||||
import logging
|
||||
|
||||
|
||||
def is_library_item(item):
|
||||
@@ -1292,4 +1293,137 @@ def sync_group_codes(primary_obj_id, base_code, individual_codes_list):
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error syncing group codes: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
||||
|
||||
|
||||
def cleanup_orphaned_images(fs, dry_run=True):
|
||||
"""
|
||||
Finds images in GridFS that are no longer referenced by any item
|
||||
and optionally deletes them.
|
||||
|
||||
Supported item fields:
|
||||
- book_cover_image
|
||||
- image
|
||||
- images
|
||||
|
||||
The fields are expected to contain GridFS file ObjectIds.
|
||||
|
||||
:param fs: GridFS instance, e.g. gridfs.GridFS(db)
|
||||
:param dry_run: If True, only reports what would be deleted.
|
||||
If False, actually deletes the files.
|
||||
"""
|
||||
|
||||
logging.info("Starte Cleanup-Skript...")
|
||||
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
|
||||
try:
|
||||
db = client[cfg.MONGODB_DB]
|
||||
items_collection = db["items"]
|
||||
|
||||
referenced_files = set()
|
||||
|
||||
for item in items_collection.find(
|
||||
{},
|
||||
{
|
||||
"Images": 1
|
||||
}
|
||||
):
|
||||
images = item.get("Images")
|
||||
|
||||
if isinstance(images, list):
|
||||
for img in images:
|
||||
if img:
|
||||
referenced_files.add(img)
|
||||
|
||||
referenced_files.discard(None)
|
||||
|
||||
logging.info(
|
||||
f"{len(referenced_files)} referenzierte GridFS-Dateien gefunden."
|
||||
)
|
||||
|
||||
orphaned_files = []
|
||||
|
||||
for grid_file in fs.find():
|
||||
file_id = grid_file._id
|
||||
filename = grid_file.filename or ""
|
||||
|
||||
if not filename.lower().endswith(
|
||||
(".jpg", ".jpeg", ".png", ".gif", ".webp")
|
||||
):
|
||||
continue
|
||||
|
||||
if file_id not in referenced_files:
|
||||
orphaned_files.append(
|
||||
{
|
||||
"_id": file_id,
|
||||
"filename": filename,
|
||||
"upload_date": grid_file.upload_date,
|
||||
}
|
||||
)
|
||||
|
||||
logging.info(
|
||||
f"Gefundene verwaiste Bilder: {len(orphaned_files)}"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
logging.info(
|
||||
"--- DRY RUN AKTIV - Es wird nichts gelöscht ---"
|
||||
)
|
||||
|
||||
for file in orphaned_files:
|
||||
logging.info(
|
||||
f"Würde löschen: "
|
||||
f"{file['filename']} "
|
||||
f"(ID: {file['_id']}, "
|
||||
f"Hochgeladen: {file['upload_date']})"
|
||||
)
|
||||
|
||||
logging.info(
|
||||
"--- Setze dry_run=False, um physisch zu löschen ---"
|
||||
)
|
||||
|
||||
else:
|
||||
logging.warning("--- LÖSCHVORGANG AKTIV ---")
|
||||
|
||||
deleted_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for file in orphaned_files:
|
||||
try:
|
||||
fs.delete(file["_id"])
|
||||
|
||||
deleted_count += 1
|
||||
|
||||
logging.info(
|
||||
f"Gelöscht: {file['filename']} "
|
||||
f"(ID: {file['_id']})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
|
||||
logging.error(
|
||||
f"Fehler beim Löschen von "
|
||||
f"{file['filename']} "
|
||||
f"(ID: {file['_id']}): {e}"
|
||||
)
|
||||
|
||||
logging.info(
|
||||
f"Cleanup beendet. "
|
||||
f"{deleted_count} Bilder gelöscht, "
|
||||
f"{failed_count} Fehler."
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"referenced_count": len(referenced_files),
|
||||
"orphaned_count": len(orphaned_files),
|
||||
"dry_run": dry_run,
|
||||
}
|
||||
|
||||
finally:
|
||||
client.close()
|
||||
@@ -450,6 +450,58 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-gallery-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--ui-surface-soft, #e5e7eb);
|
||||
}
|
||||
|
||||
.item-image-wrapper {
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.item-image-wrapper:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.item-image-wrapper:hover .item-image {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
/* Fallback / No-Image Styling im Detail-Modal */
|
||||
.detail-no-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #d1d5db;
|
||||
color: #6b7280;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
|
||||
@@ -1331,40 +1383,34 @@
|
||||
detailContent.innerHTML = '<p>Lade Details...</p>';
|
||||
detailModal.style.display = 'flex';
|
||||
|
||||
// Sicherer Zugriff auf das Item mit Fallback
|
||||
const item = libraryItems.find(i => i._id === itemId);
|
||||
let mediaHtml = '';
|
||||
|
||||
if (item) {
|
||||
// Prüfe gängige Array-Namen aus dem Backend
|
||||
const imageArray = item.Images || item.Bilder || item.images;
|
||||
|
||||
if (Array.isArray(imageArray) && imageArray.length > 0) {
|
||||
const imagesHtml = imageArray.map(image => {
|
||||
// Direkter, robuster Pfad zur Upload-Route
|
||||
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`;
|
||||
|
||||
return `<img src="${imageSrc}" alt="${escapeHtml(item.Name || 'Medium')}" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
|
||||
}).join('');
|
||||
|
||||
mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Zusätzliche Details vom Backend laden
|
||||
fetch(`/api/item_detail/${itemId}`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error('Fehler beim Laden der Artikeldetails');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(html => {
|
||||
detailContent.innerHTML = mediaHtml + DOMPurify.sanitize(html);
|
||||
.then(data => {
|
||||
let mediaHtml = '';
|
||||
const imageArray = data.images || [];
|
||||
|
||||
if (Array.isArray(imageArray) && imageArray.length > 0) {
|
||||
const imagesHtml = imageArray.map(image => {
|
||||
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`;
|
||||
|
||||
return `<img src="${imageSrc}" alt="Buchcover / Bild" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
|
||||
}).join('');
|
||||
|
||||
mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`;
|
||||
}
|
||||
|
||||
detailContent.innerHTML = mediaHtml + data.html;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error loading detail:', err);
|
||||
detailContent.innerHTML = '<p style="color: red;">Entschuldigung, die Details konnten nicht geladen werden.</p>';
|
||||
.catch(error => {
|
||||
console.error('Error fetching item detail:', error);
|
||||
detailContent.innerHTML = '<p style="color: red;">Fehler beim Laden der Artikeldetails.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user