Compare commits

...

5 Commits

Author SHA1 Message Date
DigiServer Developer
7b24245ddb updated the upload functionality to handle large files and added a new image file 2025-08-21 16:27:16 +03:00
DigiServer Developer
58694ff3f4 Update all changes before rebase and push 2025-08-21 16:26:53 +03:00
7f5991f60d fix: use correct endpoint for group media delete in manage_group.html 2025-08-20 15:11:22 -04:00
DigiServer Developer
5e4950563c Fix admin authentication and update port mapping
- Fix environment variable mismatch in create_default_user.py
- Now correctly uses ADMIN_USER and ADMIN_PASSWORD from docker-compose
- Maintains backward compatibility with DEFAULT_USER and DEFAULT_PASSWORD
- Change port mapping from 8880 to 80 for easier access
- Resolves login issues with admin user credentials
2025-08-11 17:01:58 +03:00
091e985ff2 fix: Simplified Docker deployment and fixed upload path resolution
🐳 Docker Configuration Improvements:
- Simplified docker-compose.yml to use single app folder bind mount
- Removed complex data folder mapping that caused path confusion
- Updated environment variables to match entrypoint script expectations
- Streamlined deployment for better reliability

🔧 Upload System Fixes:
- Fixed path resolution issues in uploads.py for containerized deployment
- Simplified upload folder path handling to work correctly in containers
- Removed complex absolute path conversion logic that caused file placement issues
- Ensured all file operations use consistent /app/static/uploads path

📁 File Processing Improvements:
- Fixed PPTX to JPG conversion workflow path handling
- Corrected PDF processing to save files in correct container location
- Improved video conversion path resolution
- Enhanced error handling and logging for upload operations

🚀 Production Benefits:
- Eliminates 404 errors for uploaded media files
- Ensures files are saved in correct locations within container
- Simplifies development and debugging with direct app folder mounting
- Maintains data consistency across container restarts

 This resolves the upload workflow issues where PPTX files were not
being correctly processed and saved to the expected locations.
2025-08-05 19:16:08 -04:00
11 changed files with 122 additions and 68 deletions

View File

@@ -69,8 +69,12 @@ db_path = os.path.join(instance_dir, 'dashboard.db')
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Set maximum content length to 1GB # Set maximum content length to 2GB
app.config['MAX_CONTENT_LENGTH'] = 2048 * 2048 * 2048 # 2GB, adjust as needed app.config['MAX_CONTENT_LENGTH'] = 2048 * 1024 * 1024 # 2GB, adjust as needed
# Set longer timeouts for file processing
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 300 # 5 minutes for static files
app.config['PERMANENT_SESSION_LIFETIME'] = 1800 # 30 minutes for sessions
# Ensure the instance folder exists # Ensure the instance folder exists
os.makedirs(app.instance_path, exist_ok=True) os.makedirs(app.instance_path, exist_ok=True)
@@ -95,6 +99,22 @@ login_manager.login_view = 'login'
migrate = Migrate(app, db) migrate = Migrate(app, db)
# Add error handlers for better user experience
@app.errorhandler(413)
def request_entity_too_large(error):
flash('File too large. Please upload files smaller than 2GB.', 'danger')
return redirect(url_for('dashboard'))
@app.errorhandler(408)
def request_timeout(error):
flash('Request timed out. Please try uploading smaller files or try again later.', 'danger')
return redirect(url_for('dashboard'))
@app.errorhandler(500)
def internal_server_error(error):
flash('An internal server error occurred. Please try again or contact support.', 'danger')
return redirect(url_for('dashboard'))
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return db.session.get(User, int(user_id)) return db.session.get(User, int(user_id))
@@ -213,8 +233,23 @@ def upload_content():
flash('Please select a target type and target ID.', 'danger') flash('Please select a target type and target ID.', 'danger')
return redirect(url_for('upload_content')) return redirect(url_for('upload_content'))
# Process uploaded files and get results try:
results = process_uploaded_files(app, files, media_type, duration, target_type, target_id) # Process uploaded files and get results
results = process_uploaded_files(app, files, media_type, duration, target_type, target_id)
# Check for any failed uploads
failed_files = [r for r in results if not r.get('success', True)]
if failed_files:
for failed in failed_files:
flash(f"Error uploading {failed.get('filename', 'unknown file')}: {failed.get('message', 'Unknown error')}", 'warning')
else:
flash('All files uploaded and processed successfully!', 'success')
except Exception as e:
print(f"Error in upload_content: {e}")
import traceback
traceback.print_exc()
flash(f'Upload failed: {str(e)}', 'danger')
return redirect(return_url) return redirect(return_url)

View File

@@ -2,8 +2,9 @@
import os import os
def create_default_user(db, User, bcrypt): def create_default_user(db, User, bcrypt):
username = os.getenv('DEFAULT_USER', 'admin') # Use ADMIN_USER and ADMIN_PASSWORD to match docker-compose environment variables
password = os.getenv('DEFAULT_PASSWORD', '1234') username = os.getenv('ADMIN_USER', os.getenv('DEFAULT_USER', 'admin'))
password = os.getenv('ADMIN_PASSWORD', os.getenv('DEFAULT_PASSWORD', '1234'))
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
existing_user = User.query.filter_by(username=username).first() existing_user = User.query.filter_by(username=username).first()
if not existing_user: if not existing_user:

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -110,35 +110,44 @@
<ul class="list-group sortable-list" id="groupMediaList"> <ul class="list-group sortable-list" id="groupMediaList">
{% for media in content %} {% for media in content %}
<li class="list-group-item d-flex align-items-center {{ 'dark-mode' if theme == 'dark' else '' }}" <li class="list-group-item d-flex align-items-center {{ 'dark-mode' if theme == 'dark' else '' }}"
draggable="true" draggable="true"
data-id="{{ media.id }}" data-id="{{ media.id }}"
data-position="{{ loop.index0 }}"> data-position="{{ loop.index0 }}">
<!-- Checkbox for bulk selection --> <!-- Checkbox for bulk selection -->
<div class="me-2"> <div class="me-2">
<input class="form-check-input media-checkbox" <input class="form-check-input media-checkbox"
type="checkbox" type="checkbox"
name="selected_content" name="selected_content"
value="{{ media.id }}"> value="{{ media.id }}">
</div> </div>
<!-- Drag handle --> <!-- Drag handle -->
<div class="drag-handle me-2" title="Drag to reorder"> <div class="drag-handle me-2" title="Drag to reorder">
<i class="bi bi-grip-vertical"></i> <i class="bi bi-grip-vertical"></i>
&#9776; &#9776;
</div> </div>
<div class="flex-grow-1"> <!-- Media Thumbnail and Name -->
<div class="flex-grow-1 mb-2 mb-md-0 d-flex align-items-center">
<img src="{{ url_for('static', filename='uploads/' ~ media.file_name) }}"
alt="thumbnail"
style="width: 48px; height: 48px; object-fit: cover; margin-right: 10px; border-radius: 4px;"
onerror="this.style.display='none';">
<p class="mb-0"><strong>Media Name:</strong> {{ media.file_name }}</p> <p class="mb-0"><strong>Media Name:</strong> {{ media.file_name }}</p>
</div> </div>
<form action="{{ url_for('edit_group_media', group_id=group.id, content_id=media.id) }}" method="post" class="d-flex align-items-center"> <<<<<<< HEAD
=======
>>>>>>> 2255cc2 (Show media thumbnails in manage group page, matching player page style)
<form action="{{ url_for('edit_group_media_route', group_id=group.id, content_id=media.id) }}" method="post" class="d-flex align-items-center">
<div class="input-group me-2"> <div class="input-group me-2">
<span class="input-group-text">seconds</span> <span class="input-group-text">seconds</span>
<input type="number" class="form-control {{ 'dark-mode' if theme == 'dark' else '' }}" name="duration" value="{{ media.duration }}" required> <input type="number" class="form-control {{ 'dark-mode' if theme == 'dark' else '' }}" name="duration" value="{{ media.duration }}" required>
</div> </div>
<button type="submit" class="btn btn-warning me-2">Edit</button> <button type="submit" class="btn btn-warning me-2">Edit</button>
</form> </form>
<form action="{{ url_for('delete_group_media', group_id=group.id, content_id=media.id) }}" method="post" style="display:inline;"> <form action="{{ url_for('delete_group_media_route', group_id=group.id, content_id=media.id) }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this media?');">Delete</button> <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this media?');">Delete</button>
</form> </form>
</li> </li>

View File

@@ -243,7 +243,7 @@
statusMessage.textContent = 'Converting PDF to 4K images. This may take a while...'; statusMessage.textContent = 'Converting PDF to 4K images. This may take a while...';
break; break;
case 'ppt': case 'ppt':
statusMessage.textContent = 'Converting PowerPoint to 4K images. This may take a while...'; statusMessage.textContent = 'Converting PowerPoint to images (PPTX → PDF → Images). This may take 2-5 minutes...';
break; break;
default: default:
statusMessage.textContent = 'Uploading and processing your files. Please wait...'; statusMessage.textContent = 'Uploading and processing your files. Please wait...';

View File

@@ -9,12 +9,23 @@ The converted PDF is then processed by the main upload workflow for 4K image gen
import os import os
import subprocess import subprocess
import logging import logging
import signal
import time
# Set up logging # Set up logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def cleanup_libreoffice_processes():
"""Clean up any hanging LibreOffice processes"""
try:
subprocess.run(['pkill', '-f', 'soffice'], capture_output=True, timeout=10)
time.sleep(1) # Give processes time to terminate
except Exception as e:
logger.warning(f"Failed to cleanup LibreOffice processes: {e}")
def pptx_to_pdf_libreoffice(pptx_path, output_dir): def pptx_to_pdf_libreoffice(pptx_path, output_dir):
""" """
Convert PPTX to PDF using LibreOffice for highest quality. Convert PPTX to PDF using LibreOffice for highest quality.
@@ -30,6 +41,9 @@ def pptx_to_pdf_libreoffice(pptx_path, output_dir):
str: Path to the generated PDF file, or None if conversion failed str: Path to the generated PDF file, or None if conversion failed
""" """
try: try:
# Clean up any existing LibreOffice processes
cleanup_libreoffice_processes()
# Ensure output directory exists # Ensure output directory exists
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
@@ -39,14 +53,19 @@ def pptx_to_pdf_libreoffice(pptx_path, output_dir):
'--headless', '--headless',
'--convert-to', 'pdf', '--convert-to', 'pdf',
'--outdir', output_dir, '--outdir', output_dir,
'--invisible', # Run without any UI
'--nodefault', # Don't start with default template
pptx_path pptx_path
] ]
logger.info(f"Converting PPTX to PDF using LibreOffice: {pptx_path}") logger.info(f"Converting PPTX to PDF using LibreOffice: {pptx_path}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) # Increase timeout to 300 seconds (5 minutes) for large presentations
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode != 0: if result.returncode != 0:
logger.error(f"LibreOffice conversion failed: {result.stderr}") logger.error(f"LibreOffice conversion failed: {result.stderr}")
logger.error(f"LibreOffice stdout: {result.stdout}")
cleanup_libreoffice_processes() # Clean up on failure
return None return None
# Find the generated PDF file # Find the generated PDF file
@@ -55,16 +74,22 @@ def pptx_to_pdf_libreoffice(pptx_path, output_dir):
if os.path.exists(pdf_path): if os.path.exists(pdf_path):
logger.info(f"PDF conversion successful: {pdf_path}") logger.info(f"PDF conversion successful: {pdf_path}")
cleanup_libreoffice_processes() # Clean up after success
return pdf_path return pdf_path
else: else:
logger.error(f"PDF file not found after conversion: {pdf_path}") logger.error(f"PDF file not found after conversion: {pdf_path}")
cleanup_libreoffice_processes() # Clean up on failure
return None return None
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.error("LibreOffice conversion timed out (120s)") logger.error("LibreOffice conversion timed out (300s)")
cleanup_libreoffice_processes() # Clean up on timeout
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error in PPTX to PDF conversion: {e}") logger.error(f"Error in PPTX to PDF conversion: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
cleanup_libreoffice_processes() # Clean up on error
return None return None

View File

@@ -12,10 +12,10 @@ def add_image_to_playlist(app, file, filename, duration, target_type, target_id)
""" """
Save the image file and add it to the playlist database. Save the image file and add it to the playlist database.
""" """
# Ensure we use absolute path for upload folder # Use simple path resolution for containerized environment
upload_folder = app.config['UPLOAD_FOLDER'] upload_folder = app.config['UPLOAD_FOLDER']
if not os.path.isabs(upload_folder): # In container, working directory is /app, so static/uploads resolves correctly
upload_folder = os.path.abspath(upload_folder) print(f"Upload folder config: {upload_folder}")
# Ensure upload folder exists # Ensure upload folder exists
if not os.path.exists(upload_folder): if not os.path.exists(upload_folder):
@@ -55,10 +55,12 @@ def convert_video(input_file, output_folder):
""" """
Converts a video file to MP4 format with H.264 codec. Converts a video file to MP4 format with H.264 codec.
""" """
# Ensure we use absolute path for output folder # Use simple path resolution for containerized environment
if not os.path.isabs(output_folder): if not os.path.isabs(output_folder):
output_folder = os.path.abspath(output_folder) # In container, relative paths work from /app directory
print(f"Converted output folder to absolute path: {output_folder}") print(f"Using relative path: {output_folder}")
else:
print(f"Using absolute path: {output_folder}")
if not os.path.exists(output_folder): if not os.path.exists(output_folder):
os.makedirs(output_folder, exist_ok=True) os.makedirs(output_folder, exist_ok=True)
@@ -98,11 +100,9 @@ def convert_video_and_update_playlist(app, file_path, original_filename, target_
""" """
print(f"Starting video conversion for: {file_path}") print(f"Starting video conversion for: {file_path}")
# Ensure we use absolute path for upload folder # Use simple path resolution for containerized environment
upload_folder = app.config['UPLOAD_FOLDER'] upload_folder = app.config['UPLOAD_FOLDER']
if not os.path.isabs(upload_folder): print(f"Upload folder: {upload_folder}")
upload_folder = os.path.abspath(upload_folder)
print(f"Converted upload folder to absolute path: {upload_folder}")
converted_file = convert_video(file_path, upload_folder) converted_file = convert_video(file_path, upload_folder)
if converted_file: if converted_file:
@@ -140,25 +140,7 @@ def convert_pdf_to_images(pdf_file, output_folder, delete_pdf=True, dpi=300):
Uses standard 300 DPI for reliable conversion. Uses standard 300 DPI for reliable conversion.
""" """
print(f"Converting PDF to JPG images: {pdf_file} at {dpi} DPI") print(f"Converting PDF to JPG images: {pdf_file} at {dpi} DPI")
print(f"Original output folder: {output_folder}") print(f"Output folder: {output_folder}")
# Force absolute path resolution to ensure we use the app directory
if not os.path.isabs(output_folder):
# If relative path, resolve from the current working directory
output_folder = os.path.abspath(output_folder)
print(f"Converted relative path to absolute: {output_folder}")
else:
print(f"Using provided absolute path: {output_folder}")
# Ensure we're using the app static folder, not workspace root
if output_folder.endswith('static/uploads'):
# Check if we're accidentally using workspace root instead of app folder
expected_app_path = '/opt/digiserver/app/static/uploads'
if output_folder != expected_app_path:
print(f"WARNING: Correcting path from {output_folder} to {expected_app_path}")
output_folder = expected_app_path
print(f"Final output folder: {output_folder}")
try: try:
# Ensure output folder exists # Ensure output folder exists
@@ -266,11 +248,6 @@ def process_pdf(input_file, output_folder, duration, target_type, target_id):
print(f"Processing PDF file: {input_file}") print(f"Processing PDF file: {input_file}")
print(f"Output folder: {output_folder}") print(f"Output folder: {output_folder}")
# Ensure we have absolute path for output folder
if not os.path.isabs(output_folder):
output_folder = os.path.abspath(output_folder)
print(f"Converted output folder to absolute path: {output_folder}")
# Ensure output folder exists # Ensure output folder exists
if not os.path.exists(output_folder): if not os.path.exists(output_folder):
os.makedirs(output_folder, exist_ok=True) os.makedirs(output_folder, exist_ok=True)
@@ -306,11 +283,6 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
print(f"Processing PPTX file using PDF workflow: {input_file}") print(f"Processing PPTX file using PDF workflow: {input_file}")
print(f"Output folder: {output_folder}") print(f"Output folder: {output_folder}")
# Ensure we have absolute path for output folder
if not os.path.isabs(output_folder):
output_folder = os.path.abspath(output_folder)
print(f"Converted output folder to absolute path: {output_folder}")
# Ensure output folder exists # Ensure output folder exists
if not os.path.exists(output_folder): if not os.path.exists(output_folder):
os.makedirs(output_folder, exist_ok=True) os.makedirs(output_folder, exist_ok=True)
@@ -318,21 +290,33 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
try: try:
# Step 1: Convert PPTX to PDF using LibreOffice for vector quality # Step 1: Convert PPTX to PDF using LibreOffice for vector quality
print("Step 1: Converting PPTX to PDF...")
from utils.pptx_converter import pptx_to_pdf_libreoffice from utils.pptx_converter import pptx_to_pdf_libreoffice
pdf_file = pptx_to_pdf_libreoffice(input_file, output_folder) pdf_file = pptx_to_pdf_libreoffice(input_file, output_folder)
if not pdf_file: if not pdf_file:
print("Error: Failed to convert PPTX to PDF") print("Error: Failed to convert PPTX to PDF")
print("This could be due to:")
print("- LibreOffice not properly installed")
print("- Corrupted PPTX file")
print("- Insufficient memory")
print("- File permission issues")
return False return False
print(f"PPTX successfully converted to PDF: {pdf_file}") print(f"PPTX successfully converted to PDF: {pdf_file}")
# Step 2: Use the same PDF to images workflow as direct PDF uploads # Step 2: Use the same PDF to images workflow as direct PDF uploads
print("Step 2: Converting PDF to JPG images...")
# Convert PDF to JPG images (300 DPI, same as PDF workflow) # Convert PDF to JPG images (300 DPI, same as PDF workflow)
image_filenames = convert_pdf_to_images(pdf_file, output_folder, delete_pdf=True, dpi=300) image_filenames = convert_pdf_to_images(pdf_file, output_folder, delete_pdf=True, dpi=300)
if not image_filenames: if not image_filenames:
print("Error: Failed to convert PDF to images") print("Error: Failed to convert PDF to images")
print("This could be due to:")
print("- poppler-utils not properly installed")
print("- PDF corruption during conversion")
print("- Insufficient disk space")
print("- Memory issues during image processing")
return False return False
print(f"Generated {len(image_filenames)} JPG images from PPTX → PDF") print(f"Generated {len(image_filenames)} JPG images from PPTX → PDF")
@@ -341,11 +325,14 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
if os.path.exists(input_file): if os.path.exists(input_file):
os.remove(input_file) os.remove(input_file)
print(f"Original PPTX file deleted: {input_file}") print(f"Original PPTX file deleted: {input_file}")
# Step 4: Update playlist with generated images in sequential order # Step 4: Update playlist with generated images in sequential order
print("Step 3: Adding images to playlist...")
success = update_playlist_with_files(image_filenames, duration, target_type, target_id) success = update_playlist_with_files(image_filenames, duration, target_type, target_id)
if success: if success:
print(f"Successfully processed PPTX: {len(image_filenames)} images added to playlist") print(f"Successfully processed PPTX: {len(image_filenames)} images added to playlist")
else:
print("Error: Failed to add images to playlist database")
return success return success
except Exception as e: except Exception as e:
@@ -377,10 +364,9 @@ def process_uploaded_files(app, files, media_type, duration, target_type, target
# Generate a secure filename and save the file # Generate a secure filename and save the file
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
# Ensure we use absolute path for upload folder # Use simple path resolution for containerized environment
upload_folder = app.config['UPLOAD_FOLDER'] upload_folder = app.config['UPLOAD_FOLDER']
if not os.path.isabs(upload_folder): print(f"Upload folder: {upload_folder}")
upload_folder = os.path.abspath(upload_folder)
# Ensure upload folder exists # Ensure upload folder exists
if not os.path.exists(upload_folder): if not os.path.exists(upload_folder):

View File

@@ -8,18 +8,16 @@ services:
image: digiserver:latest image: digiserver:latest
container_name: digiserver container_name: digiserver
ports: ports:
- "8880:5000" - "80:5000"
environment: environment:
- FLASK_APP=app.py - FLASK_APP=app.py
- FLASK_RUN_HOST=0.0.0.0 - FLASK_RUN_HOST=0.0.0.0
- DEFAULT_USER=admin - ADMIN_USER=admin
- DEFAULT_PASSWORD=Initial01! - ADMIN_PASSWORD=Initial01!
- SECRET_KEY=Ma_Duc_Dupa_Merele_Lui_Ana - SECRET_KEY=Ma_Duc_Dupa_Merele_Lui_Ana
volumes: volumes:
# Persistent data volumes # Bind mount the app folder for easier development and debugging
- ./data/instance:/app/instance - ./app:/app
- ./data/uploads:/app/static/uploads
- ./data/resurse:/app/static/resurse
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/"] test: ["CMD", "curl", "-f", "http://localhost:5000/"]