| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812 |
- package app
- import (
- "fmt"
- "path/filepath"
- "strings"
- "github.com/seaweedfs/seaweedfs/weed/admin/dash"
- )
- templ FileBrowser(data dash.FileBrowserData) {
- <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
- <h1 class="h2">
- if data.IsBucketPath && data.BucketName != "" {
- <i class="fas fa-cube me-2"></i>S3 Bucket: {data.BucketName}
- } else {
- <i class="fas fa-folder-open me-2"></i>File Browser
- }
- </h1>
- <div class="btn-toolbar mb-2 mb-md-0">
- <div class="btn-group me-2">
- if data.IsBucketPath && data.BucketName != "" {
- <a href="/object-store/buckets" class="btn btn-sm btn-outline-secondary">
- <i class="fas fa-arrow-left me-1"></i>Back to Buckets
- </a>
- }
- <button type="button" class="btn btn-sm btn-outline-primary" onclick="createFolder()">
- <i class="fas fa-folder-plus me-1"></i>New Folder
- </button>
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="uploadFile()">
- <i class="fas fa-upload me-1"></i>Upload
- </button>
- <button type="button" class="btn btn-sm btn-outline-danger" id="deleteSelectedBtn" onclick="confirmDeleteSelected()" style="display: none;">
- <i class="fas fa-trash me-1"></i>Delete Selected
- </button>
- <button type="button" class="btn btn-sm btn-outline-info" onclick="exportFileList()">
- <i class="fas fa-download me-1"></i>Export
- </button>
- </div>
- </div>
- </div>
- <!-- Breadcrumb Navigation -->
- <nav aria-label="breadcrumb" class="mb-3">
- <ol class="breadcrumb">
- for i, crumb := range data.Breadcrumbs {
- if i == len(data.Breadcrumbs)-1 {
- <li class="breadcrumb-item active" aria-current="page">
- <i class="fas fa-folder me-1"></i>{ crumb.Name }
- </li>
- } else {
- <li class="breadcrumb-item">
- <a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", crumb.Path)) } class="text-decoration-none">
- if crumb.Name == "Root" {
- <i class="fas fa-home me-1"></i>
- } else {
- <i class="fas fa-folder me-1"></i>
- }
- { crumb.Name }
- </a>
- </li>
- }
- }
- </ol>
- </nav>
- <!-- Summary Cards -->
- <div class="row mb-4">
- <div class="col-xl-3 col-md-6 mb-4">
- <div class="card border-left-primary shadow h-100 py-2">
- <div class="card-body">
- <div class="row no-gutters align-items-center">
- <div class="col mr-2">
- <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
- Total Entries
- </div>
- <div class="h5 mb-0 font-weight-bold text-gray-800">
- { fmt.Sprintf("%d", data.TotalEntries) }
- </div>
- </div>
- <div class="col-auto">
- <i class="fas fa-list fa-2x text-gray-300"></i>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="col-xl-3 col-md-6 mb-4">
- <div class="card border-left-success shadow h-100 py-2">
- <div class="card-body">
- <div class="row no-gutters align-items-center">
- <div class="col mr-2">
- <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
- Directories
- </div>
- <div class="h5 mb-0 font-weight-bold text-gray-800">
- { fmt.Sprintf("%d", countDirectories(data.Entries)) }
- </div>
- </div>
- <div class="col-auto">
- <i class="fas fa-folder fa-2x text-gray-300"></i>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="col-xl-3 col-md-6 mb-4">
- <div class="card border-left-info shadow h-100 py-2">
- <div class="card-body">
- <div class="row no-gutters align-items-center">
- <div class="col mr-2">
- <div class="text-xs font-weight-bold text-info text-uppercase mb-1">
- Files
- </div>
- <div class="h5 mb-0 font-weight-bold text-gray-800">
- { fmt.Sprintf("%d", countFiles(data.Entries)) }
- </div>
- </div>
- <div class="col-auto">
- <i class="fas fa-file fa-2x text-gray-300"></i>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="col-xl-3 col-md-6 mb-4">
- <div class="card border-left-warning shadow h-100 py-2">
- <div class="card-body">
- <div class="row no-gutters align-items-center">
- <div class="col mr-2">
- <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
- Total Size
- </div>
- <div class="h5 mb-0 font-weight-bold text-gray-800">
- { formatBytes(data.TotalSize) }
- </div>
- </div>
- <div class="col-auto">
- <i class="fas fa-hdd fa-2x text-gray-300"></i>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- File Listing -->
- <div class="card shadow mb-4">
- <div class="card-header py-3 d-flex justify-content-between align-items-center">
- <h6 class="m-0 font-weight-bold text-primary">
- <i class="fas fa-folder-open me-2"></i>
- if data.CurrentPath == "/" {
- Root Directory
- } else if data.CurrentPath == "/buckets" {
- Object Store Buckets Directory
- <a href="/object-store/buckets" class="btn btn-sm btn-outline-primary ms-2">
- <i class="fas fa-cube me-1"></i>Manage Buckets
- </a>
- } else {
- { filepath.Base(data.CurrentPath) }
- }
- </h6>
- if data.ParentPath != data.CurrentPath {
- <a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", data.ParentPath)) } class="btn btn-sm btn-outline-secondary">
- <i class="fas fa-arrow-up me-1"></i>Up
- </a>
- }
- </div>
- <div class="card-body">
- if len(data.Entries) > 0 {
- <div class="table-responsive">
- <table class="table table-hover" id="fileTable">
- <thead>
- <tr>
- <th width="40px">
- <input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
- </th>
- <th>Name</th>
- <th>Size</th>
- <th>Type</th>
- <th>Modified</th>
- <th>Permissions</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- for _, entry := range data.Entries {
- <tr>
- <td>
- <input type="checkbox" class="file-checkbox" value={ entry.FullPath }>
- </td>
- <td>
- <div class="d-flex align-items-center">
- if entry.IsDirectory {
- <i class="fas fa-folder text-warning me-2"></i>
- <a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", entry.FullPath)) } class="text-decoration-none">
- { entry.Name }
- </a>
- } else {
- <i class={ fmt.Sprintf("fas %s text-muted me-2", getFileIcon(entry.Mime)) }></i>
- <span>{ entry.Name }</span>
- }
- </div>
- </td>
- <td>
- if entry.IsDirectory {
- <span class="text-muted">—</span>
- } else {
- { formatBytes(entry.Size) }
- }
- </td>
- <td>
- <span class="badge bg-light text-dark">
- if entry.IsDirectory {
- Directory
- } else {
- { getMimeDisplayName(entry.Mime) }
- }
- </span>
- </td>
- <td>
- if !entry.ModTime.IsZero() {
- { entry.ModTime.Format("2006-01-02 15:04") }
- } else {
- <span class="text-muted">—</span>
- }
- </td>
- <td>
- <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">
- if !entry.IsDirectory {
- <button type="button" class="btn btn-outline-primary btn-sm" title="Download" data-action="download" data-path={ entry.FullPath }>
- <i class="fas fa-download"></i>
- </button>
- <button type="button" class="btn btn-outline-info btn-sm" title="View" data-action="view" data-path={ entry.FullPath }>
- <i class="fas fa-eye"></i>
- </button>
- }
- <button type="button" class="btn btn-outline-secondary btn-sm" title="Properties" data-action="properties" data-path={ entry.FullPath }>
- <i class="fas fa-info-circle"></i>
- </button>
- <button type="button" class="btn btn-outline-danger btn-sm" title="Delete" data-action="delete" data-path={ entry.FullPath }>
- <i class="fas fa-trash"></i>
- </button>
- </div>
- </td>
- </tr>
- }
- </tbody>
- </table>
- </div>
- } else {
- <div class="text-center py-5">
- <i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
- <h5 class="text-muted">Empty Directory</h5>
- <p class="text-muted">This directory contains no files or subdirectories.</p>
- </div>
- }
- </div>
- </div>
- <!-- Last Updated -->
- <div class="row">
- <div class="col-12">
- <small class="text-muted">
- <i class="fas fa-clock me-1"></i>
- Last updated: { data.LastUpdated.Format("2006-01-02 15:04:05") }
- </small>
- </div>
- </div>
- <!-- Create Folder Modal -->
- <div class="modal fade" id="createFolderModal" tabindex="-1" aria-labelledby="createFolderModalLabel" aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="createFolderModalLabel">
- <i class="fas fa-folder-plus me-2"></i>Create New Folder
- </h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <form id="createFolderForm">
- <div class="mb-3">
- <label for="folderName" class="form-label">Folder Name</label>
- <input type="text" class="form-control" id="folderName" name="folderName" required
- placeholder="Enter folder name" maxlength="255">
- <div class="form-text">
- Folder names cannot contain / or \ characters.
- </div>
- </div>
- <input type="hidden" id="currentPath" name="currentPath" value={ data.CurrentPath }>
- </form>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
- <button type="button" class="btn btn-primary" onclick="submitCreateFolder()">
- <i class="fas fa-folder-plus me-1"></i>Create Folder
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- Upload File Modal -->
- <div class="modal fade" id="uploadFileModal" tabindex="-1" aria-labelledby="uploadFileModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-lg">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="uploadFileModalLabel">
- <i class="fas fa-upload me-2"></i>Upload Files
- </h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <form id="uploadFileForm" enctype="multipart/form-data">
- <div class="mb-3">
- <label for="fileInput" class="form-label">Select Files</label>
- <input type="file" class="form-control" id="fileInput" name="files" multiple required>
- <div class="form-text">
- Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.
- </div>
- </div>
- <input type="hidden" id="uploadPath" name="path" value={ data.CurrentPath }>
-
- <!-- File List Preview -->
- <div id="fileListPreview" class="mb-3" style="display: none;">
- <label class="form-label">Selected Files:</label>
- <div id="selectedFilesList" class="border rounded p-2 bg-light">
- <!-- Files will be listed here -->
- </div>
- </div>
-
- <!-- Upload Progress -->
- <div class="mb-3" id="uploadProgress" style="display: none;">
- <label class="form-label">Upload Progress:</label>
- <div class="progress mb-2">
- <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
- </div>
- <div id="uploadStatus" class="small text-muted">
- Preparing upload...
- </div>
- </div>
- </form>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
- <button type="button" class="btn btn-primary" onclick="submitUploadFile()">
- <i class="fas fa-upload me-1"></i>Upload Files
- </button>
- </div>
- </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 {
- count := 0
- for _, entry := range entries {
- if entry.IsDirectory {
- count++
- }
- }
- return count
- }
- func countFiles(entries []dash.FileEntry) int {
- count := 0
- for _, entry := range entries {
- if !entry.IsDirectory {
- count++
- }
- }
- return count
- }
- func getFileIcon(mime string) string {
- switch {
- case strings.HasPrefix(mime, "image/"):
- return "fa-image"
- case strings.HasPrefix(mime, "video/"):
- return "fa-video"
- case strings.HasPrefix(mime, "audio/"):
- return "fa-music"
- case strings.HasPrefix(mime, "text/"):
- return "fa-file-text"
- case mime == "application/pdf":
- return "fa-file-pdf"
- case mime == "application/zip" || strings.Contains(mime, "archive"):
- return "fa-file-archive"
- case mime == "application/json":
- return "fa-file-code"
- case strings.Contains(mime, "script") || strings.Contains(mime, "javascript"):
- return "fa-file-code"
- default:
- return "fa-file"
- }
- }
- func getMimeDisplayName(mime string) string {
- switch mime {
- case "text/plain":
- return "Text"
- case "text/html":
- return "HTML"
- case "application/json":
- return "JSON"
- case "application/pdf":
- return "PDF"
- case "image/jpeg":
- return "JPEG"
- case "image/png":
- return "PNG"
- case "image/gif":
- return "GIF"
- case "video/mp4":
- return "MP4"
- case "audio/mpeg":
- return "MP3"
- case "application/zip":
- return "ZIP"
- default:
- if strings.HasPrefix(mime, "image/") {
- return "Image"
- } else if strings.HasPrefix(mime, "video/") {
- return "Video"
- } else if strings.HasPrefix(mime, "audio/") {
- return "Audio"
- } else if strings.HasPrefix(mime, "text/") {
- return "Text"
- }
- return "File"
- }
- }
|