foldsite/src/server/file_manager.py
Tanishq Dubey 2adde253c9
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
start updating docs, other changes to file upload
2025-03-16 21:22:28 -04:00

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