432 lines
20 KiB
HTML
432 lines
20 KiB
HTML
<!-- FTP File Browser for SLMs v2.0 - Folder Download Support -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Download Files from SLMs</h2>
|
|
|
|
{% if units %}
|
|
<div class="space-y-6">
|
|
{% for unit_item in units %}
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
<!-- Unit Header -->
|
|
<div class="bg-gray-50 dark:bg-gray-900 px-4 py-3 flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<h3 class="font-semibold text-gray-900 dark:text-white">
|
|
{{ unit_item.unit.id }}
|
|
</h3>
|
|
{% if unit_item.location %}
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
@ {{ unit_item.location.name }}
|
|
</span>
|
|
{% endif %}
|
|
<span id="ftp-status-{{ unit_item.unit.id }}" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
|
Checking...
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="enableFTP('{{ unit_item.unit.id }}')"
|
|
id="enable-ftp-{{ unit_item.unit.id }}"
|
|
class="px-3 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
|
disabled>
|
|
Enable FTP
|
|
</button>
|
|
<button onclick="disableFTP('{{ unit_item.unit.id }}')"
|
|
id="disable-ftp-{{ unit_item.unit.id }}"
|
|
class="px-3 py-1 text-xs bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
|
disabled>
|
|
Disable FTP
|
|
</button>
|
|
<button onclick="loadFTPFiles('{{ unit_item.unit.id }}', '/NL-43')"
|
|
id="browse-ftp-{{ unit_item.unit.id }}"
|
|
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors"
|
|
disabled>
|
|
Browse Files
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FTP File List -->
|
|
<div id="ftp-files-{{ unit_item.unit.id }}" class="hidden" data-location-id="{{ unit_item.location.id if unit_item.location else '' }}">
|
|
<div class="p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
|
</svg>
|
|
<span id="current-path-{{ unit_item.unit.id }}" class="text-sm font-mono text-gray-600 dark:text-gray-400">/NL-43</span>
|
|
<button onclick="loadFTPFiles('{{ unit_item.unit.id }}', '/NL-43')"
|
|
class="ml-auto text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="ftp-file-list-{{ unit_item.unit.id }}" class="space-y-1">
|
|
<!-- Files will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
|
|
</svg>
|
|
<p>No units assigned to this project</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
// Check FTP status for all units on load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
{% for unit_item in units %}
|
|
checkFTPStatus('{{ unit_item.unit.id }}');
|
|
{% endfor %}
|
|
});
|
|
|
|
async function checkFTPStatus(unitId) {
|
|
const statusSpan = document.getElementById(`ftp-status-${unitId}`);
|
|
const enableBtn = document.getElementById(`enable-ftp-${unitId}`);
|
|
const disableBtn = document.getElementById(`disable-ftp-${unitId}`);
|
|
const browseBtn = document.getElementById(`browse-ftp-${unitId}`);
|
|
|
|
try {
|
|
const response = await fetch(`/api/slmm/${unitId}/ftp/status`);
|
|
const data = await response.json();
|
|
|
|
if (data.ftp_enabled) {
|
|
statusSpan.textContent = 'FTP Enabled';
|
|
statusSpan.className = 'px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
|
|
enableBtn.disabled = true;
|
|
disableBtn.disabled = false;
|
|
browseBtn.disabled = false;
|
|
} else {
|
|
statusSpan.textContent = 'FTP Disabled';
|
|
statusSpan.className = 'px-2 py-1 text-xs font-medium rounded-full bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300';
|
|
enableBtn.disabled = false;
|
|
disableBtn.disabled = true;
|
|
browseBtn.disabled = true;
|
|
}
|
|
} catch (error) {
|
|
statusSpan.textContent = 'Error';
|
|
statusSpan.className = 'px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
|
|
console.error('Error checking FTP status:', error);
|
|
}
|
|
}
|
|
|
|
async function enableFTP(unitId) {
|
|
const enableBtn = document.getElementById(`enable-ftp-${unitId}`);
|
|
enableBtn.disabled = true;
|
|
enableBtn.textContent = 'Enabling...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/slmm/${unitId}/ftp/enable`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
await checkFTPStatus(unitId);
|
|
// Auto-load files after enabling
|
|
setTimeout(() => loadFTPFiles(unitId, '/NL-43'), 1000);
|
|
} else {
|
|
alert('Failed to enable FTP');
|
|
}
|
|
} catch (error) {
|
|
alert('Error enabling FTP: ' + error);
|
|
} finally {
|
|
enableBtn.textContent = 'Enable FTP';
|
|
enableBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function disableFTP(unitId) {
|
|
if (!confirm('Disable FTP on this unit? This will close the FTP connection.')) return;
|
|
|
|
const disableBtn = document.getElementById(`disable-ftp-${unitId}`);
|
|
disableBtn.disabled = true;
|
|
disableBtn.textContent = 'Disabling...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/slmm/${unitId}/ftp/disable`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
await checkFTPStatus(unitId);
|
|
// Hide file list
|
|
document.getElementById(`ftp-files-${unitId}`).classList.add('hidden');
|
|
} else {
|
|
alert('Failed to disable FTP');
|
|
}
|
|
} catch (error) {
|
|
alert('Error disabling FTP: ' + error);
|
|
} finally {
|
|
disableBtn.textContent = 'Disable FTP';
|
|
disableBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function loadFTPFiles(unitId, path) {
|
|
const fileListDiv = document.getElementById(`ftp-file-list-${unitId}`);
|
|
const filesContainer = document.getElementById(`ftp-files-${unitId}`);
|
|
const currentPathSpan = document.getElementById(`current-path-${unitId}`);
|
|
|
|
// Show loading
|
|
fileListDiv.innerHTML = '<div class="text-center py-4 text-gray-500">Loading files...</div>';
|
|
filesContainer.classList.remove('hidden');
|
|
currentPathSpan.textContent = path;
|
|
|
|
try {
|
|
const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
|
|
const data = await response.json();
|
|
|
|
if (!data.files || data.files.length === 0) {
|
|
fileListDiv.innerHTML = '<div class="text-center py-4 text-gray-500">No files found</div>';
|
|
return;
|
|
}
|
|
|
|
// Sort: directories first, then files
|
|
const sorted = data.files.sort((a, b) => {
|
|
if (a.is_dir && !b.is_dir) return -1;
|
|
if (!a.is_dir && b.is_dir) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
// Render file list
|
|
let html = '';
|
|
for (const file of sorted) {
|
|
const icon = file.is_dir
|
|
? '<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path></svg>'
|
|
: '<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"></path></svg>';
|
|
|
|
const sizeStr = file.is_dir ? '' : formatFileSize(file.size);
|
|
|
|
html += `
|
|
<div class="flex items-center gap-3 px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-800 rounded">
|
|
${icon}
|
|
<div class="flex-1 min-w-0 ${file.is_dir ? 'cursor-pointer' : ''}" ${file.is_dir ? `onclick="loadFTPFiles('${unitId}', '${file.path}')"` : ''}>
|
|
<div class="text-sm font-medium text-gray-900 dark:text-white truncate">${file.name}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">${file.modified}${sizeStr ? ' • ' + sizeStr : ''}</div>
|
|
</div>
|
|
${file.is_dir ? `
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="event.stopPropagation(); downloadFolderToServer('${unitId}', '${file.path}', '${file.name}')"
|
|
class="px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
|
|
title="Download entire folder to server and add to database">
|
|
<svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path>
|
|
</svg>
|
|
To Server (ZIP)
|
|
</button>
|
|
<button onclick="event.stopPropagation(); downloadFTPFolder('${unitId}', '${file.path}', '${file.name}')"
|
|
class="px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
title="Download entire folder as ZIP to your computer">
|
|
<svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
To Browser (ZIP)
|
|
</button>
|
|
</div>
|
|
` : `
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="downloadToServer('${unitId}', '${file.path}', '${file.name}')"
|
|
class="px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
|
|
title="Download to server and add to database">
|
|
<svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path>
|
|
</svg>
|
|
To Server
|
|
</button>
|
|
<button onclick="downloadFTPFile('${unitId}', '${file.path}', '${file.name}')"
|
|
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded hover:bg-seismo-navy transition-colors"
|
|
title="Download directly to your computer">
|
|
<svg class="w-3 h-3 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
To Browser
|
|
</button>
|
|
</div>
|
|
`}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
fileListDiv.innerHTML = html;
|
|
} catch (error) {
|
|
fileListDiv.innerHTML = '<div class="text-center py-4 text-red-500">Error loading files: ' + error + '</div>';
|
|
console.error('Error loading FTP files:', error);
|
|
}
|
|
}
|
|
|
|
async function downloadFTPFile(unitId, remotePath, fileName) {
|
|
const btn = event.target;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Downloading...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/slmm/${unitId}/ftp/download`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
remote_path: remotePath
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
} else {
|
|
const errorData = await response.json();
|
|
alert('Download failed: ' + (errorData.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
alert('Error downloading file: ' + error);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Download';
|
|
}
|
|
}
|
|
|
|
async function downloadFTPFolder(unitId, remotePath, folderName) {
|
|
const btn = event.target;
|
|
const originalHTML = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<svg class="w-3 h-3 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Downloading...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/slmm/${unitId}/ftp/download-folder`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
remote_path: remotePath
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${folderName}.zip`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
// Show success message
|
|
alert(`✓ Folder "${folderName}" downloaded successfully as ZIP file!`);
|
|
} else {
|
|
const errorData = await response.json();
|
|
alert('Folder download failed: ' + (errorData.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
alert('Error downloading folder: ' + error);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalHTML;
|
|
}
|
|
}
|
|
|
|
async function downloadFolderToServer(unitId, remotePath, folderName) {
|
|
const btn = event.target;
|
|
const originalHTML = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<svg class="w-3 h-3 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Downloading...';
|
|
|
|
// Get location_id from the unit's data attribute
|
|
const unitContainer = btn.closest('[id^="ftp-files-"]');
|
|
const locationId = unitContainer.dataset.locationId;
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/{{ project_id }}/ftp-download-folder-to-server`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
unit_id: unitId,
|
|
remote_path: remotePath,
|
|
location_id: locationId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
// Show success message
|
|
alert(`✓ Folder "${folderName}" downloaded to server successfully as ZIP!\n\nFile ID: ${data.file_id}\nSize: ${formatFileSize(data.file_size)}`);
|
|
|
|
// Refresh the downloaded files list
|
|
htmx.trigger('#project-files', 'refresh');
|
|
} else {
|
|
alert('Folder download to server failed: ' + (data.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
alert('Error downloading folder to server: ' + error);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalHTML;
|
|
}
|
|
}
|
|
|
|
async function downloadToServer(unitId, remotePath, fileName) {
|
|
const btn = event.target;
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<svg class="w-3 h-3 inline mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>Downloading...';
|
|
|
|
// Get location_id from the unit's data attribute
|
|
const unitContainer = btn.closest('[id^="ftp-files-"]');
|
|
const locationId = unitContainer.dataset.locationId;
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/{{ project_id }}/ftp-download-to-server`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
unit_id: unitId,
|
|
remote_path: remotePath,
|
|
location_id: locationId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
// Show success message
|
|
alert(`✓ ${fileName} downloaded to server successfully!\n\nFile ID: ${data.file_id}\nSize: ${formatFileSize(data.file_size)}`);
|
|
|
|
// Refresh the downloaded files list
|
|
htmx.trigger('#project-files', 'refresh');
|
|
} else {
|
|
alert('Download to server failed: ' + (data.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
alert('Error downloading to server: ' + error);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
|
return (bytes / 1073741824).toFixed(2) + ' GB';
|
|
}
|
|
</script>
|