Compare commits

..

1 Commits

Author SHA1 Message Date
Aiirondev_dev d6d6858002 new implementation of file uploading 2026-08-01 10:34:59 +02:00
2 changed files with 157 additions and 5347 deletions
+157 -655
View File
@@ -2569,7 +2569,6 @@ def uploaded_file(filename):
if denied:
return denied
# 1. Try serving from MongoDB GridFS first
try:
fs = get_gridfs()
grid_out = fs.find_one({'filename': filename})
@@ -2582,15 +2581,6 @@ def uploaded_file(filename):
except Exception as grid_err:
app.logger.warning(f"GridFS lookup failed for {filename}: {grid_err}")
# 2. Fallback to production path for legacy local files
prod_path = "/opt/Inventarsystem/Web/uploads"
dev_path = app.config['UPLOAD_FOLDER']
if os.path.exists(os.path.join(prod_path, filename)):
return send_from_directory(prod_path, filename)
# 3. Fallback to development path
if os.path.exists(os.path.join(dev_path, filename)):
return send_from_directory(dev_path, filename)
# 4. Use a placeholder image if file not found - first try SVG, then PNG
svg_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg')
png_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
@@ -5171,7 +5161,6 @@ def upload_item():
Returns:
flask.Response: Redirect to admin homepage
"""
# Check if the user is authenticated
if 'username' not in session:
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
@@ -5182,7 +5171,9 @@ def upload_item():
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
if not _action_access_allowed(permissions, 'can_insert'):
return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403
fs = get_gridfs()
can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
if can_access_admin_home:
success_redirect_endpoint = 'home_admin'
@@ -5269,14 +5260,9 @@ def upload_item():
duplicate_images = mobile_data.get('duplicate_images', [])
except json.JSONDecodeError as e:
app.logger.error(f"Error parsing mobile data: {str(e)}")
except Exception as e:
error_msg = f"Fehler beim Verarbeiten der Formulardaten"
app.logger.error(error_msg)
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error')
return redirect(url_for(success_redirect_endpoint))
except:
flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error')
return redirect(url_for(success_redirect_endpoint))
# Expand special "all values" selections for predefined filters.
filter_upload = expand_filter_selection(filter_upload, 1)
@@ -5328,10 +5314,6 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 1. PARSE AND UNIFY CODES
# -------------------------------------------------------------------
# Assuming `code_4` might come in as a single string or a list depending on your form setup
primary_code_raw = request.form.get('code_4', '').strip()
individual_codes_raw = request.form.get('individual_codes', '').strip()
@@ -5341,19 +5323,12 @@ def upload_item():
all_item_codes.append(primary_code_raw)
if individual_codes_raw:
# Split textarea by newlines, clean whitespace, and ignore empty lines
extra_codes = [c.strip() for c in individual_codes_raw.replace('\r', '').split('\n') if c.strip()]
# Add to master list, ensuring no duplicates within the submitted list itself
for c in extra_codes:
if c not in all_item_codes:
all_item_codes.append(c)
# -------------------------------------------------------------------
# 2. OVERRIDE ITEM COUNT
# -------------------------------------------------------------------
# If the user scanned/generated codes, the amount of codes IS the item count.
# Otherwise, fallback to a manual item_count field if it exists.
if len(all_item_codes) > 0:
item_count = len(all_item_codes)
else:
@@ -5362,9 +5337,6 @@ def upload_item():
except ValueError:
item_count = 1
# -------------------------------------------------------------------
# 3. IMAGE VALIDATION
# -------------------------------------------------------------------
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile:
@@ -5373,14 +5345,9 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 4. UNIFIED DB UNIQUENESS VALIDATION
# -------------------------------------------------------------------
# Validate every code in our master list against the database
for code in all_item_codes:
if not it.is_code_unique(code):
app.logger.info(f"DEBUG: Code '{code}' is not unique.")
# Special case: If they only uploaded ONE item, redirect them to that existing item
if item_count == 1:
existing_item = it._get_items_collection().find_one({"code_4": code})
if existing_item:
@@ -5395,8 +5362,7 @@ def upload_item():
flash(error_msg, 'info')
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
# If multiple items are being uploaded, just throw a standard error
error_msg = f'Der Code "{code}" wird bereits von einem anderen Artikel verwendet. Bitte überprüfen Sie Ihre Eingaben.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
@@ -5404,9 +5370,6 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# -------------------------------------------------------------------
# 5. BATCH CODE GENERATOR (Remains mostly unchanged)
# -------------------------------------------------------------------
def generate_unique_batch_code(base_code, position):
"""Generate a unique code for every item in a batch if no specific code is provided."""
if not base_code:
@@ -5424,7 +5387,6 @@ def upload_item():
suffix += 1
return None
# Process any new uploaded images with robust error handling
image_filenames = []
processed_count = 0
error_count = 0
@@ -5433,650 +5395,190 @@ def upload_item():
# Create a structured log entry for upload session
upload_session_id = str(uuid.uuid4())[:8]
app.logger.info(f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}")
# Ensure all required directories exist
for directory in [app.config['UPLOAD_FOLDER']]:
try:
os.makedirs(directory, exist_ok=True)
except Exception as e:
app.logger.error(f"Failed to create directory {directory}: {str(e)}")
# Process each image independently
for index, image in enumerate(images):
# In library mode, skip manual image uploads (use only book_cover_image from ISBN fetch)
if upload_mode == 'library':
app.logger.info(f"[Library Mode] Skipping manual image upload {index+1}/{len(images)}")
app.logger.info(f"[Upload {upload_session_id}] Skipping manual upload (Library Mode)")
skipped_count += 1
continue
image_log_prefix = f"[Upload {upload_session_id}][Image {index+1}/{len(images)}]"
if not image or not image.filename or image.filename == '':
app.logger.warning(f"{image_log_prefix} Empty file or filename")
if not image or not image.filename:
skipped_count += 1
continue
# Get file extension for special handling
_, file_ext = os.path.splitext(image.filename.lower())
is_png = file_ext.lower() == '.png'
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Detected PNG file: {image.filename}")
# Check file size
image.seek(0, os.SEEK_END)
file_size = image.tell() / (1024 * 1024) # Size in MB
image.seek(0) # Reset file pointer
app.logger.info(f"PNG DEBUG: {image_log_prefix} PNG file size: {file_size:.2f}MB")
# Check first few bytes for PNG signature and analyze header
header_bytes = image.read(64) # Read more for thorough analysis
image.seek(0) # Reset pointer
png_signature = b'\x89PNG\r\n\x1a\n'
is_valid_signature = header_bytes.startswith(png_signature)
# Create a hex dump of header for debugging
hex_dump = ' '.join([f"{b:02x}" for b in header_bytes[:32]])
app.logger.info(f"PNG DEBUG: {image_log_prefix} PNG header hex: {hex_dump}")
app.logger.info(f"PNG DEBUG: {image_log_prefix} PNG signature valid: {is_valid_signature}, bytes: {header_bytes[:8]!r}")
# Analyze PNG chunks if signature is valid
if is_valid_signature:
try:
# Look for IHDR chunk that should follow the signature
if header_bytes[8:12] == b'IHDR':
# Extract width and height from IHDR chunk (bytes 16-23)
import struct
width = struct.unpack('>I', header_bytes[16:20])[0]
height = struct.unpack('>I', header_bytes[20:24])[0]
bit_depth = header_bytes[24]
color_type = header_bytes[25]
app.logger.info(f"PNG DEBUG: {image_log_prefix} PNG dimensions from header: {width}x{height}, bit depth: {bit_depth}, color type: {color_type}")
else:
app.logger.warning(f"PNG DEBUG: {image_log_prefix} Expected IHDR chunk not found. Found: {header_bytes[8:12]!r}")
except Exception as chunk_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error analyzing PNG chunks: {str(chunk_err)}")
else:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Invalid PNG signature!")
image_log_prefix = f"[Upload {upload_session_id}][Image {index + 1}/{len(images)}]"
app.logger.info(f"{image_log_prefix} Processing: {image.filename}")
try:
# Comprehensive file validation with detailed logging
# 1. Validation
is_allowed, error_message = allowed_file(image.filename, image, max_size_mb=cfg.IMAGE_MAX_UPLOAD_MB)
if not is_allowed:
app.logger.warning(f"{image_log_prefix} Validation failed: {error_message}")
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} PNG validation failed: {error_message}")
skipped_count += 1
if not is_mobile:
flash(error_message, 'error')
continue
# Get the file extension for content type determination
secure_name = secure_filename(image.filename)
_, ext_part = os.path.splitext(secure_name)
is_png = ext_part.lower() == '.png'
# Generate a completely unique filename using UUID
unique_id = str(uuid.uuid4())
timestamp = time.strftime("%Y%m%d%H%M%S")
# New filename format with UUID to ensure uniqueness
saved_filename = f"{unique_id}_{timestamp}{ext_part}"
app.logger.info(f"{image_log_prefix} Assigned unique filename: {saved_filename}")
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Creating PNG with filename: {saved_filename}")
# For iOS devices, we need special handling for the file save
if is_ios:
app.logger.info(f"{image_log_prefix} Using iOS-specific file handling")
# Save to a temporary file first to avoid iOS stream issues
temp_path = os.path.join(app.config['UPLOAD_FOLDER'], f"temp_{saved_filename}")
try:
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Using iOS PNG save method")
# Before saving, verify the file content again
try:
image.seek(0)
pre_save_data = image.read(16)
image.seek(0)
pre_save_hex = ' '.join([f"{b:02x}" for b in pre_save_data])
app.logger.info(f"PNG DEBUG: {image_log_prefix} Pre-save data: {pre_save_hex}")
except Exception as pre_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error checking pre-save data: {str(pre_err)}")
# For PNGs, try a direct binary save first
if is_png:
try:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Attempting binary save for iOS PNG")
image.seek(0)
png_data = image.read()
with open(temp_path, 'wb') as f:
f.write(png_data)
app.logger.info(f"PNG DEBUG: {image_log_prefix} Binary write complete, size: {len(png_data)} bytes")
except Exception as bin_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Binary write failed: {str(bin_err)}")
# Fall back to normal save
image.seek(0)
image.save(temp_path)
else:
image.save(temp_path)
# Validate the saved file
if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
if is_png:
file_size = os.path.getsize(temp_path)
app.logger.info(f"PNG DEBUG: {image_log_prefix} Temp PNG file saved successfully: {file_size/1024:.1f}KB")
# Verify it's a valid PNG
try:
with open(temp_path, 'rb') as f:
png_header = f.read(16)
png_signature = b'\x89PNG\r\n\x1a\n'
is_valid = png_header.startswith(png_signature)
header_hex = ' '.join([f"{b:02x}" for b in png_header])
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved file header: {header_hex}")
if not is_valid:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Invalid PNG signature in saved file!")
else:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Valid PNG signature confirmed in saved file")
# Try opening with PIL to confirm it's valid
try:
with Image.open(temp_path) as img:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved PNG validates with PIL: {img.format} {img.size}")
except Exception as pil_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Saved PNG fails PIL validation: {str(pil_err)}")
except Exception as verify_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error verifying PNG: {str(verify_err)}")
# Rename to the final filename
final_path = os.path.join(app.config['UPLOAD_FOLDER'], saved_filename)
os.rename(temp_path, final_path)
app.logger.info(f"{image_log_prefix} Successfully saved via iOS handler: {os.path.getsize(final_path)/1024:.1f}KB")
else:
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Failed to save temp PNG file (zero size or missing)")
raise Exception("Failed to save image file (zero size or missing)")
except Exception as e:
app.logger.error(f"{image_log_prefix} iOS save failed: {str(e)}")
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} iOS PNG save failed: {str(e)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(e).__name__}")
# Log full traceback for PNG errors
import io
tb_output = io.StringIO()
traceback.print_exc(file=tb_output)
app.logger.error(f"PNG DEBUG: {image_log_prefix} Full traceback:\n{tb_output.getvalue()}")
# Try regular save as fallback
try:
image.seek(0) # Reset file pointer
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Attempting fallback PNG save method")
image.save(os.path.join(app.config['UPLOAD_FOLDER'], saved_filename))
app.logger.info(f"{image_log_prefix} Fallback save successful")
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Fallback PNG save successful")
except Exception as fallback_err:
app.logger.error(f"{image_log_prefix} Fallback save also failed: {str(fallback_err)}")
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Fallback PNG save also failed: {str(fallback_err)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(fallback_err).__name__}")
error_count += 1
continue
else:
# Regular file save for non-iOS devices
try:
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Using standard PNG save method")
# Check file content before saving
try:
image.seek(0)
pre_save_data = image.read(16)
image.seek(0)
pre_save_hex = ' '.join([f"{b:02x}" for b in pre_save_data])
app.logger.info(f"PNG DEBUG: {image_log_prefix} Pre-save data: {pre_save_hex}")
# Check if it's really a PNG
png_signature = b'\x89PNG\r\n\x1a\n'
if not pre_save_data.startswith(png_signature):
app.logger.error(f"PNG DEBUG: {image_log_prefix} File does not have valid PNG signature before save!")
except Exception as pre_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error checking pre-save data: {str(pre_err)}")
# Try an alternative saving method for PNGs with detailed error tracking
try:
# Read the image data directly
image.seek(0)
image_data = image.read()
app.logger.info(f"PNG DEBUG: {image_log_prefix} Read {len(image_data)} bytes of PNG data")
# Write it manually to file
save_path = os.path.join(app.config['UPLOAD_FOLDER'], saved_filename)
with open(save_path, 'wb') as f:
f.write(image_data)
file_size = os.path.getsize(save_path)
app.logger.info(f"PNG DEBUG: {image_log_prefix} Direct binary PNG write successful: {file_size/1024:.1f}KB")
# Verify the saved file
try:
with open(save_path, 'rb') as f:
saved_header = f.read(16)
header_hex = ' '.join([f"{b:02x}" for b in saved_header])
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved PNG header: {header_hex}")
is_valid = saved_header.startswith(png_signature)
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved PNG has valid signature: {is_valid}")
if is_valid:
# Additional validation with PIL
try:
with Image.open(save_path) as img:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved PNG validates with PIL: {img.format} {img.size}")
except Exception as pil_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Saved PNG fails PIL validation: {str(pil_err)}")
except Exception as verify_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error verifying saved PNG: {str(verify_err)}")
except Exception as png_write_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Direct PNG write failed: {str(png_write_err)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(png_write_err).__name__}")
# Log traceback for PNG errors
import io
tb_output = io.StringIO()
traceback.print_exc(file=tb_output)
app.logger.error(f"PNG DEBUG: {image_log_prefix} Binary write traceback:\n{tb_output.getvalue()}")
# Fall back to standard method
app.logger.info(f"PNG DEBUG: {image_log_prefix} Attempting standard PIL save method")
image.seek(0)
image.save(os.path.join(app.config['UPLOAD_FOLDER'], saved_filename))
else:
# Standard save for non-PNG files
image.save(os.path.join(app.config['UPLOAD_FOLDER'], saved_filename))
app.logger.info(f"{image_log_prefix} Standard save successful")
except Exception as save_err:
app.logger.error(f"{image_log_prefix} Failed to save file: {str(save_err)}")
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Standard PNG save failed: {str(save_err)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(save_err).__name__}")
# Log traceback for PNG errors
import io
tb_output = io.StringIO()
traceback.print_exc(file=tb_output)
app.logger.error(f"PNG DEBUG: {image_log_prefix} Standard save traceback:\n{tb_output.getvalue()}")
# Try one last method - save as a different format
try:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Attempting last resort conversion to WebP")
image.seek(0)
# Try to convert PNG to WebP as a last resort
with Image.open(image) as img:
# Ensure RGBA for transparency
if img.mode != 'RGBA':
img = img.convert('RGBA')
webp_path = os.path.splitext(os.path.join(app.config['UPLOAD_FOLDER'], saved_filename))[0] + '.webp'
img.save(webp_path, 'WEBP')
app.logger.info(f"PNG DEBUG: {image_log_prefix} Successfully saved as WebP instead: {webp_path}")
# Update the saved_filename to reflect the new extension
saved_filename = os.path.basename(webp_path)
except Exception as webp_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Final WebP conversion failed: {str(webp_err)}")
error_count += 1
continue
# Verify the file was saved correctly
saved_path = os.path.join(app.config['UPLOAD_FOLDER'], saved_filename)
if not os.path.exists(saved_path) or os.path.getsize(saved_path) == 0:
app.logger.error(f"{image_log_prefix} Saved file is missing or empty: {saved_path}")
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Saved PNG is missing or empty: {saved_path}")
error_count += 1
continue
# Special verification for PNG files
if is_png:
try:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Verifying saved PNG: {saved_path}")
saved_size = os.path.getsize(saved_path) / 1024.0 # in KB
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved PNG size: {saved_size:.1f}KB")
# Check file integrity by trying to open it
try:
with Image.open(saved_path) as img:
png_width, png_height = img.size
png_mode = img.mode
app.logger.info(f"PNG DEBUG: {image_log_prefix} Saved PNG valid: {png_width}x{png_height}, mode: {png_mode}")
except Exception as png_verify_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} PNG verification failed: {str(png_verify_err)}")
# Try to fix by copying from the original
try:
image.seek(0)
with open(saved_path, 'wb') as f:
f.write(image.read())
app.logger.info(f"PNG DEBUG: {image_log_prefix} Attempted to fix PNG by direct copy")
except Exception as png_fix_err:
app.logger.error(f"PNG DEBUG: {image_log_prefix} PNG fix attempt failed: {str(png_fix_err)}")
except Exception as e:
app.logger.error(f"PNG DEBUG: {image_log_prefix} PNG verification error: {str(e)}")
# Generate optimized versions (thumbnails and previews)
optimization_success = False
try:
# Log original file size before optimization
original_path = os.path.join(app.config['UPLOAD_FOLDER'], saved_filename)
original_size = os.path.getsize(original_path)
# Get original image dimensions
original_dimensions = "unknown"
try:
with Image.open(original_path) as img:
original_dimensions = f"{img.width}x{img.height}"
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Original PNG dimensions: {original_dimensions}, mode: {img.mode}")
except Exception as dim_err:
app.logger.warning(f"{image_log_prefix} Could not get image dimensions: {str(dim_err)}")
if is_png:
app.logger.error(f"PNG DEBUG: {image_log_prefix} Could not get PNG dimensions: {str(dim_err)}")
app.logger.error(f"PNG DEBUG: {image_log_prefix} Error type: {type(dim_err).__name__}")
app.logger.info(f"{image_log_prefix} Starting optimization for {saved_filename} ({original_size/1024:.1f}KB, {original_dimensions})")
# PNG-specific optimization options
if is_png:
app.logger.info(f"PNG DEBUG: {image_log_prefix} Starting PNG optimization")
# For PNGs, we might need different parameters
optimization_result = generate_optimized_versions(
saved_filename,
max_original_width=500,
target_size_kb=100, # Higher target for PNGs to maintain transparency
debug_prefix=f"PNG DEBUG: {image_log_prefix}"
)
else:
# Standard optimization for non-PNG images
optimization_result = generate_optimized_versions(saved_filename, max_original_width=500, target_size_kb=80)
# Log file size after optimization
if optimization_result['success'] and optimization_result['original']:
optimized_name = optimization_result['original']
optimized_path = os.path.join(app.config['UPLOAD_FOLDER'], optimized_name)
if os.path.exists(optimized_path):
optimized_size = os.path.getsize(optimized_path)
reduction = (1 - (optimized_size / original_size)) * 100 if original_size > 0 else 0
# Get optimized dimensions
optimized_dimensions = "unknown"
try:
with Image.open(optimized_path) as img:
optimized_dimensions = f"{img.width}x{img.height}"
except Exception as dim_err:
app.logger.warning(f"{image_log_prefix} Could not get optimized dimensions: {str(dim_err)}")
app.logger.info(
f"{image_log_prefix} Optimization results:\n"
f" File: {saved_filename}{optimized_name}\n"
f" Size: {original_size/1024:.1f}KB → {optimized_size/1024:.1f}KB ({reduction:.1f}% reduction)\n"
f" Dimensions: {original_dimensions}{optimized_dimensions}"
)
# Use the optimized filename
saved_filename = optimized_name
else:
app.logger.warning(f"{image_log_prefix} Optimized file reported success but not found: {optimized_path}")
else:
app.logger.warning(f"{image_log_prefix} Optimization failed or returned no file")
except Exception as e:
app.logger.error(f"{image_log_prefix} Optimization failed: {str(e)}")
# No fallback thumbnail generation needed as we only use the main image
# Always add the filename to our list even if optimization failed
# We'll use the original in that case
image_filenames.append(saved_filename)
# 2. Read directly into memory (Bypasses OS-level file quirks and iOS temp-file bugs)
image_bytes = image.read()
# 3. Process, standardize, and optimize using Pillow in-memory
optimized_io = io.BytesIO()
with Image.open(io.BytesIO(image_bytes)) as img:
# Ensure safe color mode
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA')
# Standardize dimensions (e.g., max width 500px)
max_width = 500
if img.width > max_width:
ratio = max_width / img.width
new_size = (max_width, int(img.height * ratio))
img = img.resize(new_size, Image.Resampling.LANCZOS)
# Export as WebP to a memory buffer
# (WebP naturally handles transparency, drastically reduces size, and bypasses PNG signature corruption)
img.save(optimized_io, format='WEBP', quality=85, optimize=True)
optimized_io.seek(0)
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
file_id = fs.put(
optimized_io,
filename=new_filename,
content_type='image/webp',
metadata={
'original_filename': secure_name,
'upload_session': upload_session_id
}
)
image_filenames.append(file_id)
processed_count += 1
app.logger.info(f"{image_log_prefix} Successfully processed")
final_size_kb = len(optimized_io.getvalue()) / 1024
app.logger.info(
f"{image_log_prefix} Saved to GridFS as {new_filename} | ID: {file_id} | Size: {final_size_kb:.1f}KB")
except Exception as e:
app.logger.error(f"{image_log_prefix} Unexpected error: {str(e)}")
app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}")
error_count += 1
# Continue with the next image
# Log summary of upload session
app.logger.info(f"Upload session {upload_session_id} completed: {processed_count} processed, {error_count} errors, {skipped_count} skipped")
# Handle duplicate images if duplicating
# 1. Duplikate verarbeiten (Direkt via MongoDB / GridFS)
if duplicate_images:
app.logger.info(f"Processing {len(duplicate_images)} duplicate images: {duplicate_images}")
# For mobile browsers, we need to verify the duplicate images exist first
verified_duplicates = []
for dup_img in duplicate_images:
# Try looking in different paths
# Add all possible paths where images might be stored
dev_upload_path = app.config['UPLOAD_FOLDER']
prod_upload_path = '/var/Inventarsystem/Web/uploads'
# Also look for image variations with suffixes that might be in the path
name_part, ext_part = os.path.splitext(dup_img)
possible_filenames = [
dup_img,
f"{name_part}.webp", # Check WebP first
f"{name_part}.jpg", # In case it was converted to JPG
f"{name_part}.png", # In case it was saved as PNG
]
possible_paths = []
for filename in possible_filenames:
possible_paths.extend([
os.path.join(dev_upload_path, filename), # Development upload path
os.path.join(prod_upload_path, filename), # Production upload path
])
app.logger.info(f"Looking for duplicate image {dup_img} in paths: {possible_paths}")
# Try to find the original image
found = False
for path in possible_paths:
if os.path.exists(path) and os.path.isfile(path):
verified_duplicates.append((dup_img, path))
app.logger.info(f"Found duplicate image at: {path}")
found = True
break
if not found:
app.logger.warning(f"Duplicate image not found: {dup_img}")
# Try to find any image with a similar filename (removing size or resolution parts)
# This handles cases where the filename may have variations like "_800" suffix
base_name = os.path.splitext(dup_img)[0]
base_name = re.sub(r'_\d+$', '', base_name) # Remove trailing _NUMBER
if len(base_name) > 5: # Only if we have a meaningful base name
app.logger.info(f"Trying to find similar images with base name: {base_name}")
# Search in development directory
dev_files = os.listdir(app.config['UPLOAD_FOLDER']) if os.path.exists(app.config['UPLOAD_FOLDER']) else []
# Search in production directory
prod_path = "/var/Inventarsystem/Web/uploads"
prod_files = os.listdir(prod_path) if os.path.exists(prod_path) else []
# Combine all files
all_files = dev_files + prod_files
# Find similar files
for f in all_files:
if base_name in f:
img_path = os.path.join(app.config['UPLOAD_FOLDER'], f)
if not os.path.exists(img_path):
img_path = os.path.join(prod_path, f)
if os.path.exists(img_path) and os.path.isfile(img_path):
app.logger.info(f"Found similar image: {f} at {img_path}")
verified_duplicates.append((f, img_path))
found = True
break
# If we still can't find anything, just use a placeholder
if not found:
app.logger.warning(f"Could not find any similar image for: {dup_img}, will use placeholder")
# Create copies of verified images with new unique filenames
app.logger.info(f"Processing {len(duplicate_images)} duplicate images from GridFS: {duplicate_images}")
duplicate_image_copies = []
# Create a placeholder name for each image that wasn't found
placeholder_used = False
original_count = len(duplicate_images)
# Process each original image - either use found file or placeholder
for i, dup_img in enumerate(duplicate_images):
# Look for corresponding verified image
found_image = None
for verified_img, src_path in verified_duplicates:
if verified_img == dup_img:
found_image = (verified_img, src_path)
break
try:
# Generate a new unique filename (same for real or placeholder)
unique_id = str(uuid.uuid4())
timestamp = time.strftime("%Y%m%d%H%M%S")
_, ext_part = os.path.splitext(dup_img) if dup_img else '.jpg'
new_filename = f"{unique_id}_{timestamp}{ext_part}"
# If we found the image, copy it
if found_image:
dup_img, src_path = found_image
app.logger.info(f"Copying image {i+1}/{original_count} from {src_path} to {new_filename}")
# Copy the image file to the new name
dst_path = os.path.join(app.config['UPLOAD_FOLDER'], new_filename)
# Make sure the target directory exists
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
# Copy the file
shutil.copy2(src_path, dst_path)
# Verify the file was copied successfully
if os.path.exists(dst_path):
app.logger.info(f"Successfully copied image to {dst_path}")
else:
app.logger.error(f"Failed to copy image to {dst_path}")
# If copy fails, use placeholder
raise Exception("Copy failed - will use placeholder")
# Generate optimized versions (thumbnails and previews) for the new copy
try:
result = generate_optimized_versions(new_filename, max_original_width=500, target_size_kb=80)
app.logger.info(f"Generated optimized versions: {result}")
if result['success'] and result['original']:
new_filename = result['original']
except Exception as e:
app.logger.error(f"Error generating optimized versions for {new_filename}: {e}")
# If optimization fails, at least keep the original file
result = {'original': new_filename}
# If we didn't find the image, use a placeholder
# Suche das Originalbild direkt in der GridFS-Datenbank
existing_file = fs.find_one({"filename": dup_img})
# Fallback: Falls der exakte Name nicht gefunden wird, suche über Regex nach dem Basisnamen
if not existing_file:
base_name = re.sub(r'_\d+(?=\.[^.]+$)', '', dup_img) # Entfernt Suffixe wie _800
base_name_no_ext = os.path.splitext(base_name)[0]
if len(base_name_no_ext) > 5:
app.logger.info(f"Trying regex search for base name: {base_name_no_ext}")
existing_file = fs.find_one({"filename": {"$regex": f"^{re.escape(base_name_no_ext)}"}})
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
if existing_file:
app.logger.info(f"Found image in GridFS for {dup_img}. Duplicating...")
# Bilddaten in den Speicher laden und direkt als neue Datei in GridFS ablegen
image_data = existing_file.read()
fs.put(
image_data,
filename=new_filename,
content_type=existing_file.content_type,
metadata={'original_duplicate_of': dup_img}
)
duplicate_image_copies.append(new_filename)
processed_count += 1
else:
app.logger.warning(f"Using placeholder for image {i+1}/{original_count} (original: {dup_img})")
# Copy placeholder to uploads directory with the new filename
placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg')
if not os.path.exists(placeholder_path):
placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
app.logger.warning(f"Could not find {dup_img} in GridFS. Using placeholder.")
placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
if os.path.exists(placeholder_path):
dst_path = os.path.join(app.config['UPLOAD_FOLDER'], new_filename)
shutil.copy2(placeholder_path, dst_path)
app.logger.info(f"Copied placeholder image to {dst_path}")
with open(placeholder_path, 'rb') as pf:
placeholder_data = pf.read()
fs.put(
placeholder_data,
filename=new_filename,
content_type='image/png',
metadata={'is_placeholder': True}
)
duplicate_image_copies.append(new_filename)
placeholder_used = True
# Skip the optimization step for placeholder images
# Just add directly to the list of image filenames
continue
else:
app.logger.error(f"Placeholder image not found at {placeholder_path}")
# Create a simple placeholder file
with open(os.path.join(app.config['UPLOAD_FOLDER'], new_filename), 'w') as f:
f.write("Placeholder")
placeholder_used = True
# Skip the optimization step
continue
# Add the new filename to our list (either copied or placeholder)
duplicate_image_copies.append(new_filename)
processed_count += 1
app.logger.info(f"Processed image {i+1}/{original_count}: {new_filename}")
except Exception as e:
app.logger.error(f"Error processing image {i+1}/{original_count} ({dup_img}): {str(e)}")
app.logger.error(f"Error duplicating image {dup_img}: {str(e)}")
error_count += 1
# Log placeholder usage
if placeholder_used:
app.logger.warning(f"Used placeholders for some missing images during duplication")
# Log if no images were processed
if not duplicate_image_copies:
app.logger.warning(f"No duplicate images were processed")
if duplicate_images:
app.logger.warning(f"Original had {len(duplicate_images)} images, but none were copied")
# Add the new image copies to our list of filenames
app.logger.warning("Used placeholders for some missing images during duplication")
image_filenames.extend(duplicate_image_copies)
# Handle book cover image if provided
# 2. Buchcover verarbeiten
if book_cover_image:
# Verify the book cover image exists
full_path = os.path.join(app.config['UPLOAD_FOLDER'], book_cover_image)
if os.path.exists(full_path) and os.path.isfile(full_path):
# Create a unique filename for the book cover
unique_id = str(uuid.uuid4())
timestamp = time.strftime("%Y%m%d%H%M%S")
_, ext_part = os.path.splitext(book_cover_image)
new_filename = f"{unique_id}_{timestamp}_book_cover{ext_part}"
new_path = os.path.join(app.config['UPLOAD_FOLDER'], new_filename)
# Copy the file to the new unique name
shutil.copy2(full_path, new_path)
# Use the new filename instead
image_filenames.append(new_filename)
app.logger.info(f"Copied book cover from {book_cover_image} to {new_filename}")
else:
app.logger.warning(f"Book cover image not found: {book_cover_image}")
# If location is not in the predefined list, add it
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}_book_cover.webp"
# Prüfen, ob das Cover noch als lokale temporäre Datei existiert (z.B. von einer API geladen)
local_path = os.path.join(app.config.get('UPLOAD_FOLDER', ''), book_cover_image)
try:
if os.path.exists(local_path):
# Lokales Bild einlesen, zu WebP optimieren und in GridFS schieben
with Image.open(local_path) as img:
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA')
optimized_io = io.BytesIO()
img.save(optimized_io, format='WEBP', quality=85, optimize=True)
optimized_io.seek(0)
fs.put(
optimized_io,
filename=new_filename,
content_type='image/webp',
metadata={'is_book_cover': True}
)
image_filenames.append(new_filename)
app.logger.info(f"Processed local book cover and saved to GridFS: {new_filename}")
else:
# Falls es bereits in GridFS liegt, einfach duplizieren
existing_cover = fs.find_one({"filename": book_cover_image})
if existing_cover:
fs.put(
existing_cover.read(),
filename=new_filename,
content_type=existing_cover.content_type,
metadata={'is_book_cover': True}
)
image_filenames.append(new_filename)
app.logger.info(f"Duplicated existing GridFS book cover: {new_filename}")
else:
app.logger.warning(f"Book cover not found locally or in GridFS: {book_cover_image}")
except Exception as e:
app.logger.error(f"Error processing book cover {book_cover_image}: {str(e)}")
# 3. Item-Erstellung (Logik bleibt identisch, da it.add_item nun die GridFS-Filenames erhält)
predefined_locations = it.get_predefined_locations()
if ort and ort not in predefined_locations:
it.add_predefined_location(ort)
reservierbar = 'reservierbar' in request.form
# Add one or more own items; sub-items are hidden in overview and counted on parent.
reservierbar = 'reservierbar' in request.form
created_item_ids = []
series_group_id = str(uuid.uuid4()) if item_count > 1 else None
base_code = code_4[0] if code_4 else None
@@ -6096,6 +5598,8 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint))
parent_item_id = str(created_item_ids[0]) if created_item_ids else None
# image_filenames enthält jetzt ausschließlich GridFS-Filenames (oder ObjectIds, je nach deinem Setup)
item_id = it.add_item(
name, ort, beschreibung, image_filenames, filter_upload,
filter_upload2, filter_upload3,
@@ -6125,11 +5629,10 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint))
item_id = created_item_ids[0] if created_item_ids else None
if item_id:
# Create QR code for the item (deactivated)
# create_qr_code(str(item_id))
success_msg = f'Element wurde erfolgreich hinzugefügt ({len(created_item_ids)} erstellt)'
if upload_mode == 'library':
try:
_append_audit_event_standalone(
@@ -6145,7 +5648,6 @@ def upload_item():
except Exception:
app.logger.warning('Audit write failed for library_item_created')
flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
else:
File diff suppressed because it is too large Load Diff