<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SFTP File Manager</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2c3e50, #3498db);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 300;
}
.content {
padding: 30px;
}
.connection-form {
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-memory {
background: #e3f2fd;
border-radius: 8px;
padding: 12px 15px;
margin-bottom: 15px;
font-size: 12px;
color: #0c5460;
border: 1px solid #bee5eb;
position: relative;
}
.form-memory strong {
color: #0c5460;
}
.clear-memory-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #0c5460;
cursor: pointer;
text-decoration: underline;
font-size: 11px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2c3e50;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.password-container {
position: relative;
}
.password-container input {
padding-right: 45px;
}
.password-toggle {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #6c757d;
transition: color 0.2s ease;
padding: 5px;
}
.password-toggle:hover {
color: #3498db;
}
.form-group input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-row {
display: flex;
gap: 20px;
}
.form-row .form-group {
flex: 1;
}
.btn {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
padding: 12px 25px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.3);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-danger {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.btn-danger:hover {
box-shadow: 0 5px 15px rgba(231, 76, 60, 0.3);
}
.btn-success {
background: linear-gradient(135deg, #27ae60, #219a52);
}
.btn-success:hover {
box-shadow: 0 5px 15px rgba(39, 174, 96, 0.3);
}
.status {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 600;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.file-manager {
display: none;
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
border: 1px solid #e9ecef;
}
.file-manager.active {
display: block;
}
.file-controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.current-path {
background: white;
padding: 10px 15px;
border-radius: 8px;
border: 1px solid #dee2e6;
font-family: monospace;
flex: 1;
min-width: 200px;
}
.file-list {
background: white;
border-radius: 10px;
border: 1px solid #dee2e6;
overflow: hidden;
}
.file-item {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f1f3f4;
transition: background-color 0.2s ease;
}
.file-item:hover {
background: #f8f9fa;
}
.file-item:last-child {
border-bottom: none;
}
.file-icon {
width: 20px;
height: 20px;
margin-right: 10px;
flex-shrink: 0;
}
.file-name {
flex: 1;
font-weight: 500;
}
.file-name.directory {
color: #3498db;
cursor: pointer;
}
.file-size {
color: #6c757d;
font-size: 12px;
margin-right: 15px;
min-width: 60px;
}
.file-date {
color: #6c757d;
font-size: 12px;
margin-right: 15px;
min-width: 120px;
}
.file-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.upload-area {
border: 2px dashed #dee2e6;
border-radius: 10px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #3498db;
background: #f8f9fa;
}
.upload-area.dragover {
border-color: #3498db;
background: #e3f2fd;
}
@media (max-width: 768px) {
.file-controls {
flex-direction: column;
align-items: stretch;
}
.current-path {
order: -1;
margin-bottom: 10px;
}
.file-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.file-info {
display: flex;
width: 100%;
justify-content: space-between;
font-size: 12px;
}
.form-row {
flex-direction: column;
}
.btn {
margin: 5px 0;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗂️ SFTP File Manager</h1>
<p>Secure File Transfer Protocol Connector</p>
</div>
<div class="content">
<div id="status-message"></div>
<!-- Connection Form -->
<div id="connection-form" class="connection-form">
<h3 style="margin-bottom: 20px; color: #2c3e50;">Connect to SFTP Server</h3>
<div id="form-memory" class="form-memory" style="display: none;">
<strong>Last used:</strong> <span id="memory-info"></span>
<button type="button" class="clear-memory-btn" onclick="clearFormMemory()">Clear</button>
</div>
<div class="form-row">
<div class="form-group">
<label for="host">Host/Server</label>
<input type="text" id="host" placeholder="example.com" required>
</div>
<div class="form-group">
<label for="port">Port</label>
<input type="number" id="port" value="22" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<div class="password-container">
<input type="password" id="password" required>
<button type="button" class="password-toggle" onclick="togglePassword()" title="Show password">👁️</button>
</div>
</div>
</div>
<div class="form-row">
<button class="btn" id="connect-btn" onclick="connect()">Connect to Server</button>
<button class="btn btn-success" onclick="testConnection()">Test Server</button>
<button class="btn btn-danger" onclick="clearSavedCredentials()">Clear Saved</button>
</div>
</div>
<!-- File Manager -->
<div id="file-manager" class="file-manager">
<div class="file-controls">
<input type="text" id="current-path" class="current-path" value="/" readonly>
<button class="btn btn-success" onclick="refreshFiles()">↻ Refresh</button>
<button class="btn" onclick="createFolder()">📁 New Folder</button>
<button class="btn btn-danger" onclick="disconnect()">Disconnect</button>
</div>
<div class="upload-area" id="upload-area">
<p>📤 Drag and drop files here or <strong>click to select files</strong></p>
<input type="file" id="file-input" multiple style="display: none;">
</div>
<div id="file-list" class="file-list">
<!-- Files will be populated here -->
</div>
</div>
</div>
</div>
<script>
let currentPath = '/';
let isConnected = false;
const API_URL = 'sftp_connector.php'; // Point this to your PHP file
// Enhanced form memory management
function saveFormData(host, port, username) {
const formData = {
host: host,
port: port,
username: username,
lastUsed: new Date().toISOString(),
timestamp: Date.now()
};
localStorage.setItem('sftp_form_data', JSON.stringify(formData));
}
function loadFormData() {
const savedData = localStorage.getItem('sftp_form_data');
if (savedData) {
try {
const formData = JSON.parse(savedData);
document.getElementById('host').value = formData.host || '';
document.getElementById('port').value = formData.port || 22;
document.getElementById('username').value = formData.username || '';
if (formData.host) {
const memoryDiv = document.getElementById('form-memory');
const memoryInfo = document.getElementById('memory-info');
const lastUsedDate = new Date(formData.lastUsed);
const timeAgo = getTimeAgo(lastUsedDate);
memoryInfo.textContent = `${formData.username}@${formData.host}:${formData.port} (${timeAgo})`;
memoryDiv.style.display = 'block';
showStatus(`Loaded saved connection details for ${formData.host}`, 'info');
}
} catch (e) {
console.error('Error loading saved form data:', e);
}
}
}
function clearFormMemory() {
localStorage.removeItem('sftp_form_data');
document.getElementById('form-memory').style.display = 'none';
showStatus('Form memory cleared', 'info');
}
function getTimeAgo(date) {
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins} minutes ago`;
if (diffHours < 24) return `${diffHours} hours ago`;
if (diffDays < 7) return `${diffDays} days ago`;
return date.toLocaleDateString();
}
function togglePassword() {
const passwordField = document.getElementById('password');
const toggleButton = document.querySelector('.password-toggle');
if (passwordField.type === 'password') {
passwordField.type = 'text';
toggleButton.textContent = '🙈';
toggleButton.title = 'Hide password';
} else {
passwordField.type = 'password';
toggleButton.textContent = '👁️';
toggleButton.title = 'Show password';
}
}
function saveCredentials(host, port, username) {
// Use the new saveFormData function
saveFormData(host, port, username);
}
function clearSavedCredentials() {
// Clear both old and new storage formats
localStorage.removeItem('sftp_credentials');
clearFormMemory();
document.getElementById('host').value = '';
document.getElementById('username').value = '';
document.getElementById('port').value = 22;
document.getElementById('password').value = '';
}
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('status-message');
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
const timeout = type === 'success' ? 8000 : 5000;
setTimeout(() => {
if (statusDiv.innerHTML.includes(message)) {
statusDiv.innerHTML = '';
}
}, timeout);
}
async function makeRequest(action, data = {}) {
try {
const requestData = { action, ...data };
console.log('Sending request:', requestData);
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
});
const text = await response.text();
console.log('Raw response:', text);
let result;
try {
result = JSON.parse(text);
} catch (parseError) {
console.error('JSON parse error:', parseError);
return {
success: false,
message: 'Invalid JSON response from server',
debug: text.substring(0, 500)
};
}
console.log('Parsed response:', result);
return result;
} catch (error) {
console.error('Request error:', error);
return { success: false, message: 'Network error: ' + error.message };
}
}
async function testConnection() {
showStatus('Testing SFTP connector capabilities...', 'info');
const result = await makeRequest('test');
if (result.success) {
const data = result.data;
let message = 'Server responding! ';
if (data.ssh2_loaded) {
message += 'SSH2 extension: Available ✅';
} else if (data.exec_available) {
message += 'System commands: Available ✅';
} else {
message += 'Limited capabilities ⚠️';
}
showStatus(message, 'success');
console.log('Server capabilities:', result.data);
} else {
showStatus('Server test failed: ' + result.message, 'error');
if (result.debug) {
console.error('Debug info:', result.debug);
}
}
}
async function connect() {
const host = document.getElementById('host').value;
const port = document.getElementById('port').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!host || !username || !password) {
showStatus('Please fill in all required fields', 'error');
return;
}
const connectBtn = document.getElementById('connect-btn');
connectBtn.disabled = true;
connectBtn.textContent = 'Connecting...';
showStatus('Connecting to server...', 'info');
try {
const result = await makeRequest('connect', {
host, port: parseInt(port), username, password
});
if (result.success) {
isConnected = true;
saveCredentials(host, port, username);
document.getElementById('connection-form').style.display = 'none';
document.getElementById('file-manager').classList.add('active');
showStatus('✅ Connected successfully! Loading files...', 'success');
refreshFiles();
} else {
showStatus('❌ Connection failed: ' + result.message, 'error');
}
} catch (error) {
showStatus('❌ Connection error: ' + error.message, 'error');
} finally {
connectBtn.disabled = false;
connectBtn.textContent = 'Connect to Server';
}
}
async function refreshFiles() {
if (!isConnected) return;
showStatus('Loading files...', 'info');
const result = await makeRequest('list', { path: currentPath });
if (result.success) {
displayFiles(result.data);
document.getElementById('current-path').value = currentPath;
showStatus(`Loaded ${result.data.length} items`, 'success');
} else {
showStatus('Failed to load files: ' + result.message, 'error');
}
}
function displayFiles(files) {
const fileList = document.getElementById('file-list');
let html = '';
if (currentPath !== '/') {
html += `
<div class="file-item">
<div class="file-icon">📁</div>
<div class="file-name directory" onclick="navigateToParent()">.. (Parent Directory)</div>
<div class="file-size"></div>
<div class="file-date"></div>
<div class="file-actions"></div>
</div>
`;
}
files.forEach(file => {
const icon = file.is_dir ? '📁' : '📄';
const nameClass = file.is_dir ? 'directory' : '';
const onClick = file.is_dir ? `onclick="navigateToDirectory('${file.path}')"` : '';
const size = file.is_dir ? '' : formatFileSize(file.size);
html += `
<div class="file-item">
<div class="file-icon">${icon}</div>
<div class="file-name ${nameClass}" ${onClick}>${file.name}</div>
<div class="file-size">${size}</div>
<div class="file-date">${file.modified}</div>
<div class="file-actions">
${!file.is_dir ? `<button class="btn btn-small" onclick="downloadFile('${file.path}')">↓ Download</button>` : ''}
<button class="btn btn-small btn-danger" onclick="deleteItem('${file.path}', ${file.is_dir})">🗑️ Delete</button>
</div>
</div>
`;
});
fileList.innerHTML = html;
}
function navigateToDirectory(path) {
currentPath = path;
refreshFiles();
}
function navigateToParent() {
const pathParts = currentPath.split('/').filter(p => p);
pathParts.pop();
currentPath = '/' + pathParts.join('/');
if (currentPath !== '/') currentPath += '/';
refreshFiles();
}
async function deleteItem(path, isDirectory) {
if (!confirm(`Are you sure you want to delete ${isDirectory ? 'this directory' : 'this file'}?`)) {
return;
}
const result = await makeRequest('delete', { path });
if (result.success) {
showStatus('Item deleted successfully', 'success');
refreshFiles();
} else {
showStatus('Delete failed: ' + result.message, 'error');
}
}
async function createFolder() {
const folderName = prompt('Enter folder name:');
if (!folderName) return;
const folderPath = currentPath + folderName;
const result = await makeRequest('create_folder', { path: folderPath });
if (result.success) {
showStatus('Folder created successfully', 'success');
refreshFiles();
} else {
showStatus('Failed to create folder: ' + result.message, 'error');
}
}
async function disconnect() {
const result = await makeRequest('disconnect');
isConnected = false;
document.getElementById('connection-form').style.display = 'block';
document.getElementById('file-manager').classList.remove('active');
showStatus('Disconnected from server', 'info');
document.getElementById('password').value = '';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function downloadFile(remotePath) {
const filename = remotePath.split('/').pop();
showStatus(`Downloading ${filename}...`, 'info');
console.log('Download requested for:', remotePath);
showStatus('Download feature requires additional server-side implementation', 'error');
}
// File upload handling
document.addEventListener('DOMContentLoaded', function() {
loadFormData(); // Load saved form data on page load
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
uploadArea.addEventListener('click', () => {
fileInput.click();
});
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && e.target.closest('#connection-form')) {
connect();
}
});
// Auto-save form data when fields change
['host', 'port', 'username'].forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.addEventListener('blur', () => {
const host = document.getElementById('host').value.trim();
const port = document.getElementById('port').value.trim();
const username = document.getElementById('username').value.trim();
if (host && username) {
saveFormData(host, port || 22, username);
loadFormData(); // Refresh the memory display
}
});
}
});
});
function handleFiles(files) {
if (!isConnected) {
showStatus('Please connect to server first', 'error');
return;
}
for (let file of files) {
uploadFile(file);
}
}
async function uploadFile(file) {
showStatus(`Uploading ${file.name}...`, 'info');
console.log('Upload requested for:', file.name);
showStatus('Upload feature requires additional server-side implementation for file handling', 'error');
}
</script>
</body>
</html>