Skip to content

Instantly share code, notes, and snippets.

@VTSTech
Last active March 11, 2026 18:02
Show Gist options
  • Select an option

  • Save VTSTech/8014e5b8b7fa26cec3457c15d77c2a17 to your computer and use it in GitHub Desktop.

Select an option

Save VTSTech/8014e5b8b7fa26cec3457c15d77c2a17 to your computer and use it in GitHub Desktop.
GLM-5 File Manager by VTSTech
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
"""GLM-5 File Manager Panel v1.31 for z.ai by VTSTech"""
#
# ╔══════════════════════════════════════════════════════════════════════════════╗
# ║ TO RUN THIS FILE MANAGER: ║
# ║ ║
# ║ 1. Start the server: ║
# ║ python3 GLM-FileManager.py & ║
# ║ ║
# ║ 2. Set up cloudflared tunnel (for remote access): ║
# ║ ║
# ║ Install cloudflared (if not installed): ║
# ║ - Linux: curl -L https://github.com/cloudflare/cloudflared/releases/ ║
# ║ latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared && ║
# ║ chmod +x /tmp/cloudflared ║
# ║ - Or download from: https://github.com/cloudflare/cloudflared/releases ║
# ║ ║
# ║ Run the tunnel: ║
# ║ /tmp/cloudflared tunnel --url http://localhost:8765 ║
# ║ ║
# ║ 3. Access via the cloudflared URL provided (e.g., https://xxx.trycloudflare.com) ║
# ║ ║
# ║ 4. Login with credentials (default): ║
# ║ Username: admin ║
# ║ Password: changeme ║
# ║ (Change AUTH_USERNAME and AUTH_PASSWORD in the script to customize) ║
# ║ ║
# ║ To stop: pkill -f GLM-FileManager OR use the new STOP button in the UI ║
# ╚══════════════════════════════════════════════════════════════════════════════╝
#
# ═══════════════════════════════════════════════════════════════════════════════
# CHANGELOG
# ═══════════════════════════════════════════════════════════════════════════════
# v1.31 - 2025-03-10
# + ADDED STOP BUTTON to shutdown server from the web UI
#
# v1.3 - 2025-03-07
# + Added archive extraction (zip, tar, tar.gz, tar.bz2)
# + Added archive compression (create zip from selected files/folders)
# + Improved folder upload with webkitGetAsEntry API for recursive directory upload
# + Added right-click context menu for quick actions
# + Added PHP syntax highlighting support
# + Added file actions on hover (Extract button for archives)
# + Improved error handling with try-catch blocks and user notifications
# + Added SO_REUSEADDR for faster server restarts
# * Fixed file viewing functionality (double-click to open)
# * Fixed Prism.js syntax highlighting in editor
# * Fixed context menu compress to use all selected files (not just clicked one)
#
# v1.2 - Previous release
# + Basic file manager with upload, download, delete, edit
# + Syntax highlighting for common languages
# + Image preview
# + Drag and drop upload
# ═══════════════════════════════════════════════════════════════════════════════
import http.server
import socketserver
import os
import json
import mimetypes
import base64
import zipfile
import tarfile
import shutil
import tempfile
import threading
import time
from datetime import datetime
from urllib.parse import unquote
BASE_DIR = "/home/z/my-project"
PORT = 8765
# ═══════════════════════════════════════════════════════════════════════════════
# AUTH CONFIGURATION
# Change these credentials to secure your File Manager
AUTH_USERNAME = "admin"
AUTH_PASSWORD = "changeme"
# ═══════════════════════════════════════════════════════════════════════════════
def is_archive(filename):
"""Check if file is a supported archive format."""
ext = filename.lower().split('.')[-1]
return ext in ['zip', 'tar', 'gz', 'bz2', 'tgz', 'tbz2']
def get_archive_type(filename):
"""Get archive type from filename."""
lower = filename.lower()
if lower.endswith('.zip'):
return 'zip'
elif lower.endswith('.tar.gz') or lower.endswith('.tgz'):
return 'tar.gz'
elif lower.endswith('.tar.bz2') or lower.endswith('.tbz2'):
return 'tar.bz2'
elif lower.endswith('.tar'):
return 'tar'
return None
def extract_archive(archive_path, dest_dir):
"""Extract archive to destination directory."""
archive_type = get_archive_type(archive_path)
if archive_type == 'zip':
with zipfile.ZipFile(archive_path, 'r') as zf:
zf.extractall(dest_dir)
elif archive_type in ['tar.gz', 'tar']:
with tarfile.open(archive_path, 'r:*') as tf:
tf.extractall(dest_dir)
elif archive_type == 'tar.bz2':
with tarfile.open(archive_path, 'r:bz2') as tf:
tf.extractall(dest_dir)
else:
raise ValueError(f"Unsupported archive type: {archive_type}")
def create_zip(source_path, dest_zip):
"""Create a zip archive from file or directory."""
with zipfile.ZipFile(dest_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
if os.path.isfile(source_path):
zf.write(source_path, os.path.basename(source_path))
else:
for root, dirs, files in os.walk(source_path):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, os.path.dirname(source_path))
zf.write(file_path, arcname)
class FileManager(http.server.SimpleHTTPRequestHandler):
def check_auth(self):
"""Check Basic Auth credentials."""
auth = self.headers.get('Authorization')
if not auth:
return False
try:
# Parse "Basic <base64 credentials>"
auth_decoded = base64.b64decode(auth.split()[1]).decode('utf-8')
username, password = auth_decoded.split(':', 1)
return username == AUTH_USERNAME and password == AUTH_PASSWORD
except:
return False
def send_auth_required(self):
"""Send 401 Unauthorized response."""
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm="File Manager"')
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b'<h1>401 Unauthorized</h1><p>Valid credentials required.</p>')
def get_dir_listing(self, path, sort_by='name', sort_dir='asc'):
"""Get directory listing with sorting support."""
try:
items = []
for item in os.listdir(path):
full_path = os.path.join(path, item)
stat = os.stat(full_path)
items.append({
'name': item,
'is_dir': os.path.isdir(full_path),
'size': stat.st_size if os.path.isfile(full_path) else '-',
'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
'modified_ts': stat.st_mtime, # For sorting
'is_archive': is_archive(item)
})
# Sort: folders first, then files
folders = [i for i in items if i['is_dir']]
files = [i for i in items if not i['is_dir']]
# Sort folders and files separately
reverse = sort_dir == 'desc'
if sort_by == 'name':
folders.sort(key=lambda x: x['name'].lower(), reverse=reverse)
files.sort(key=lambda x: x['name'].lower(), reverse=reverse)
elif sort_by == 'date':
folders.sort(key=lambda x: x['modified_ts'], reverse=reverse)
files.sort(key=lambda x: x['modified_ts'], reverse=reverse)
elif sort_by == 'size':
folders.sort(key=lambda x: x['name'].lower()) # Folders by name
files.sort(key=lambda x: x['size'] if x['size'] != '-' else 0, reverse=reverse)
# Remove temp sorting field before returning
for item in items:
item.pop('modified_ts', None)
return folders + files
except Exception as e:
return []
def send_json(self, data):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def do_GET(self):
# Check auth for all requests
if not self.check_auth():
self.send_auth_required()
return
if self.path == '/' or self.path == '/index.html':
self.serve_panel()
elif self.path == '/api/list':
rel_path = self.headers.get('X-Path', '')
sort_by = self.headers.get('X-Sort-By', 'name')
sort_dir = self.headers.get('X-Sort-Dir', 'asc')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR):
self.send_json({'error': 'Access denied'})
return
self.send_json({
'path': rel_path,
'items': self.get_dir_listing(full_path, sort_by, sort_dir)
})
elif self.path == '/api/download':
rel_path = self.headers.get('X-Path', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR) or not os.path.isfile(full_path):
self.send_response(404)
self.end_headers()
return
self.send_response(200)
self.send_header('Content-type', 'application/octet-stream')
self.send_header('Content-Disposition', f'attachment; filename="{os.path.basename(full_path)}"')
self.end_headers()
with open(full_path, 'rb') as f:
self.wfile.write(f.read())
elif self.path == '/api/view':
rel_path = self.headers.get('X-Path', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR) or not os.path.isfile(full_path):
self.send_json({'error': 'File not found'})
return
try:
with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
lines = content.count('\n') + 1
self.send_json({'content': content, 'path': rel_path, 'lines': lines})
except:
self.send_json({'error': 'Cannot read file'})
elif self.path == '/api/image':
rel_path = self.headers.get('X-Path', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR) or not os.path.isfile(full_path):
self.send_response(404)
self.end_headers()
return
mime_type, _ = mimetypes.guess_type(full_path)
if not mime_type or not mime_type.startswith('image/'):
self.send_response(400)
self.end_headers()
return
with open(full_path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-type', mime_type)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
elif self.path == '/api/stats':
total_files = 0
total_dirs = 0
total_size = 0
for root, dirs, files in os.walk(BASE_DIR):
total_dirs += len(dirs)
total_files += len(files)
for f in files:
try:
total_size += os.path.getsize(os.path.join(root, f))
except:
pass
self.send_json({
'files': total_files,
'dirs': total_dirs,
'size': total_size,
'size_str': f"{total_size / 1024 / 1024:.1f} MB"
})
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
# Check auth for all requests
if not self.check_auth():
self.send_auth_required()
return
content_length = int(self.headers.get('Content-Length', 0))
content_type = self.headers.get('Content-Type', '')
if self.path == '/api/upload':
try:
rel_path = self.headers.get('X-Path', '')
filename = self.headers.get('X-Filename', '')
if not filename:
self.send_json({'error': 'No filename'})
return
full_dir = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_dir.startswith(BASE_DIR):
self.send_json({'error': 'Access denied'})
return
os.makedirs(full_dir, exist_ok=True)
full_path = os.path.join(full_dir, filename)
body = self.rfile.read(content_length)
with open(full_path, 'wb') as f:
f.write(body)
self.send_json({'success': True, 'message': f'Uploaded {filename}'})
except Exception as e:
self.send_json({'error': str(e)})
elif self.path == '/api/save':
try:
body = self.rfile.read(content_length).decode()
data = json.loads(body)
rel_path = data.get('path', '')
content = data.get('content', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR):
self.send_json({'error': 'Access denied'})
return
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
self.send_json({'success': True, 'message': 'File saved'})
except Exception as e:
self.send_json({'error': str(e)})
elif self.path == '/api/delete':
try:
body = self.rfile.read(content_length).decode()
data = json.loads(body)
rel_path = data.get('path', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR):
self.send_json({'error': 'Access denied'})
return
if os.path.isfile(full_path):
os.remove(full_path)
elif os.path.isdir(full_path):
shutil.rmtree(full_path)
self.send_json({'success': True, 'message': 'Deleted'})
except Exception as e:
self.send_json({'error': str(e)})
elif self.path == '/api/mkdir':
try:
body = self.rfile.read(content_length).decode()
data = json.loads(body)
rel_path = data.get('path', '')
name = data.get('name', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path, name))
if not full_path.startswith(BASE_DIR):
self.send_json({'error': 'Access denied'})
return
os.makedirs(full_path, exist_ok=True)
self.send_json({'success': True, 'message': 'Folder created'})
except Exception as e:
self.send_json({'error': str(e)})
elif self.path == '/api/extract':
try:
body = self.rfile.read(content_length).decode()
data = json.loads(body)
rel_path = data.get('path', '')
full_path = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not full_path.startswith(BASE_DIR) or not os.path.isfile(full_path):
self.send_json({'error': 'File not found'})
return
if not is_archive(full_path):
self.send_json({'error': 'Not a supported archive format'})
return
# Extract to same directory as archive
dest_dir = os.path.dirname(full_path)
extract_archive(full_path, dest_dir)
self.send_json({'success': True, 'message': f'Extracted to {os.path.basename(dest_dir) or "root"}'})
except Exception as e:
self.send_json({'error': str(e)})
elif self.path == '/api/compress':
try:
body = self.rfile.read(content_length).decode()
data = json.loads(body)
items = data.get('items', [])
rel_path = data.get('path', '')
archive_name = data.get('name', 'archive.zip')
if not items:
self.send_json({'error': 'No items selected'})
return
# Ensure .zip extension
if not archive_name.endswith('.zip'):
archive_name += '.zip'
dest_dir = os.path.normpath(os.path.join(BASE_DIR, rel_path))
if not dest_dir.startswith(BASE_DIR):
self.send_json({'error': 'Access denied'})
return
archive_path = os.path.join(dest_dir, archive_name)
# Create zip with selected items
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for item in items:
item_path = os.path.normpath(os.path.join(BASE_DIR, rel_path, item))
if not item_path.startswith(BASE_DIR):
continue
if os.path.isfile(item_path):
zf.write(item_path, item)
elif os.path.isdir(item_path):
for root, dirs, files in os.walk(item_path):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.join(item, os.path.relpath(file_path, item_path))
zf.write(file_path, arcname)
self.send_json({'success': True, 'message': f'Created {archive_name}'})
except Exception as e:
self.send_json({'error': str(e)})
elif self.path == '/api/stop':
try:
self.send_json({'success': True, 'message': 'Server shutting down...'})
# Run shutdown in a separate thread to allow response to be sent
threading.Thread(target=self.server.shutdown).start()
except Exception as e:
self.send_json({'error': str(e)})
else:
self.send_response(404)
self.end_headers()
def serve_panel(self):
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
html_content = '''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>File Manager - my-project</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
<style>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; }
.container { display: flex; height: 100vh; }
/* Sidebar */
.sidebar { width: 260px; background: #161b22; border-right: 1px solid #30363d; padding: 16px; display: flex; flex-direction: column; }
.logo { font-size: 1.2rem; font-weight: 800; color: #3fb950; margin-bottom: 4px; }
.logo-sub { font-size: 0.75rem; color: #8b949e; margin-bottom: 8px; }
.branding { font-size: 0.7rem; color: #484f58; margin-bottom: 16px; padding-top: 8px; border-top: 1px solid #21262d; }
.branding a { color: #58a6ff; text-decoration: none; }
.branding a:hover { text-decoration: underline; }
.stats { background: #21262d; border-radius: 8px; padding: 12px; margin-bottom: 16px; }
.stat-row { display: flex; justify-content: space-between; font-size: 0.85rem; margin: 4px 0; }
.stat-label { color: #8b949e; }
.stat-value { color: #e6edf3; font-weight: 600; }
.nav-btn { background: transparent; border: none; color: #8b949e; text-align: left; padding: 10px 12px; border-radius: 6px; cursor: pointer; width: 100%; margin: 2px 0; }
.nav-btn:hover { background: #21262d; color: #e6edf3; }
.nav-btn.active { background: #21262d; color: #3fb950; }
.actions { margin-top: auto; }
.btn { padding: 10px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; width: 100%; margin: 4px 0; }
.btn-primary { background: #3fb950; color: white; }
.btn-primary:hover { background: #2ea043; }
.btn-danger { background: #f85149; color: white; }
.btn-danger:hover { background: #da3633; }
.btn-secondary { background: #21262d; color: #e6edf3; }
.btn-secondary:hover { background: #30363d; }
.btn-warning { background: #d29922; color: white; }
.btn-warning:hover { background: #bb8009; }
/* Main content */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 20px; display: flex; align-items: center; gap: 12px; }
.breadcrumb { display: flex; align-items: center; gap: 4px; flex: 1; overflow-x: auto; }
.breadcrumb-item { background: #21262d; padding: 6px 12px; border-radius: 6px; cursor: pointer; white-space: nowrap; }
.breadcrumb-item:hover { background: #30363d; }
.breadcrumb-item.home { background: #3fb950; color: white; }
.toolbar { display: flex; gap: 8px; }
.tool-btn { background: #21262d; border: none; color: #e6edf3; padding: 8px 12px; border-radius: 6px; cursor: pointer; }
.tool-btn:hover { background: #30363d; }
.sort-btn { background: #21262d; border: none; color: #8b949e; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; transition: all 0.15s; }
.sort-btn:hover { background: #30363d; color: #e6edf3; }
.sort-btn.active { background: #3fb950; color: white; font-weight: 600; }
/* File list */
.file-list { flex: 1; overflow-y: auto; padding: 16px 20px; }
.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.file-item { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; cursor: pointer; display: flex; align-items: center; gap: 10px; transition: all 0.15s; position: relative; }
.file-item:hover { border-color: #3fb950; background: #21262d; }
.file-item.selected { border-color: #58a6ff; background: rgba(88,166,255,0.1); }
.file-icon { font-size: 1.5rem; }
.file-info { flex: 1; min-width: 0; }
.file-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-meta { font-size: 0.75rem; color: #8b949e; }
.file-actions { display: none; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); gap: 4px; }
.file-item:hover .file-actions { display: flex; }
.file-action-btn { background: #30363d; border: none; color: #e6edf3; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 0.75rem; }
.file-action-btn:hover { background: #3fb950; }
/* Editor with syntax highlighting */
.editor-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: none; z-index: 100; }
.editor-overlay.active { display: flex; align-items: center; justify-content: center; }
.editor-panel { background: #161b22; border: 1px solid #30363d; border-radius: 12px; width: 95%; max-width: 1200px; height: 90vh; display: flex; flex-direction: column; }
.editor-header { padding: 12px 16px; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 12px; background: #21262d; border-radius: 12px 12px 0 0; }
.editor-title { flex: 1; font-weight: 600; font-family: monospace; }
.editor-info { color: #8b949e; font-size: 0.8rem; margin-right: 12px; }
.editor-content { flex: 1; overflow: hidden; background: #0d1117; border-radius: 0 0 12px 12px; }
.editor-content pre { margin: 0; padding: 16px; height: 100%; overflow: auto; }
.editor-content pre code { font-family: 'JetBrains Mono', 'Consolas', monospace; font-size: 13px; line-height: 1.5; }
/* Editable textarea overlay */
.edit-wrapper { position: relative; height: 100%; overflow: hidden; }
.edit-textarea {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: transparent; color: transparent; caret-color: #e6edf3;
border: none; padding: 16px; resize: none; outline: none;
font-family: 'JetBrains Mono', 'Consolas', monospace; font-size: 13px; line-height: 1.5;
z-index: 2; white-space: pre; overflow: auto; tab-size: 4;
}
.edit-highlight {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
padding: 16px; overflow: hidden; pointer-events: none; z-index: 1;
}
.edit-highlight pre {
margin: 0; padding: 0; background: transparent; white-space: pre; overflow: visible;
}
.edit-highlight code {
font-family: 'JetBrains Mono', 'Consolas', monospace; font-size: 13px; line-height: 1.5;
white-space: pre; tab-size: 4; display: block;
}
/* Image viewer */
.image-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.9); display: none; z-index: 100; align-items: center; justify-content: center; }
.image-overlay.active { display: flex; }
.image-container { max-width: 95%; max-height: 95%; }
.image-container img { max-width: 100%; max-height: 90vh; object-fit: contain; border-radius: 8px; }
.image-close { position: absolute; top: 20px; right: 20px; background: #21262d; border: 1px solid #30363d; color: #e6edf3; padding: 10px 16px; border-radius: 6px; cursor: pointer; font-weight: 600; }
.image-close:hover { background: #30363d; }
.image-info { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background: #21262d; border: 1px solid #30363d; padding: 8px 16px; border-radius: 6px; font-size: 0.85rem; color: #8b949e; }
/* Upload drop zone */
.drop-zone { position: fixed; inset: 0; background: rgba(63,185,80,0.2); border: 4px dashed #3fb950; display: none; z-index: 50; align-items: center; justify-content: center; flex-direction: column; gap: 16px; }
.drop-zone.active { display: flex; }
.drop-zone-text { font-size: 2rem; font-weight: 800; color: #3fb950; }
.drop-zone-sub { font-size: 1rem; color: #8b949e; }
/* Notifications */
.notifications { position: fixed; top: 16px; right: 16px; z-index: 200; }
.notification { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 16px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; animation: slideIn 0.3s; }
.notification.success { border-color: #3fb950; }
.notification.error { border-color: #f85149; }
.notification.info { border-color: #58a6ff; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* Upload progress */
.upload-progress { position: fixed; bottom: 16px; right: 16px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 16px; min-width: 300px; display: none; z-index: 201; }
.upload-progress.active { display: block; }
.upload-bar { height: 4px; background: #21262d; border-radius: 2px; margin-top: 8px; }
.upload-bar-fill { height: 100%; background: #3fb950; border-radius: 2px; transition: width 0.3s; }
/* Context menu */
.context-menu { position: fixed; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 4px 0; min-width: 180px; z-index: 300; display: none; }
.context-menu.active { display: block; }
.context-item { padding: 8px 16px; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 0.9rem; }
.context-item:hover { background: #21262d; }
.context-item.danger { color: #f85149; }
.context-divider { height: 1px; background: #30363d; margin: 4px 0; }
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<div class="logo">📁 File Manager v1.31</div>
<div class="logo-sub">/home/z/my-project</div>
<div class="branding">
Written by <a href="https://www.vts-tech.org" target="_blank">VTSTech</a> ·
<a href="https://github.com/VTSTech" target="_blank">GitHub</a>
</div>
<div class="stats" id="stats">
<div class="stat-row"><span class="stat-label">Files</span><span class="stat-value" id="stat-files">-</span></div>
<div class="stat-row"><span class="stat-label">Folders</span><span class="stat-value" id="stat-dirs">-</span></div>
<div class="stat-row"><span class="stat-label">Size</span><span class="stat-value" id="stat-size">-</span></div>
</div>
<button class="nav-btn active" onclick="navigateTo('')">🏠 Root</button>
<button class="nav-btn" onclick="navigateTo('upload')">📤 Upload</button>
<button class="nav-btn" onclick="navigateTo('download')">📥 Download</button>
<button class="nav-btn" onclick="refresh()">🔄 Refresh</button>
<div class="actions">
<button class="btn btn-primary" onclick="showUpload()">📤 Upload Files/Folder</button>
<button class="btn btn-secondary" onclick="createFolder()">📁 New Folder</button>
<button class="btn btn-warning" onclick="compressSelected()">📦 Compress Selected</button>
<button class="btn btn-danger" onclick="stopServer()">🛑 Stop Server</button>
</div>
</div>
<div class="main">
<div class="header">
<div class="breadcrumb" id="breadcrumb"></div>
<div class="toolbar">
<button class="sort-btn active" data-sort="name" data-label="Name" onclick="changeSort('name')">A→Z</button>
<button class="sort-btn" data-sort="date" data-label="Date" onclick="changeSort('date')">Date</button>
<button class="sort-btn" data-sort="size" data-label="Size" onclick="changeSort('size')">Size</button>
<span style="color:#30363d;margin:0 8px;">|</span>
<button class="tool-btn" onclick="refresh()">🔄</button>
<button class="tool-btn" onclick="downloadSelected()">⬇️</button>
<button class="tool-btn" onclick="deleteSelected()">🗑️</button>
</div>
</div>
<div class="file-list">
<div class="file-grid" id="file-grid"></div>
</div>
</div>
</div>
<!-- Editor Modal -->
<div class="editor-overlay" id="editor-overlay" onclick="closeEditor(event)">
<div class="editor-panel" onclick="event.stopPropagation()">
<div class="editor-header">
<span class="editor-title" id="editor-title">file.py</span>
<span class="editor-info" id="editor-info"></span>
<button class="btn btn-secondary" onclick="closeEditor()" style="width:auto;">Cancel</button>
<button class="btn btn-primary" onclick="saveFile()" style="width:auto;">💾 Save</button>
</div>
<div class="editor-content">
<div class="edit-wrapper">
<div class="edit-highlight" id="edit-highlight"><code class="language-plaintext"></code></div>
<textarea class="edit-textarea" id="edit-textarea" oninput="updateHighlight()" onscroll="syncScroll()" spellcheck="false"></textarea>
</div>
</div>
</div>
</div>
<!-- Image Viewer -->
<div class="image-overlay" id="image-overlay" onclick="closeImageViewer()">
<button class="image-close">✕ Close</button>
<div class="image-container">
<img id="image-preview" src="" alt="Preview">
</div>
<div class="image-info" id="image-info"></div>
</div>
<div class="drop-zone" id="drop-zone">
<span class="drop-zone-text">📁 Drop files or folders here</span>
<span class="drop-zone-sub">Supports folder upload and archive extraction</span>
</div>
<div class="notifications" id="notifications"></div>
<div class="upload-progress" id="upload-progress">
<div id="upload-filename">Uploading...</div>
<div class="upload-bar"><div class="upload-bar-fill" id="upload-bar-fill"></div></div>
</div>
<!-- Context Menu -->
<div class="context-menu" id="context-menu">
<div class="context-item" onclick="contextAction('open')">📂 Open</div>
<div class="context-item" onclick="contextAction('download')">⬇️ Download</div>
<div class="context-divider"></div>
<div class="context-item" onclick="contextAction('extract')">📦 Extract Archive</div>
<div class="context-item" onclick="contextAction('compress')">🗜️ Compress</div>
<div class="context-divider"></div>
<div class="context-item danger" onclick="contextAction('delete')">🗑️ Delete</div>
</div>
<!-- Prism.js for syntax highlighting -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup-templating.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-php.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>
<script>
let currentPath = '';
let selectedFiles = [];
let currentEditPath = '';
let currentLang = 'plaintext';
let sortBy = 'name';
let sortDir = 'asc';
let authCredentials = null;
let contextTarget = null;
// Prompt for credentials if not set
function getAuthHeader() {
if (!authCredentials) {
const username = prompt('Username:');
if (!username) return null;
const password = prompt('Password:');
authCredentials = btoa(`${username}:${password}`);
}
return `Basic ${authCredentials}`;
}
document.addEventListener('DOMContentLoaded', () => {
loadStats();
navigateTo('');
setupDragDrop();
setupContextMenu();
});
async function api(endpoint, options = {}) {
try {
const auth = getAuthHeader();
if (!auth) {
alert('Authentication required');
return null;
}
options.headers = options.headers || {};
options.headers['Authorization'] = auth;
const res = await fetch(endpoint, options);
if (res.status === 401) {
authCredentials = null;
alert('Invalid credentials. Please try again.');
const newAuth = getAuthHeader();
if (newAuth) return api(endpoint, options);
return null;
}
if (!res.ok) {
return { error: `HTTP ${res.status}: ${res.statusText}` };
}
return res.json();
} catch (err) {
console.error('API error:', err);
return { error: err.message };
}
}
async function loadStats() {
const data = await api('/api/stats');
if (data) {
document.getElementById('stat-files').textContent = data.files;
document.getElementById('stat-dirs').textContent = data.dirs;
document.getElementById('stat-size').textContent = data.size_str;
}
}
async function loadDirectory(path) {
const auth = getAuthHeader();
if (!auth) return;
const data = await api('/api/list', {
headers: {
'X-Path': path,
'X-Sort-By': sortBy,
'X-Sort-Dir': sortDir
}
});
if (data) {
renderBreadcrumb(path);
renderFiles(data.items);
}
}
function changeSort(newSort) {
if (sortBy === newSort) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortBy = newSort;
sortDir = 'asc';
}
updateSortButtons();
loadDirectory(currentPath);
}
function updateSortButtons() {
document.querySelectorAll('.sort-btn').forEach(btn => {
btn.classList.remove('active');
const sort = btn.dataset.sort;
if (sort === sortBy) {
btn.classList.add('active');
btn.textContent = sort === 'name' ? (sortDir === 'asc' ? 'A→Z' : 'Z→A') :
sort === 'date' ? (sortDir === 'asc' ? 'Oldest' : 'Newest') :
(sortDir === 'asc' ? 'Smallest' : 'Largest');
} else {
btn.textContent = btn.dataset.label;
}
});
}
function renderBreadcrumb(path) {
const parts = path.split('/').filter(p => p);
let html = `<div class="breadcrumb-item home" onclick="navigateTo('')">🏠</div>`;
let buildPath = '';
for (const part of parts) {
buildPath += '/' + part;
html += `<div class="breadcrumb-item" onclick="navigateTo('${buildPath.slice(1)}')">${part}</div>`;
}
document.getElementById('breadcrumb').innerHTML = html;
}
function renderFiles(items) {
const grid = document.getElementById('file-grid');
if (items.length === 0) {
grid.innerHTML = '<div style="color:#8b949e;padding:20px;">Empty folder - drag & drop files or folders to upload</div>';
return;
}
grid.innerHTML = items.map(item => `
<div class="file-item ${selectedFiles.includes(item.name) ? 'selected' : ''}"
data-name="${item.name.replace(/"/g, '&quot;')}"
data-is-dir="${item.is_dir}"
data-is-archive="${item.is_archive || false}"
onclick="selectFile(event, '${item.name.replace(/'/g, "\\'")}', ${item.is_dir})"
ondblclick="openItem('${item.name.replace(/'/g, "\\'")}', ${item.is_dir})"
oncontextmenu="showContextMenu(event, '${item.name.replace(/'/g, "\\'")}', ${item.is_dir}, ${item.is_archive || false})">
<span class="file-icon">${item.is_dir ? '📁' : getFileIcon(item.name)}</span>
<div class="file-info">
<div class="file-name">${item.name}</div>
<div class="file-meta">${item.is_dir ? 'Folder' : formatSize(item.size)} • ${item.modified}${item.is_archive ? ' • 📦' : ''}</div>
</div>
<div class="file-actions">
${item.is_archive ? '<button class="file-action-btn" onclick="event.stopPropagation(); extractFile(\\'' + item.name.replace(/'/g, "\\'") + '\\')">Extract</button>' : ''}
</div>
</div>
`).join('');
}
function getFileIcon(name) {
const ext = name.split('.').pop().toLowerCase();
const icons = {
'py': '🐍', 'js': '📜', 'ts': '💠', 'jsx': '⚛️', 'tsx': '⚛️',
'html': '🌐', 'css': '🎨', 'json': '📋', 'yaml': '⚙️', 'yml': '⚙️',
'md': '📝', 'txt': '📄', 'csv': '📊', 'sql': '🗃️',
'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️', 'gif': '🖼️', 'svg': '🖼️', 'webp': '🖼️',
'pdf': '📕', 'zip': '📦', 'tar': '📦', 'gz': '📦', 'bz2': '📦', 'tgz': '📦',
'sh': '💻', 'env': '🔐', 'php': '🐘'
};
return icons[ext] || '📄';
}
function isImageFile(name) {
const ext = name.split('.').pop().toLowerCase();
return ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico'].includes(ext);
}
function isArchiveFile(name) {
const lower = name.toLowerCase();
return lower.endsWith('.zip') || lower.endsWith('.tar') ||
lower.endsWith('.tar.gz') || lower.endsWith('.tgz') ||
lower.endsWith('.tar.bz2') || lower.endsWith('.tbz2');
}
function getLanguage(name) {
const ext = name.split('.').pop().toLowerCase();
const langMap = {
'py': 'python', 'js': 'javascript', 'ts': 'typescript',
'jsx': 'jsx', 'tsx': 'tsx', 'html': 'markup', 'htm': 'markup',
'css': 'css', 'json': 'json', 'yaml': 'yaml', 'yml': 'yaml',
'md': 'markdown', 'sh': 'bash', 'bash': 'bash', 'sql': 'sql',
'xml': 'markup', 'svg': 'markup', 'php': 'php'
};
return langMap[ext] || 'plaintext';
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
return (bytes/1024/1024).toFixed(1) + ' MB';
}
function navigateTo(path) {
currentPath = path;
selectedFiles = [];
loadDirectory(path);
}
function refresh() {
loadDirectory(currentPath);
loadStats();
notify('Refreshed', 'success');
}
function selectFile(event, name, isDir) {
if (event.ctrlKey || event.metaKey) {
const idx = selectedFiles.indexOf(name);
if (idx >= 0) selectedFiles.splice(idx, 1);
else selectedFiles.push(name);
} else {
selectedFiles = [name];
}
loadDirectory(currentPath);
}
function openItem(name, isDir) {
try {
console.log('openItem:', name, 'isDir:', isDir);
if (isDir) {
navigateTo(currentPath ? `${currentPath}/${name}` : name);
} else if (isImageFile(name)) {
viewImage(currentPath ? `${currentPath}/${name}` : name, name);
} else {
viewFile(currentPath ? `${currentPath}/${name}` : name);
}
} catch (err) {
console.error('openItem error:', err);
notify('Error opening item: ' + err.message, 'error');
}
}
// Context menu
function setupContextMenu() {
document.addEventListener('click', () => {
document.getElementById('context-menu').classList.remove('active');
});
}
function showContextMenu(event, name, isDir, isArchive) {
event.preventDefault();
event.stopPropagation();
contextTarget = { name, isDir, isArchive };
// Preserve existing selection if right-clicked item is part of it
// Otherwise, set selection to just the clicked item
if (!selectedFiles.includes(name)) {
selectedFiles = [name];
}
const menu = document.getElementById('context-menu');
menu.style.left = event.pageX + 'px';
menu.style.top = event.pageY + 'px';
menu.classList.add('active');
// Show/hide extract option based on archive status
const extractItem = menu.querySelector('[onclick*="extract"]');
if (extractItem) extractItem.style.display = isArchive ? 'flex' : 'none';
}
function contextAction(action) {
if (!contextTarget) return;
const { name, isDir, isArchive } = contextTarget;
const path = currentPath ? `${currentPath}/${name}` : name;
switch(action) {
case 'open':
openItem(name, isDir);
break;
case 'download':
downloadFile(path);
break;
case 'extract':
if (isArchive) extractFile(name);
break;
case 'compress':
// Selection is already preserved in showContextMenu
compressSelected();
break;
case 'delete':
deleteSelected();
break;
}
document.getElementById('context-menu').classList.remove('active');
}
// Archive functions
async function extractFile(name) {
const path = currentPath ? `${currentPath}/${name}` : name;
notify('Extracting archive...', 'info');
const res = await api('/api/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
if (res && res.success) {
notify(res.message, 'success');
refresh();
} else {
notify(res ? res.error : 'Extraction failed', 'error');
}
}
async function compressSelected() {
if (selectedFiles.length === 0) {
notify('No files selected', 'error');
return;
}
const archiveName = prompt('Archive name:', 'archive.zip');
if (!archiveName) return;
notify('Creating archive...', 'info');
const res = await api('/api/compress', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: currentPath,
items: selectedFiles,
name: archiveName
})
});
if (res && res.success) {
notify(res.message, 'success');
selectedFiles = [];
refresh();
} else {
notify(res ? res.error : 'Compression failed', 'error');
}
}
// Image viewer
async function viewImage(path, name) {
const auth = getAuthHeader();
const res = await fetch('/api/image', { headers: { 'X-Path': path, 'Authorization': auth } });
const blob = await res.blob();
const img = document.getElementById('image-preview');
img.src = URL.createObjectURL(blob);
document.getElementById('image-info').textContent = `${name} • ${formatSize(blob.size)}`;
document.getElementById('image-overlay').classList.add('active');
}
function closeImageViewer() {
document.getElementById('image-overlay').classList.remove('active');
}
// Editor with syntax highlighting
async function viewFile(path) {
try {
const data = await api('/api/view', { headers: { 'X-Path': path } });
if (!data) {
notify('Failed to load file', 'error');
return;
}
if (data.error) {
notify(data.error, 'error');
return;
}
currentEditPath = path;
currentLang = getLanguage(path);
const filename = path.split('/').pop();
document.getElementById('editor-title').textContent = filename;
document.getElementById('editor-info').textContent = `${data.lines || 0} lines • ${formatSize(data.content.length)} • ${currentLang}`;
const textarea = document.getElementById('edit-textarea');
textarea.value = data.content;
updateHighlight();
document.getElementById('editor-overlay').classList.add('active');
} catch (err) {
console.error('viewFile error:', err);
notify('Error loading file: ' + err.message, 'error');
}
}
function updateHighlight() {
const textarea = document.getElementById('edit-textarea');
const highlight = document.getElementById('edit-highlight');
const code = textarea.value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
highlight.innerHTML = `<pre class="language-${currentLang}"><code class="language-${currentLang}">${code}</code></pre>`;
Prism.highlightElement(highlight.querySelector('code'));
// Update line count
const lines = textarea.value.split('\\n').length;
document.getElementById('editor-info').textContent = `${lines} lines • ${formatSize(textarea.value.length)} • ${currentLang}`;
}
function syncScroll() {
const textarea = document.getElementById('edit-textarea');
const highlight = document.getElementById('edit-highlight');
highlight.scrollTop = textarea.scrollTop;
highlight.scrollLeft = textarea.scrollLeft;
}
function closeEditor(event) {
if (event && event.target !== event.currentTarget) return;
document.getElementById('editor-overlay').classList.remove('active');
}
async function saveFile() {
const content = document.getElementById('edit-textarea').value;
const res = await api('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentEditPath, content })
});
if (res.success) {
notify('File saved!', 'success');
closeEditor();
} else {
notify(res.error, 'error');
}
}
async function downloadFile(path) {
const auth = getAuthHeader();
const res = await fetch('/api/download', { headers: { 'X-Path': path, 'Authorization': auth } });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = path.split('/').pop();
a.click();
URL.revokeObjectURL(url);
}
function downloadSelected() {
if (selectedFiles.length === 0) {
notify('No files selected', 'error');
return;
}
for (const name of selectedFiles) {
const path = currentPath ? `${currentPath}/${name}` : name;
downloadFile(path);
}
}
async function deleteSelected() {
if (selectedFiles.length === 0) {
notify('No files selected', 'error');
return;
}
if (!confirm(`Delete ${selectedFiles.length} item(s)?`)) return;
for (const name of selectedFiles) {
const path = currentPath ? `${currentPath}/${name}` : name;
await api('/api/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
}
notify('Deleted!', 'success');
selectedFiles = [];
refresh();
}
async function createFolder() {
const name = prompt('Folder name:');
if (!name) return;
const res = await api('/api/mkdir', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentPath, name })
});
if (res.success) {
notify('Folder created!', 'success');
refresh();
} else {
notify(res.error, 'error');
}
}
function showUpload() {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
// Enable directory selection
input.setAttribute('webkitdirectory', '');
input.setAttribute('directory', '');
input.onchange = () => {
if (input.files.length > 0) {
uploadFiles(input.files);
}
};
input.click();
}
async function stopServer() {
if (!confirm('Are you sure you want to STOP the server? The page will become unavailable.')) return;
notify('Sending stop signal...', 'info');
try {
await api('/api/stop', { method: 'POST' });
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;background:#0d1117;color:#e6edf3;font-family:system-ui;flex-direction:column;"><h1>Server Stopped</h1><p>You can close this tab.</p></div>';
} catch (e) {
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;background:#0d1117;color:#e6edf3;font-family:system-ui;flex-direction:column;"><h1>Server Stopped</h1><p>You can close this tab.</p></div>';
}
}
async function uploadFiles(files) {
const progress = document.getElementById('upload-progress');
const bar = document.getElementById('upload-bar-fill');
const filenameEl = document.getElementById('upload-filename');
progress.classList.add('active');
// Build a map of directories to create
const dirs = new Set();
const fileArray = Array.from(files);
for (const file of fileArray) {
// Handle both relativePath (webkitRelativePath) and webkitRelativePath
const relPath = file.webkitRelativePath || file.relativePath || '';
if (relPath) {
const parts = relPath.split('/');
for (let i = 0; i < parts.length - 1; i++) {
dirs.add(parts.slice(0, i + 1).join('/'));
}
}
}
// Create directories first
const auth = getAuthHeader();
for (const dir of Array.from(dirs).sort()) {
const dirPath = currentPath ? `${currentPath}/${dir}` : dir;
await fetch('/api/mkdir', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': auth
},
body: JSON.stringify({ path: '', name: dirPath })
});
}
// Upload files
for (let i = 0; i < fileArray.length; i++) {
const file = fileArray[i];
filenameEl.textContent = `Uploading ${file.name}... (${i + 1}/${fileArray.length})`;
bar.style.width = ((i / fileArray.length) * 100) + '%';
const content = await file.arrayBuffer();
const relPath = file.webkitRelativePath || file.relativePath || '';
const filePath = relPath ? relPath.split('/').slice(0, -1).join('/') : '';
const uploadPath = currentPath ? (filePath ? `${currentPath}/${filePath}` : currentPath) : filePath;
await fetch('/api/upload', {
method: 'POST',
headers: {
'X-Path': uploadPath,
'X-Filename': file.name,
'Authorization': auth
},
body: content
});
}
bar.style.width = '100%';
setTimeout(() => {
progress.classList.remove('active');
notify(`Uploaded ${fileArray.length} file(s)!`, 'success');
refresh();
}, 500);
}
function setupDragDrop() {
const dropZone = document.getElementById('drop-zone');
document.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('active');
});
document.addEventListener('dragleave', (e) => {
if (!e.relatedTarget) dropZone.classList.remove('active');
});
document.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.classList.remove('active');
const items = e.dataTransfer.items;
const files = [];
// Use webkitGetAsEntry for better folder support
if (items && items.length > 0) {
const entries = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === 'file') {
const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
if (entry) {
entries.push(entry);
} else {
const file = item.getAsFile();
if (file) files.push(file);
}
}
}
if (entries.length > 0) {
// Process entries recursively
const allFiles = await processEntries(entries);
if (allFiles.length > 0) {
uploadFiles(allFiles);
}
} else if (files.length > 0) {
uploadFiles(files);
}
} else if (e.dataTransfer.files.length) {
uploadFiles(e.dataTransfer.files);
}
});
}
async function processEntries(entries) {
const files = [];
async function readEntry(entry, path = '') {
if (entry.isFile) {
return new Promise((resolve) => {
entry.file((file) => {
// Add relative path info
file.relativePath = path + file.name;
files.push(file);
resolve();
});
});
} else if (entry.isDirectory) {
const reader = entry.createReader();
const entries = await new Promise((resolve) => {
reader.readEntries(resolve);
});
for (const childEntry of entries) {
await readEntry(childEntry, path + entry.name + '/');
}
}
}
for (const entry of entries) {
await readEntry(entry);
}
return files;
}
function notify(message, type = 'success') {
const container = document.getElementById('notifications');
const div = document.createElement('div');
div.className = `notification ${type}`;
const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
div.innerHTML = `${icon} ${message}`;
container.appendChild(div);
setTimeout(() => div.remove(), 3000);
}
</script>
</body>
</html>'''
self.wfile.write(html_content.encode('utf-8'))
def log_message(self, format, *args):
print(f"[FileManager v1.31] {args[0]}" if args else "")
if __name__ == "__main__":
os.makedirs(BASE_DIR, exist_ok=True)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("0.0.0.0", PORT), FileManager) as httpd:
print(f"✅ File Manager v1.31 (build 2025-03-10) running on port {PORT}")
print(f"📁 Managing: {BASE_DIR}")
print(f"📦 Archive support: zip, tar, tar.gz, tar.bz2")
httpd.serve_forever()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment