All checks were successful
Release / build (push) Successful in 34s
Release / publish_head (push) Successful in 33s
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 15s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 10s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 20s
Release / publish_head (release) Has been skipped
Release / build (release) Successful in 37s
360 lines
15 KiB
Python
360 lines
15 KiB
Python
import os
|
|
import shutil
|
|
from flask import Blueprint, request, render_template_string, send_from_directory, redirect, url_for, flash, session
|
|
from werkzeug.utils import secure_filename
|
|
|
|
def create_filemanager_blueprint(base_dir, url_prefix='/files', auth_password=None):
|
|
"""
|
|
Creates a Flask blueprint providing a simple file manager with a clipboard-style
|
|
move operation (cut/paste) for the given base directory.
|
|
"""
|
|
base_dir = os.path.abspath(base_dir)
|
|
os.makedirs(base_dir, exist_ok=True)
|
|
filemanager = Blueprint('filemanager', __name__, url_prefix=url_prefix)
|
|
|
|
def secure_path(path):
|
|
"""Ensure that the provided relative path stays within the base_dir."""
|
|
safe_path = os.path.abspath(os.path.join(base_dir, path))
|
|
if not safe_path.startswith(base_dir):
|
|
raise Exception("Invalid path")
|
|
return safe_path
|
|
|
|
@filemanager.before_request
|
|
def require_login():
|
|
if auth_password is not None:
|
|
# Allow access to login and logout pages without being authenticated.
|
|
if request.endpoint in ['filemanager.login', 'filemanager.logout']:
|
|
return None
|
|
if not session.get('filemanager_authenticated'):
|
|
return redirect(url_for('filemanager.login', next=request.url))
|
|
return None
|
|
|
|
@filemanager.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'POST':
|
|
password = request.form.get('password', '')
|
|
if password == auth_password:
|
|
session['filemanager_authenticated'] = True
|
|
flash("Logged in successfully")
|
|
next_url = request.args.get('next') or url_for('filemanager.index')
|
|
return redirect(next_url)
|
|
else:
|
|
flash("Incorrect password")
|
|
return render_template_string('''
|
|
<!doctype html>
|
|
<html>
|
|
<head><title>Login</title></head>
|
|
<body>
|
|
<h1>Login</h1>
|
|
{% with messages = get_flashed_messages() %}
|
|
{% if messages %}
|
|
<ul>
|
|
{% for message in messages %}
|
|
<li>{{ message }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
{% endwith %}
|
|
<form method="post">
|
|
<input type="password" name="password" placeholder="Password">
|
|
<input type="submit" value="Login">
|
|
</form>
|
|
</body>
|
|
</html>
|
|
''')
|
|
|
|
@filemanager.route('/logout', methods=['POST'])
|
|
def logout():
|
|
session.pop('filemanager_authenticated', None)
|
|
flash("Logged out")
|
|
return redirect(url_for('filemanager.login'))
|
|
|
|
@filemanager.route('/')
|
|
def index():
|
|
# Determine current directory from query parameter; defaults to the base.
|
|
rel_path = request.args.get('path', '')
|
|
try:
|
|
abs_path = secure_path(rel_path)
|
|
except Exception:
|
|
return "Invalid path", 400
|
|
if not os.path.isdir(abs_path):
|
|
return "Not a directory", 400
|
|
|
|
# Build a list of items (files and folders) in the current directory.
|
|
items = []
|
|
for entry in os.listdir(abs_path):
|
|
entry_path = os.path.join(abs_path, entry)
|
|
rel_entry_path = os.path.join(rel_path, entry) if rel_path else entry
|
|
items.append({
|
|
'name': entry,
|
|
'is_dir': os.path.isdir(entry_path),
|
|
'path': rel_entry_path
|
|
})
|
|
items.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
|
|
parent = os.path.dirname(rel_path) if rel_path else ''
|
|
|
|
# Minimal HTML template that displays the file tree along with a clipboard if active.
|
|
template = """
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>File Manager</title>
|
|
</head>
|
|
<body>
|
|
<h1>File Manager</h1>
|
|
<p>Current Directory: /{{ rel_path }}</p>
|
|
{% if rel_path %}
|
|
<p><a href="{{ url_for('filemanager.index', path=parent) }}">Go up</a></p>
|
|
{% endif %}
|
|
|
|
{% if session.clipboard %}
|
|
<div style="border:1px solid #ccc; padding:10px; margin:10px 0;">
|
|
<h3>Clipboard</h3>
|
|
<ul>
|
|
{% for item in session.clipboard %}
|
|
<li>{{ item }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<!-- Complete Move: paste the clipboard items into the current folder -->
|
|
<form method="post" action="{{ url_for('filemanager.paste') }}">
|
|
<input type="hidden" name="dest" value="{{ rel_path }}">
|
|
<button type="submit">Complete Move Here</button>
|
|
</form>
|
|
<!-- Cancel the move -->
|
|
<form method="post" action="{{ url_for('filemanager.cancel_move') }}">
|
|
<button type="submit">Cancel Move</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<ul>
|
|
{% for item in items %}
|
|
<li>
|
|
<!-- Checkbox for bulk operations -->
|
|
<input type="checkbox" class="bulk" value="{{ item.path }}">
|
|
{% if item.is_dir %}
|
|
📁 <a href="{{ url_for('filemanager.index', path=item.path) }}">{{ item.name }}</a>
|
|
[<a href="#" onclick="deleteItem('{{ item.path }}'); return false;">Delete</a>]
|
|
[<a href="#" onclick="renameItem('{{ item.path }}'); return false;">Rename</a>]
|
|
[<a href="#" onclick="cutItem('{{ item.path }}'); return false;">Cut</a>]
|
|
{% else %}
|
|
📄 {{ item.name }}
|
|
[<a href="{{ url_for('filemanager.download', path=item.path) }}">Download</a>]
|
|
[<a href="#" onclick="deleteItem('{{ item.path }}'); return false;">Delete</a>]
|
|
[<a href="#" onclick="renameItem('{{ item.path }}'); return false;">Rename</a>]
|
|
[<a href="#" onclick="cutItem('{{ item.path }}'); return false;">Cut</a>]
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<button onclick="bulkCut()">Bulk Cut Selected</button>
|
|
<hr>
|
|
<h2>Upload File(s)</h2>
|
|
<form action="{{ url_for('filemanager.upload') }}" method="post" enctype="multipart/form-data">
|
|
<input type="hidden" name="path" value="{{ rel_path }}">
|
|
<input type="file" name="file" multiple>
|
|
<input type="submit" value="Upload">
|
|
</form>
|
|
<hr>
|
|
<h2>Create New Directory</h2>
|
|
<form action="{{ url_for('filemanager.mkdir') }}" method="post">
|
|
<!-- Pass the current directory as a hidden value -->
|
|
<input type="hidden" name="path" value="{{ rel_path }}">
|
|
<input type="text" name="dirname" placeholder="New Directory Name">
|
|
<input type="submit" value="Create Directory">
|
|
</form>
|
|
<script>
|
|
function deleteItem(path) {
|
|
if(confirm("Are you sure you want to delete " + path + "?")) {
|
|
var form = document.createElement("form");
|
|
form.method = "post";
|
|
form.action = "{{ url_for('filemanager.delete') }}";
|
|
var input = document.createElement("input");
|
|
input.type = "hidden";
|
|
input.name = "path";
|
|
input.value = path;
|
|
form.appendChild(input);
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
function renameItem(path) {
|
|
var newName = prompt("Enter new name for " + path);
|
|
if(newName) {
|
|
var form = document.createElement("form");
|
|
form.method = "post";
|
|
form.action = "{{ url_for('filemanager.rename') }}";
|
|
var input1 = document.createElement("input");
|
|
input1.type = "hidden";
|
|
input1.name = "path";
|
|
input1.value = path;
|
|
form.appendChild(input1);
|
|
var input2 = document.createElement("input");
|
|
input2.type = "hidden";
|
|
input2.name = "new_name";
|
|
input2.value = newName;
|
|
form.appendChild(input2);
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
function cutItem(path) {
|
|
// Submit a form to add a single item to the clipboard.
|
|
var form = document.createElement("form");
|
|
form.method = "post";
|
|
form.action = "{{ url_for('filemanager.cut') }}";
|
|
var input = document.createElement("input");
|
|
input.type = "hidden";
|
|
input.name = "paths";
|
|
input.value = path;
|
|
form.appendChild(input);
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
function bulkCut() {
|
|
// Gather all selected items and submit them to the clipboard.
|
|
var checkboxes = document.querySelectorAll('.bulk:checked');
|
|
if(checkboxes.length === 0) {
|
|
alert("No items selected");
|
|
return;
|
|
}
|
|
var form = document.createElement("form");
|
|
form.method = "post";
|
|
form.action = "{{ url_for('filemanager.cut') }}";
|
|
checkboxes.forEach(function(box) {
|
|
var input = document.createElement("input");
|
|
input.type = "hidden";
|
|
input.name = "paths";
|
|
input.value = box.value;
|
|
form.appendChild(input);
|
|
});
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
return render_template_string(template, items=items, rel_path=rel_path, parent=parent)
|
|
|
|
@filemanager.route('/download')
|
|
def download():
|
|
rel_path = request.args.get('path', '')
|
|
try:
|
|
abs_path = secure_path(rel_path)
|
|
except Exception:
|
|
return "Invalid path", 400
|
|
if not os.path.isfile(abs_path):
|
|
return "File not found", 404
|
|
directory = os.path.dirname(abs_path)
|
|
filename = os.path.basename(abs_path)
|
|
return send_from_directory(directory, filename, as_attachment=True)
|
|
|
|
@filemanager.route('/delete', methods=['POST'])
|
|
def delete():
|
|
rel_path = request.form.get('path', '')
|
|
try:
|
|
abs_path = secure_path(rel_path)
|
|
except Exception:
|
|
return "Invalid path", 400
|
|
if os.path.isfile(abs_path):
|
|
os.remove(abs_path)
|
|
elif os.path.isdir(abs_path):
|
|
shutil.rmtree(abs_path)
|
|
else:
|
|
return "Not found", 404
|
|
flash("Deleted successfully")
|
|
parent = os.path.dirname(rel_path)
|
|
return redirect(url_for('filemanager.index', path=parent))
|
|
|
|
@filemanager.route('/upload', methods=['POST'])
|
|
def upload():
|
|
rel_path = request.form.get('path', '')
|
|
try:
|
|
abs_path = secure_path(rel_path)
|
|
except Exception:
|
|
return "Invalid path", 400
|
|
if not os.path.isdir(abs_path):
|
|
return "Not a directory", 400
|
|
files = request.files.getlist('file')
|
|
if files:
|
|
for file in files:
|
|
if file and file.filename:
|
|
filename = secure_filename(file.filename)
|
|
file.save(os.path.join(abs_path, filename))
|
|
flash("Uploaded files successfully")
|
|
return redirect(url_for('filemanager.index', path=rel_path))
|
|
|
|
@filemanager.route('/rename', methods=['POST'])
|
|
def rename():
|
|
rel_path = request.form.get('path', '')
|
|
new_name = request.form.get('new_name', '')
|
|
try:
|
|
abs_path = secure_path(rel_path)
|
|
new_rel_path = os.path.join(os.path.dirname(rel_path), new_name)
|
|
new_abs_path = secure_path(new_rel_path)
|
|
except Exception:
|
|
return "Invalid path", 400
|
|
os.rename(abs_path, new_abs_path)
|
|
flash("Renamed successfully")
|
|
parent = os.path.dirname(rel_path)
|
|
return redirect(url_for('filemanager.index', path=parent))
|
|
|
|
@filemanager.route('/cut', methods=['POST'])
|
|
def cut():
|
|
# Add one or more items to the clipboard.
|
|
paths = request.form.getlist('paths')
|
|
session['clipboard'] = paths
|
|
flash("Item(s) added to clipboard")
|
|
return redirect(request.referrer or url_for('filemanager.index'))
|
|
|
|
@filemanager.route('/paste', methods=['POST'])
|
|
def paste():
|
|
# Move clipboard items to the destination (current folder).
|
|
dest = request.form.get('dest', '')
|
|
try:
|
|
dest_abs = secure_path(dest)
|
|
except Exception:
|
|
return "Invalid destination", 400
|
|
if not os.path.isdir(dest_abs):
|
|
return "Destination not a directory", 400
|
|
clipboard = session.get('clipboard', [])
|
|
for rel_path in clipboard:
|
|
try:
|
|
abs_path = secure_path(rel_path)
|
|
filename = os.path.basename(abs_path)
|
|
shutil.move(abs_path, os.path.join(dest_abs, filename))
|
|
except Exception as e:
|
|
flash(f"Error moving {rel_path}: {str(e)}")
|
|
session.pop('clipboard', None)
|
|
flash("Moved items successfully")
|
|
return redirect(url_for('filemanager.index', path=dest))
|
|
|
|
@filemanager.route('/mkdir', methods=['POST'])
|
|
def mkdir():
|
|
# The current directory is passed as a hidden form field.
|
|
rel_path = request.form.get('path', '')
|
|
# The new directory name is passed from the form.
|
|
dirname = request.form.get('dirname', '')
|
|
if not dirname:
|
|
flash("Directory name cannot be empty")
|
|
return redirect(url_for('filemanager.index', path=rel_path))
|
|
try:
|
|
# Use secure_filename to sanitize the new directory name.
|
|
safe_dirname = secure_filename(dirname)
|
|
# Build the target path relative to the current directory.
|
|
new_dir_path = secure_path(os.path.join(rel_path, safe_dirname))
|
|
os.makedirs(new_dir_path, exist_ok=False)
|
|
flash(f"Directory '{dirname}' created successfully")
|
|
except Exception as e:
|
|
flash(f"Error creating directory: {str(e)}")
|
|
return redirect(url_for('filemanager.index', path=rel_path))
|
|
|
|
@filemanager.route('/cancel_move', methods=['POST'])
|
|
def cancel_move():
|
|
session.pop('clipboard', None)
|
|
flash("Move cancelled")
|
|
return redirect(request.referrer or url_for('filemanager.index'))
|
|
|
|
return filemanager
|