diff --git a/Web/app.py b/Web/app.py index fda4f2a..c7f21cd 100755 --- a/Web/app.py +++ b/Web/app.py @@ -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: diff --git a/Web/templates/main.html b/Web/templates/main.html deleted file mode 100755 index 2090c96..0000000 --- a/Web/templates/main.html +++ /dev/null @@ -1,4692 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - - -
- - - -
-

Inventar Objekte -
- - -
-

-
-
-
- - - -
-
-
-
- -
-
-
- -
-
- - - -
-
-
-
- -
-
-
- -
-
- - - -
-
-
-
- -
-
-
- -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
-
-
- -
-
- -
- -
- -
-
-
- -
- - -
- -
- - - - - - - - - - - - -{% if open_item %} - -{% endif %} -{% endblock %} -