first commit
This commit is contained in:
commit
513f24e655
|
@ -0,0 +1,2 @@
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
chown www-data:www-data -R src
|
||||||
|
cp diff.service /lib/systemd/system/
|
||||||
|
systemctl enable --now diff
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl restart diff-diff
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name diff.digitalstudium.com;
|
||||||
|
client_max_body_size 0;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:///tmp/diff.sock;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Diff web application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/srv/diff/src
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
|
||||||
|
# Virtual environment setup
|
||||||
|
ExecStartPre=/bin/bash -c '[ -d ".venv" ] || python3 -m venv .venv'
|
||||||
|
ExecStartPre=/bin/bash -c 'source .venv/bin/activate && pip install -r requirements.txt'
|
||||||
|
|
||||||
|
# uWSGI execution
|
||||||
|
ExecStart=/srv/diff/src/.venv/bin/uwsgi --ini uwsgi.ini
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
Environment="PYTHONUNBUFFERED=1"
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=diff
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
from flask import Flask, request, render_template_string
|
||||||
|
import difflib
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
HTML_TEMPLATE = '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Enhanced Diff Tool</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
/* Modern color scheme */
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--success: #10b981;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--background: #ffffff;
|
||||||
|
--text: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary: #60a5fa;
|
||||||
|
--success: #34d399;
|
||||||
|
--danger: #f87171;
|
||||||
|
--background: #1e293b;
|
||||||
|
--text: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 2px solid #cbd5e1;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-output {
|
||||||
|
margin-top: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-table td {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-add {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-del {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-line-chg {
|
||||||
|
background: #fef9c3;
|
||||||
|
color: #854d0e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
color: #64748b;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-diff {
|
||||||
|
background: #ffedd5;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.1rem 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="diff-header">
|
||||||
|
<h1>Diff Tool</h1>
|
||||||
|
<div class="theme-toggle" onclick="toggleTheme()">🌓 Toggle Theme</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" action="/diff">
|
||||||
|
<div class="diff-container">
|
||||||
|
<div>
|
||||||
|
<h3>Original Content</h3>
|
||||||
|
<input type="file" name="file1" onchange="previewFile(this, 'text1')">
|
||||||
|
<textarea id="text1" name="text1" placeholder="Paste or drop text here...">{{ text1 }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Modified Content</h3>
|
||||||
|
<input type="file" name="file2" onchange="previewFile(this, 'text2')">
|
||||||
|
<textarea id="text2" name="text2" placeholder="Paste or drop text here...">{{ text2 }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<button type="submit">Compare Files</button>
|
||||||
|
{% if diff_html %}
|
||||||
|
<button type="button" onclick="downloadDiff()">📥 Download Diff</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="message">{{ message }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if diff_html %}
|
||||||
|
<div class="diff-output">
|
||||||
|
{{ diff_html|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme management
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.setAttribute('data-theme',
|
||||||
|
document.body.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File preview in textareas
|
||||||
|
async function previewFile(input, textareaId) {
|
||||||
|
const textarea = document.getElementById(textareaId);
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const text = await input.files[0].text();
|
||||||
|
textarea.value = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag and drop handling
|
||||||
|
document.querySelectorAll('textarea').forEach(textarea => {
|
||||||
|
textarea.addEventListener('dragover', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
textarea.style.backgroundColor = '#f1f5f9';
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.addEventListener('dragleave', e => {
|
||||||
|
textarea.style.backgroundColor = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.addEventListener('drop', async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
textarea.style.backgroundColor = '';
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file) {
|
||||||
|
textarea.value = await file.text();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download diff
|
||||||
|
function downloadDiff() {
|
||||||
|
const diffContent = document.querySelector('.diff-output').innerText;
|
||||||
|
const blob = new Blob([diffContent], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'diff-result.txt';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def index():
|
||||||
|
return render_template_string(HTML_TEMPLATE,
|
||||||
|
text1=request.args.get('text1', ''),
|
||||||
|
text2=request.args.get('text2', ''))
|
||||||
|
|
||||||
|
@app.route('/diff', methods=['POST'])
|
||||||
|
def diff_files():
|
||||||
|
# Handle file uploads and text inputs
|
||||||
|
file1 = request.files.get('file1')
|
||||||
|
file2 = request.files.get('file2')
|
||||||
|
text1 = request.form.get('text1', '').strip()
|
||||||
|
text2 = request.form.get('text2', '').strip()
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
if (not file1 and not text1) or (not file2 and not text2):
|
||||||
|
return render_template_string(HTML_TEMPLATE,
|
||||||
|
message="Please provide content for both sides to compare.",
|
||||||
|
text1=text1,
|
||||||
|
text2=text2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Process first content
|
||||||
|
if file1 and file1.filename:
|
||||||
|
content1 = file1.read().decode('utf-8').splitlines()
|
||||||
|
filename1 = file1.filename
|
||||||
|
else:
|
||||||
|
content1 = text1.splitlines()
|
||||||
|
filename1 = "Original Content"
|
||||||
|
|
||||||
|
# Process second content
|
||||||
|
if file2 and file2.filename:
|
||||||
|
content2 = file2.read().decode('utf-8').splitlines()
|
||||||
|
filename2 = file2.filename
|
||||||
|
else:
|
||||||
|
content2 = text2.splitlines()
|
||||||
|
filename2 = "Modified Content"
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return render_template_string(HTML_TEMPLATE,
|
||||||
|
message="Error: Only UTF-8 encoded text files are supported.",
|
||||||
|
text1=text1,
|
||||||
|
text2=text2)
|
||||||
|
|
||||||
|
# Generate diff
|
||||||
|
differ = difflib.HtmlDiff(
|
||||||
|
tabsize=4,
|
||||||
|
wrapcolumn=80,
|
||||||
|
linejunk=None,
|
||||||
|
charjunk=difflib.IS_CHARACTER_JUNK
|
||||||
|
)
|
||||||
|
|
||||||
|
diff_html = differ.make_file(
|
||||||
|
content1,
|
||||||
|
content2,
|
||||||
|
fromdesc=filename1,
|
||||||
|
todesc=filename2,
|
||||||
|
context=True,
|
||||||
|
numlines=3
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template_string(HTML_TEMPLATE,
|
||||||
|
text1=text1,
|
||||||
|
text2=text2,
|
||||||
|
diff_html=diff_html)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description="Run SSL Certificate Decoder")
|
||||||
|
parser.add_argument('--port', type=int, default=5000, help='Port to run the service on')
|
||||||
|
parser.add_argument('--host', type=str, default='127.0.0.1', help='Host to run the service on')
|
||||||
|
args = parser.parse_args()
|
||||||
|
app.run(debug=True, port=args.port, host=args.host)
|
|
@ -0,0 +1,15 @@
|
||||||
|
astroid==3.3.10
|
||||||
|
blinker==1.9.0
|
||||||
|
click==8.2.0
|
||||||
|
dill==0.4.0
|
||||||
|
Flask==3.1.1
|
||||||
|
isort==6.0.1
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
mccabe==0.7.0
|
||||||
|
platformdirs==4.3.8
|
||||||
|
pylint==3.3.7
|
||||||
|
tomlkit==0.13.2
|
||||||
|
uWSGI==2.0.29
|
||||||
|
Werkzeug==3.1.3
|
|
@ -0,0 +1,12 @@
|
||||||
|
[uwsgi]
|
||||||
|
socket = /tmp/diff.sock
|
||||||
|
chmod-socket = 660
|
||||||
|
uid = www-data
|
||||||
|
gid = www-data
|
||||||
|
module = main:app
|
||||||
|
master = true
|
||||||
|
processes = 1
|
||||||
|
threads = 2
|
||||||
|
vacuum = true
|
||||||
|
die-on-term = true
|
||||||
|
|
Loading…
Reference in New Issue