fileconverter / app.py
basilbenny1002's picture
Update app.py
aadedf6 verified
Raw
History Blame Contribute Delete
9.44 kB
import os
import subprocess
import uuid
from flask import Flask, request, render_template_string, send_file, jsonify
app = Flask(__name__)
# Safe directories using the user's home directory context
UPLOAD_FOLDER = os.path.expanduser('~/uploads')
OUTPUT_FOLDER = os.path.expanduser('~/outputs')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
# Extension mapping matrix defining what can safely turn into what
ALLOWED_CONVERSIONS = {
'docx': ['pdf', 'odt', 'txt', 'html'],
'doc': ['pdf', 'docx', 'odt'],
'xlsx': ['pdf', 'csv', 'ods', 'html'],
'xls': ['pdf', 'xlsx', 'csv'],
'pptx': ['pdf', 'odp', 'html'],
'ppt': ['pdf', 'pptx'],
'odt': ['pdf', 'docx', 'txt'],
'ods': ['pdf', 'xlsx', 'csv'],
'txt': ['pdf', 'docx'],
'csv': ['xlsx', 'pdf']
}
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Universal Multi-Format Document Converter</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.card { background: white; padding: 2.5rem; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); width: 480px; text-align: center; }
h2 { color: #1a1a1a; margin-top: 0; margin-bottom: 0.5rem; font-size: 1.6rem; }
p.subtitle { color: #666; margin-bottom: 2rem; font-size: 0.9rem; }
.drop-zone { border: 2px dashed #4A90E2; padding: 2.5rem 1.5rem; border-radius: 10px; background: #f8faff; cursor: pointer; transition: all 0.2s ease; margin-bottom: 1.5rem; }
.drop-zone:hover { background: #f0f5ff; border-color: #357ABD; }
.drop-zone p { margin: 0; color: #444; font-size: 1rem; font-weight: 500; }
.drop-zone span { display: block; color: #888; font-size: 0.8rem; margin-top: 0.5rem; }
.control-group { text-align: left; margin-bottom: 1.5rem; display: none; }
label { display: block; font-size: 0.85rem; color: #555; margin-bottom: 0.5rem; font-weight: 600; }
select { width: 100%; padding: 0.75rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; background: white; outline: none; }
select:focus { border-color: #4A90E2; }
button { background: #4A90E2; color: white; border: none; padding: 0.85rem; font-size: 1rem; border-radius: 6px; cursor: pointer; width: 100%; font-weight: bold; transition: background 0.2s; }
button:hover { background: #357ABD; }
button:disabled { background: #cbd5e1; cursor: not-allowed; }
#status { margin-top: 1.2rem; font-size: 0.9rem; color: #333; font-weight: 500; }
</style>
</head>
<body>
<div class="card">
<h2>Multi-Format Converter</h2>
<p class="subtitle">Powered by optimized LibreOffice daemon</p>
<div class="drop-zone" id="dropZone">
<p id="dropText">Drag & drop your document here</p>
<span>Supports DOCX, XLSX, PPTX, ODT, TXT, CSV etc.</span>
<input type="file" id="fileInput" style="display: none;">
</div>
<div class="control-group" id="targetGroup">
<label for="targetFormat">SELECT OUTPUT FORMAT</label>
<select id="targetFormat"></select>
</div>
<button id="convertBtn" disabled>Convert Document</button>
<p id="status"></p>
</div>
<script>
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const convertBtn = document.getElementById('convertBtn');
const targetGroup = document.getElementById('targetGroup');
const targetFormat = document.getElementById('targetFormat');
const status = document.getElementById('status');
const dropText = document.getElementById('dropText');
let selectedFile = null;
// Allowed format configuration matrix injected straight from server config rules
const map = {{ mapping | tojson }};
dropZone.onclick = () => fileInput.click();
fileInput.onchange = (e) => handleFile(e.target.files[0]);
dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.background = '#eef3f9'; };
dropZone.ondragleave = () => { dropZone.style.background = '#f8faff'; };
dropZone.ondrop = (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); };
function handleFile(file) {
if (!file) return;
const ext = file.name.split('.').pop().toLowerCase();
if (map[ext]) {
selectedFile = file;
dropText.innerText = `Selected: ${file.name}`;
// Build dynamic downstream selectable dropdown context arrays
targetFormat.innerHTML = "";
map[ext].forEach(fmt => {
const opt = document.createElement('option');
opt.value = fmt;
opt.innerText = fmt.toUpperCase();
targetFormat.appendChild(opt);
});
targetGroup.style.display = 'block';
convertBtn.disabled = false;
status.innerText = "";
} else {
targetGroup.style.display = 'none';
convertBtn.disabled = true;
status.innerText = `Extension .${ext.toUpperCase()} is not natively mapable.`;
}
}
convertBtn.onclick = async () => {
if (!selectedFile) return;
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('target', targetFormat.value);
convertBtn.disabled = true;
status.style.color = "#4A90E2";
status.innerText = "Processing via hot memory stream... Engine active.";
try {
const response = await fetch('/convert', { method: 'POST', body: formData });
if (!response.ok) throw new Error('Daemon processing execution fault');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const baseName = selectedFile.name.substring(0, selectedFile.name.lastIndexOf('.'));
a.download = `${baseName}.${targetFormat.value}`;
document.body.appendChild(a);
a.click();
a.remove();
status.style.color = "#2e7d32";
status.innerText = "Success! Converted immediately.";
} catch (err) {
status.style.color = "#d32f2f";
status.innerText = "Conversion error occurred. Verify data integrity.";
} finally {
convertBtn.disabled = false;
}
};
</script>
</body>
</html>
"""
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE, mapping=ALLOWED_CONVERSIONS)
@app.route('/convert', methods=['POST'])
def convert():
if 'file' not in request.files or 'target' not in request.form:
return jsonify({"error": "Malformed query structure payload"}), 400
file = request.files['file']
target_ext = request.form['target'].lower()
if file.filename == '':
return jsonify({"error": "Empty filename structure context"}), 400
source_ext = file.filename.split('.')[-1].lower()
if source_ext not in ALLOWED_CONVERSIONS or target_ext not in ALLOWED_CONVERSIONS[source_ext]:
return jsonify({"error": "Unsupported cross-format compilation sequence requested"}), 400
# Ensure thread-safe runtime paths
job_id = str(uuid.uuid4())
input_filename = f"{job_id}.{source_ext}"
output_filename = f"{job_id}.{target_ext}"
input_path = os.path.join(UPLOAD_FOLDER, input_filename)
output_path = os.path.join(OUTPUT_FOLDER, output_filename)
file.save(input_path)
try:
# We hook straight into the background listening port 2002 via 'unoconvert'
# From app.py line ~160
subprocess.run([
'unoconvert',
'--port', '2002', # Connects to unoserver's main port
'--convert-to', target_ext,
input_path,
output_path
], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if os.path.exists(output_path):
return send_file(output_path, as_attachment=True)
else:
return jsonify({"error": "Pipeline processing did not generate binary assets"}), 500
except subprocess.CalledProcessError as e:
return jsonify({"error": "Daemon stream pipeline communications broken"}), 500
finally:
# Continuous clean execution lifecycle routines
if os.path.exists(input_path):
os.remove(input_path)
if os.path.exists(output_path):
os.remove(output_path)
if __name__ == '__main__':
# Fallback to standard testing configurations locally if needed
app.run(host='0.0.0.0', port=7860)