import io import os import subprocess import tempfile import zipfile from flask import Flask, render_template, request, send_file, jsonify app = Flask(__name__) # Max upload size: 100 MB app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 ALLOWED_EXTENSIONS = {'.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx'} def is_allowed_file(filename): _, ext = os.path.splitext(filename.lower()) return ext in ALLOWED_EXTENSIONS @app.route('/') def index(): return render_template('index.html') @app.route('/convert', methods=['POST']) def convert(): if 'files' not in request.files: return jsonify({'error': 'No files found in the request.'}), 400 files = request.files.getlist('files') if not files or all(file.filename == '' for file in files): return jsonify({'error': 'No files selected for upload.'}), 400 # Build the ZIP archive in memory zip_buffer = io.BytesIO() converted_files_count = 0 failed_conversions = [] # Use TemporaryDirectory context manager to ensure automatic clean-up of files with tempfile.TemporaryDirectory() as temp_dir: for file in files: if not file.filename: continue _, ext = os.path.splitext(file.filename.lower()) if ext not in ALLOWED_EXTENSIONS: failed_conversions.append((file.filename, f"Unsupported extension '{ext}'")) continue # Save the uploaded file in the temporary directory input_path = os.path.join(temp_dir, file.filename) try: file.save(input_path) except Exception as e: failed_conversions.append((file.filename, f"Failed to save file: {e}")) continue # Build the LibreOffice execution command cmd = [ "soffice", "--headless", "--convert-to", "pdf", "--outdir", temp_dir, input_path ] print(f"Web conversion: executing {' '.join(cmd)}") try: # Convert the file safely using subprocess list invocation with a 180s timeout per file result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=180 ) if result.returncode != 0: failed_conversions.append((file.filename, f"LibreOffice failed (code {result.returncode})")) continue except subprocess.TimeoutExpired: failed_conversions.append((file.filename, "Conversion timed out (180s)")) continue except Exception as e: failed_conversions.append((file.filename, f"Conversion error: {e}")) continue # Verify that the PDF was successfully created base_name, _ = os.path.splitext(file.filename) pdf_filename = f"{base_name}.pdf" pdf_path = os.path.join(temp_dir, pdf_filename) if os.path.exists(pdf_path): converted_files_count += 1 else: failed_conversions.append((file.filename, "Expected PDF output was not created")) # If zero files were successfully converted, return a detailed error response if converted_files_count == 0: error_details = ", ".join([f"'{name}': {reason}" for name, reason in failed_conversions]) return jsonify({'error': f'Failed to convert any files. Details: {error_details}'}), 400 # If only a single file was successfully converted, return the PDF directly if converted_files_count == 1: pdf_filename = None pdf_path = None for file in files: base_name, _ = os.path.splitext(file.filename) pdf_filename = f"{base_name}.pdf" pdf_path = os.path.join(temp_dir, pdf_filename) if os.path.exists(pdf_path): break if pdf_path and pdf_filename: # Read PDF file into memory buffer so we can safely delete temporary directory pdf_buffer = io.BytesIO() with open(pdf_path, 'rb') as f: pdf_buffer.write(f.read()) pdf_buffer.seek(0) return send_file( pdf_buffer, mimetype='application/pdf', as_attachment=True, download_name=pdf_filename ) # Create the ZIP archive containing all successfully converted PDFs (for 2 or more files) with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: for file in files: base_name, _ = os.path.splitext(file.filename) pdf_filename = f"{base_name}.pdf" pdf_path = os.path.join(temp_dir, pdf_filename) if os.path.exists(pdf_path): # Write to the zip archive with only the filename (avoiding temp directory paths) zip_file.write(pdf_path, pdf_filename) # Append a conversion log if any files failed if failed_conversions: log_lines = ["Conversion Report (Some files failed to convert):\n", "="*50, "\n"] for name, reason in failed_conversions: log_lines.append(f"FAILED: {name} | Reason: {reason}\n") zip_file.writestr("conversion_errors.log", "".join(log_lines)) # Move pointer to the beginning of the memory buffer zip_buffer.seek(0) # Return the zip file for download return send_file( zip_buffer, mimetype='application/zip', as_attachment=True, download_name='converted_pdfs.zip' ) if __name__ == '__main__': # Get port from environment (Hugging Face defaults to 7860) or default to 7860 port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port)