diff options
Diffstat (limited to 'weed/admin/view/app/file_browser.templ')
| -rw-r--r-- | weed/admin/view/app/file_browser.templ | 376 |
1 files changed, 375 insertions, 1 deletions
diff --git a/weed/admin/view/app/file_browser.templ b/weed/admin/view/app/file_browser.templ index a1e00555f..83db7df0f 100644 --- a/weed/admin/view/app/file_browser.templ +++ b/weed/admin/view/app/file_browser.templ @@ -228,7 +228,7 @@ templ FileBrowser(data dash.FileBrowserData) { } </td> <td> - <code class="small">{ entry.Mode }</code> + <code class="small permissions-display" data-mode={ entry.Mode } data-is-directory={ fmt.Sprintf("%t", entry.IsDirectory) }>{ entry.Mode }</code> </td> <td> <div class="btn-group btn-group-sm" role="group"> @@ -356,6 +356,380 @@ templ FileBrowser(data dash.FileBrowserData) { </div> </div> </div> + + <!-- JavaScript for file browser functionality --> + <script> + document.addEventListener('DOMContentLoaded', function() { + // Format permissions in the main table + document.querySelectorAll('.permissions-display').forEach(element => { + const mode = element.getAttribute('data-mode'); + const isDirectory = element.getAttribute('data-is-directory') === 'true'; + if (mode) { + element.textContent = formatPermissions(mode, isDirectory); + } + }); + + // Handle file browser action buttons (download, view, properties, delete) + document.addEventListener('click', function(e) { + const button = e.target.closest('[data-action]'); + if (!button) return; + + const action = button.getAttribute('data-action'); + const path = button.getAttribute('data-path'); + + if (!path) return; + + switch(action) { + case 'download': + downloadFile(path); + break; + case 'view': + viewFile(path); + break; + case 'properties': + showFileProperties(path); + break; + case 'delete': + if (confirm('Are you sure you want to delete "' + path + '"?')) { + deleteFile(path); + } + break; + } + }); + + // Initialize file manager event handlers from admin.js + if (typeof setupFileManagerEventHandlers === 'function') { + setupFileManagerEventHandlers(); + } + }); + + // File browser specific functions + function downloadFile(path) { + // Open download URL in new tab + window.open('/api/files/download?path=' + encodeURIComponent(path), '_blank'); + } + + function viewFile(path) { + // Open file viewer in new tab + window.open('/api/files/view?path=' + encodeURIComponent(path), '_blank'); + } + + function showFileProperties(path) { + // Fetch file properties and show in modal + fetch('/api/files/properties?path=' + encodeURIComponent(path)) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert('Error loading file properties: ' + data.error); + } else { + displayFileProperties(data); + } + }) + .catch(error => { + console.error('Error fetching file properties:', error); + alert('Error loading file properties: ' + error.message); + }); + } + + function displayFileProperties(data) { + // Create a comprehensive modal for file properties + const modalHtml = '<div class="modal fade" id="filePropertiesModal" tabindex="-1">' + + '<div class="modal-dialog modal-lg">' + + '<div class="modal-content">' + + '<div class="modal-header">' + + '<h5 class="modal-title"><i class="fas fa-info-circle me-2"></i>Properties: ' + (data.name || 'Unknown') + '</h5>' + + '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' + + '</div>' + + '<div class="modal-body">' + + createFilePropertiesContent(data) + + '</div>' + + '<div class="modal-footer">' + + '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' + + '</div>' + + '</div>' + + '</div>' + + '</div>'; + + // Remove existing modal if present + const existingModal = document.getElementById('filePropertiesModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body and show + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modal = new bootstrap.Modal(document.getElementById('filePropertiesModal')); + modal.show(); + + // Remove modal when hidden + document.getElementById('filePropertiesModal').addEventListener('hidden.bs.modal', function() { + this.remove(); + }); + } + + function createFilePropertiesContent(data) { + let html = '<div class="row">' + + '<div class="col-12">' + + '<h6 class="text-primary"><i class="fas fa-file me-1"></i>Basic Information</h6>' + + '<table class="table table-sm">' + + '<tr><td style="width: 120px;"><strong>Name:</strong></td><td>' + (data.name || 'N/A') + '</td></tr>' + + '<tr><td><strong>Full Path:</strong></td><td><code class="text-break">' + (data.full_path || 'N/A') + '</code></td></tr>' + + '<tr><td><strong>Type:</strong></td><td>' + (data.is_directory ? 'Directory' : 'File') + '</td></tr>'; + + if (!data.is_directory) { + html += '<tr><td><strong>Size:</strong></td><td>' + (data.size_formatted || (data.size ? formatBytes(data.size) : 'N/A')) + '</td></tr>' + + '<tr><td><strong>MIME Type:</strong></td><td>' + (data.mime_type || 'N/A') + '</td></tr>'; + } + + html += '</table>' + + '</div>' + + '</div>' + + '<div class="row">' + + '<div class="col-md-6">' + + '<h6 class="text-primary"><i class="fas fa-clock me-1"></i>Timestamps</h6>' + + '<table class="table table-sm">'; + + if (data.modified_time) { + html += '<tr><td><strong>Modified:</strong></td><td>' + data.modified_time + '</td></tr>'; + } + if (data.created_time) { + html += '<tr><td><strong>Created:</strong></td><td>' + data.created_time + '</td></tr>'; + } + + html += '</table>' + + '</div>' + + '<div class="col-md-6">' + + '<h6 class="text-primary"><i class="fas fa-shield-alt me-1"></i>Permissions</h6>' + + '<table class="table table-sm">'; + + if (data.file_mode) { + const rwxPermissions = formatPermissions(data.file_mode, data.is_directory); + html += '<tr><td><strong>Permissions:</strong></td><td><code>' + rwxPermissions + '</code></td></tr>'; + } + if (data.uid !== undefined) { + html += '<tr><td><strong>User ID:</strong></td><td>' + data.uid + '</td></tr>'; + } + if (data.gid !== undefined) { + html += '<tr><td><strong>Group ID:</strong></td><td>' + data.gid + '</td></tr>'; + } + + html += '</table>' + + '</div>' + + '</div>'; + + // Add advanced info + html += '<div class="row">' + + '<div class="col-12">' + + '<h6 class="text-primary"><i class="fas fa-cog me-1"></i>Advanced</h6>' + + '<table class="table table-sm">'; + + if (data.chunk_count) { + html += '<tr><td style="width: 120px;"><strong>Chunks:</strong></td><td>' + data.chunk_count + '</td></tr>'; + } + if (data.ttl_formatted) { + html += '<tr><td><strong>TTL:</strong></td><td>' + data.ttl_formatted + '</td></tr>'; + } + + html += '</table>' + + '</div>' + + '</div>'; + + // Add chunk details if available (show top 5) + if (data.chunks && data.chunks.length > 0) { + const chunksToShow = data.chunks.slice(0, 5); + html += '<div class="row mt-3">' + + '<div class="col-12">' + + '<h6 class="text-primary"><i class="fas fa-puzzle-piece me-1"></i>Chunk Details' + + (data.chunk_count > 5 ? ' (Top 5 of ' + data.chunk_count + ')' : ' (' + data.chunk_count + ')') + + '</h6>' + + '<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">' + + '<table class="table table-sm table-striped">' + + '<thead>' + + '<tr>' + + '<th>File ID</th>' + + '<th>Offset</th>' + + '<th>Size</th>' + + '<th>ETag</th>' + + '</tr>' + + '</thead>' + + '<tbody>'; + + chunksToShow.forEach(chunk => { + html += '<tr>' + + '<td><code class="small">' + (chunk.file_id || 'N/A') + '</code></td>' + + '<td>' + formatBytes(chunk.offset || 0) + '</td>' + + '<td>' + formatBytes(chunk.size || 0) + '</td>' + + '<td><code class="small">' + (chunk.e_tag || 'N/A') + '</code></td>' + + '</tr>'; + }); + + html += '</tbody>' + + '</table>' + + '</div>' + + '</div>' + + '</div>'; + } + + // Add extended attributes if present + if (data.extended && Object.keys(data.extended).length > 0) { + html += '<div class="row">' + + '<div class="col-12">' + + '<h6 class="text-primary"><i class="fas fa-tags me-1"></i>Extended Attributes</h6>' + + '<table class="table table-sm">'; + + for (const [key, value] of Object.entries(data.extended)) { + html += '<tr><td><strong>' + key + ':</strong></td><td>' + value + '</td></tr>'; + } + + html += '</table>' + + '</div>' + + '</div>'; + } + + return html; + } + + function uploadFile() { + const modal = new bootstrap.Modal(document.getElementById('uploadFileModal')); + modal.show(); + } + + function toggleSelectAll() { + const selectAllCheckbox = document.getElementById('selectAll'); + const checkboxes = document.querySelectorAll('.file-checkbox'); + + checkboxes.forEach(checkbox => { + checkbox.checked = selectAllCheckbox.checked; + }); + + updateDeleteSelectedButton(); + } + + function updateDeleteSelectedButton() { + const checkboxes = document.querySelectorAll('.file-checkbox:checked'); + const deleteBtn = document.getElementById('deleteSelectedBtn'); + + if (checkboxes.length > 0) { + deleteBtn.style.display = 'inline-block'; + } else { + deleteBtn.style.display = 'none'; + } + } + + // Helper function to format bytes + function formatBytes(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + // Helper function to format permissions in rwxrwxrwx format + function formatPermissions(mode, isDirectory) { + // Check if mode is already in rwxrwxrwx format (e.g., "drwxr-xr-x" or "-rw-r--r--") + if (mode && (mode.startsWith('d') || mode.startsWith('-') || mode.startsWith('l')) && mode.length === 10) { + return mode; // Already formatted + } + + // Convert to number - could be octal string or decimal + let permissions; + if (typeof mode === 'string') { + // Try parsing as octal first, then decimal + if (mode.startsWith('0') && mode.length <= 4) { + permissions = parseInt(mode, 8); + } else { + permissions = parseInt(mode, 10); + } + } else { + permissions = parseInt(mode, 10); + } + + if (isNaN(permissions)) { + return isDirectory ? 'drwxr-xr-x' : '-rw-r--r--'; // Default fallback + } + + // Handle Go's os.ModeDir conversion + // Go's os.ModeDir is 0x80000000 (2147483648), but Unix S_IFDIR is 0o40000 (16384) + let fileType = '-'; + + // Check for Go's os.ModeDir flag + if (permissions & 0x80000000) { + fileType = 'd'; + } + // Check for standard Unix file type bits + else if ((permissions & 0xF000) === 0x4000) { // S_IFDIR (0o40000) + fileType = 'd'; + } else if ((permissions & 0xF000) === 0x8000) { // S_IFREG (0o100000) + fileType = '-'; + } else if ((permissions & 0xF000) === 0xA000) { // S_IFLNK (0o120000) + fileType = 'l'; + } else if ((permissions & 0xF000) === 0x2000) { // S_IFCHR (0o020000) + fileType = 'c'; + } else if ((permissions & 0xF000) === 0x6000) { // S_IFBLK (0o060000) + fileType = 'b'; + } else if ((permissions & 0xF000) === 0x1000) { // S_IFIFO (0o010000) + fileType = 'p'; + } else if ((permissions & 0xF000) === 0xC000) { // S_IFSOCK (0o140000) + fileType = 's'; + } + // Fallback to isDirectory parameter if file type detection fails + else if (isDirectory) { + fileType = 'd'; + } + + // Permission bits (always use the lower 12 bits for permissions) + const owner = (permissions >> 6) & 7; + const group = (permissions >> 3) & 7; + const others = permissions & 7; + + // Convert number to rwx format + function numToRwx(num) { + const r = (num & 4) ? 'r' : '-'; + const w = (num & 2) ? 'w' : '-'; + const x = (num & 1) ? 'x' : '-'; + return r + w + x; + } + + return fileType + numToRwx(owner) + numToRwx(group) + numToRwx(others); + } + + function exportFileList() { + // Simple CSV export of file list + const rows = Array.from(document.querySelectorAll('#fileTable tbody tr')).map(row => { + const cells = row.querySelectorAll('td'); + if (cells.length > 1) { + return { + name: cells[1].textContent.trim(), + size: cells[2].textContent.trim(), + type: cells[3].textContent.trim(), + modified: cells[4].textContent.trim(), + permissions: cells[5].textContent.trim() + }; + } + return null; + }).filter(row => row !== null); + + const csvContent = "data:text/csv;charset=utf-8," + + "Name,Size,Type,Modified,Permissions\n" + + rows.map(r => '"' + r.name + '","' + r.size + '","' + r.type + '","' + r.modified + '","' + r.permissions + '"').join("\n"); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", "files.csv"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + // Handle file checkbox changes + document.addEventListener('change', function(e) { + if (e.target.classList.contains('file-checkbox')) { + updateDeleteSelectedButton(); + } + }); + </script> } func countDirectories(entries []dash.FileEntry) int { |
