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