file_browser.templ 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  1. package app
  2. import (
  3. "fmt"
  4. "path/filepath"
  5. "strings"
  6. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  7. )
  8. templ FileBrowser(data dash.FileBrowserData) {
  9. <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
  10. <h1 class="h2">
  11. if data.IsBucketPath && data.BucketName != "" {
  12. <i class="fas fa-cube me-2"></i>S3 Bucket: {data.BucketName}
  13. } else {
  14. <i class="fas fa-folder-open me-2"></i>File Browser
  15. }
  16. </h1>
  17. <div class="btn-toolbar mb-2 mb-md-0">
  18. <div class="btn-group me-2">
  19. if data.IsBucketPath && data.BucketName != "" {
  20. <a href="/object-store/buckets" class="btn btn-sm btn-outline-secondary">
  21. <i class="fas fa-arrow-left me-1"></i>Back to Buckets
  22. </a>
  23. }
  24. <button type="button" class="btn btn-sm btn-outline-primary" onclick="createFolder()">
  25. <i class="fas fa-folder-plus me-1"></i>New Folder
  26. </button>
  27. <button type="button" class="btn btn-sm btn-outline-secondary" onclick="uploadFile()">
  28. <i class="fas fa-upload me-1"></i>Upload
  29. </button>
  30. <button type="button" class="btn btn-sm btn-outline-danger" id="deleteSelectedBtn" onclick="confirmDeleteSelected()" style="display: none;">
  31. <i class="fas fa-trash me-1"></i>Delete Selected
  32. </button>
  33. <button type="button" class="btn btn-sm btn-outline-info" onclick="exportFileList()">
  34. <i class="fas fa-download me-1"></i>Export
  35. </button>
  36. </div>
  37. </div>
  38. </div>
  39. <!-- Breadcrumb Navigation -->
  40. <nav aria-label="breadcrumb" class="mb-3">
  41. <ol class="breadcrumb">
  42. for i, crumb := range data.Breadcrumbs {
  43. if i == len(data.Breadcrumbs)-1 {
  44. <li class="breadcrumb-item active" aria-current="page">
  45. <i class="fas fa-folder me-1"></i>{ crumb.Name }
  46. </li>
  47. } else {
  48. <li class="breadcrumb-item">
  49. <a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", crumb.Path)) } class="text-decoration-none">
  50. if crumb.Name == "Root" {
  51. <i class="fas fa-home me-1"></i>
  52. } else {
  53. <i class="fas fa-folder me-1"></i>
  54. }
  55. { crumb.Name }
  56. </a>
  57. </li>
  58. }
  59. }
  60. </ol>
  61. </nav>
  62. <!-- Summary Cards -->
  63. <div class="row mb-4">
  64. <div class="col-xl-3 col-md-6 mb-4">
  65. <div class="card border-left-primary shadow h-100 py-2">
  66. <div class="card-body">
  67. <div class="row no-gutters align-items-center">
  68. <div class="col mr-2">
  69. <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
  70. Total Entries
  71. </div>
  72. <div class="h5 mb-0 font-weight-bold text-gray-800">
  73. { fmt.Sprintf("%d", data.TotalEntries) }
  74. </div>
  75. </div>
  76. <div class="col-auto">
  77. <i class="fas fa-list fa-2x text-gray-300"></i>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. <div class="col-xl-3 col-md-6 mb-4">
  84. <div class="card border-left-success shadow h-100 py-2">
  85. <div class="card-body">
  86. <div class="row no-gutters align-items-center">
  87. <div class="col mr-2">
  88. <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
  89. Directories
  90. </div>
  91. <div class="h5 mb-0 font-weight-bold text-gray-800">
  92. { fmt.Sprintf("%d", countDirectories(data.Entries)) }
  93. </div>
  94. </div>
  95. <div class="col-auto">
  96. <i class="fas fa-folder fa-2x text-gray-300"></i>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. </div>
  102. <div class="col-xl-3 col-md-6 mb-4">
  103. <div class="card border-left-info shadow h-100 py-2">
  104. <div class="card-body">
  105. <div class="row no-gutters align-items-center">
  106. <div class="col mr-2">
  107. <div class="text-xs font-weight-bold text-info text-uppercase mb-1">
  108. Files
  109. </div>
  110. <div class="h5 mb-0 font-weight-bold text-gray-800">
  111. { fmt.Sprintf("%d", countFiles(data.Entries)) }
  112. </div>
  113. </div>
  114. <div class="col-auto">
  115. <i class="fas fa-file fa-2x text-gray-300"></i>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. <div class="col-xl-3 col-md-6 mb-4">
  122. <div class="card border-left-warning shadow h-100 py-2">
  123. <div class="card-body">
  124. <div class="row no-gutters align-items-center">
  125. <div class="col mr-2">
  126. <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
  127. Total Size
  128. </div>
  129. <div class="h5 mb-0 font-weight-bold text-gray-800">
  130. { formatBytes(data.TotalSize) }
  131. </div>
  132. </div>
  133. <div class="col-auto">
  134. <i class="fas fa-hdd fa-2x text-gray-300"></i>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. <!-- File Listing -->
  142. <div class="card shadow mb-4">
  143. <div class="card-header py-3 d-flex justify-content-between align-items-center">
  144. <h6 class="m-0 font-weight-bold text-primary">
  145. <i class="fas fa-folder-open me-2"></i>
  146. if data.CurrentPath == "/" {
  147. Root Directory
  148. } else if data.CurrentPath == "/buckets" {
  149. Object Store Buckets Directory
  150. <a href="/object-store/buckets" class="btn btn-sm btn-outline-primary ms-2">
  151. <i class="fas fa-cube me-1"></i>Manage Buckets
  152. </a>
  153. } else {
  154. { filepath.Base(data.CurrentPath) }
  155. }
  156. </h6>
  157. if data.ParentPath != data.CurrentPath {
  158. <a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", data.ParentPath)) } class="btn btn-sm btn-outline-secondary">
  159. <i class="fas fa-arrow-up me-1"></i>Up
  160. </a>
  161. }
  162. </div>
  163. <div class="card-body">
  164. if len(data.Entries) > 0 {
  165. <div class="table-responsive">
  166. <table class="table table-hover" id="fileTable">
  167. <thead>
  168. <tr>
  169. <th width="40px">
  170. <input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
  171. </th>
  172. <th>Name</th>
  173. <th>Size</th>
  174. <th>Type</th>
  175. <th>Modified</th>
  176. <th>Permissions</th>
  177. <th>Actions</th>
  178. </tr>
  179. </thead>
  180. <tbody>
  181. for _, entry := range data.Entries {
  182. <tr>
  183. <td>
  184. <input type="checkbox" class="file-checkbox" value={ entry.FullPath }>
  185. </td>
  186. <td>
  187. <div class="d-flex align-items-center">
  188. if entry.IsDirectory {
  189. <i class="fas fa-folder text-warning me-2"></i>
  190. <a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", entry.FullPath)) } class="text-decoration-none">
  191. { entry.Name }
  192. </a>
  193. } else {
  194. <i class={ fmt.Sprintf("fas %s text-muted me-2", getFileIcon(entry.Mime)) }></i>
  195. <span>{ entry.Name }</span>
  196. }
  197. </div>
  198. </td>
  199. <td>
  200. if entry.IsDirectory {
  201. <span class="text-muted">—</span>
  202. } else {
  203. { formatBytes(entry.Size) }
  204. }
  205. </td>
  206. <td>
  207. <span class="badge bg-light text-dark">
  208. if entry.IsDirectory {
  209. Directory
  210. } else {
  211. { getMimeDisplayName(entry.Mime) }
  212. }
  213. </span>
  214. </td>
  215. <td>
  216. if !entry.ModTime.IsZero() {
  217. { entry.ModTime.Format("2006-01-02 15:04") }
  218. } else {
  219. <span class="text-muted">—</span>
  220. }
  221. </td>
  222. <td>
  223. <code class="small permissions-display" data-mode={ entry.Mode } data-is-directory={ fmt.Sprintf("%t", entry.IsDirectory) }>{ entry.Mode }</code>
  224. </td>
  225. <td>
  226. <div class="btn-group btn-group-sm" role="group">
  227. if !entry.IsDirectory {
  228. <button type="button" class="btn btn-outline-primary btn-sm" title="Download" data-action="download" data-path={ entry.FullPath }>
  229. <i class="fas fa-download"></i>
  230. </button>
  231. <button type="button" class="btn btn-outline-info btn-sm" title="View" data-action="view" data-path={ entry.FullPath }>
  232. <i class="fas fa-eye"></i>
  233. </button>
  234. }
  235. <button type="button" class="btn btn-outline-secondary btn-sm" title="Properties" data-action="properties" data-path={ entry.FullPath }>
  236. <i class="fas fa-info-circle"></i>
  237. </button>
  238. <button type="button" class="btn btn-outline-danger btn-sm" title="Delete" data-action="delete" data-path={ entry.FullPath }>
  239. <i class="fas fa-trash"></i>
  240. </button>
  241. </div>
  242. </td>
  243. </tr>
  244. }
  245. </tbody>
  246. </table>
  247. </div>
  248. } else {
  249. <div class="text-center py-5">
  250. <i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
  251. <h5 class="text-muted">Empty Directory</h5>
  252. <p class="text-muted">This directory contains no files or subdirectories.</p>
  253. </div>
  254. }
  255. </div>
  256. </div>
  257. <!-- Last Updated -->
  258. <div class="row">
  259. <div class="col-12">
  260. <small class="text-muted">
  261. <i class="fas fa-clock me-1"></i>
  262. Last updated: { data.LastUpdated.Format("2006-01-02 15:04:05") }
  263. </small>
  264. </div>
  265. </div>
  266. <!-- Create Folder Modal -->
  267. <div class="modal fade" id="createFolderModal" tabindex="-1" aria-labelledby="createFolderModalLabel" aria-hidden="true">
  268. <div class="modal-dialog">
  269. <div class="modal-content">
  270. <div class="modal-header">
  271. <h5 class="modal-title" id="createFolderModalLabel">
  272. <i class="fas fa-folder-plus me-2"></i>Create New Folder
  273. </h5>
  274. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  275. </div>
  276. <div class="modal-body">
  277. <form id="createFolderForm">
  278. <div class="mb-3">
  279. <label for="folderName" class="form-label">Folder Name</label>
  280. <input type="text" class="form-control" id="folderName" name="folderName" required
  281. placeholder="Enter folder name" maxlength="255">
  282. <div class="form-text">
  283. Folder names cannot contain / or \ characters.
  284. </div>
  285. </div>
  286. <input type="hidden" id="currentPath" name="currentPath" value={ data.CurrentPath }>
  287. </form>
  288. </div>
  289. <div class="modal-footer">
  290. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
  291. <button type="button" class="btn btn-primary" onclick="submitCreateFolder()">
  292. <i class="fas fa-folder-plus me-1"></i>Create Folder
  293. </button>
  294. </div>
  295. </div>
  296. </div>
  297. </div>
  298. <!-- Upload File Modal -->
  299. <div class="modal fade" id="uploadFileModal" tabindex="-1" aria-labelledby="uploadFileModalLabel" aria-hidden="true">
  300. <div class="modal-dialog modal-lg">
  301. <div class="modal-content">
  302. <div class="modal-header">
  303. <h5 class="modal-title" id="uploadFileModalLabel">
  304. <i class="fas fa-upload me-2"></i>Upload Files
  305. </h5>
  306. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  307. </div>
  308. <div class="modal-body">
  309. <form id="uploadFileForm" enctype="multipart/form-data">
  310. <div class="mb-3">
  311. <label for="fileInput" class="form-label">Select Files</label>
  312. <input type="file" class="form-control" id="fileInput" name="files" multiple required>
  313. <div class="form-text">
  314. Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.
  315. </div>
  316. </div>
  317. <input type="hidden" id="uploadPath" name="path" value={ data.CurrentPath }>
  318. <!-- File List Preview -->
  319. <div id="fileListPreview" class="mb-3" style="display: none;">
  320. <label class="form-label">Selected Files:</label>
  321. <div id="selectedFilesList" class="border rounded p-2 bg-light">
  322. <!-- Files will be listed here -->
  323. </div>
  324. </div>
  325. <!-- Upload Progress -->
  326. <div class="mb-3" id="uploadProgress" style="display: none;">
  327. <label class="form-label">Upload Progress:</label>
  328. <div class="progress mb-2">
  329. <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>
  330. </div>
  331. <div id="uploadStatus" class="small text-muted">
  332. Preparing upload...
  333. </div>
  334. </div>
  335. </form>
  336. </div>
  337. <div class="modal-footer">
  338. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
  339. <button type="button" class="btn btn-primary" onclick="submitUploadFile()">
  340. <i class="fas fa-upload me-1"></i>Upload Files
  341. </button>
  342. </div>
  343. </div>
  344. </div>
  345. </div>
  346. <!-- JavaScript for file browser functionality -->
  347. <script>
  348. document.addEventListener('DOMContentLoaded', function() {
  349. // Format permissions in the main table
  350. document.querySelectorAll('.permissions-display').forEach(element => {
  351. const mode = element.getAttribute('data-mode');
  352. const isDirectory = element.getAttribute('data-is-directory') === 'true';
  353. if (mode) {
  354. element.textContent = formatPermissions(mode, isDirectory);
  355. }
  356. });
  357. // Handle file browser action buttons (download, view, properties, delete)
  358. document.addEventListener('click', function(e) {
  359. const button = e.target.closest('[data-action]');
  360. if (!button) return;
  361. const action = button.getAttribute('data-action');
  362. const path = button.getAttribute('data-path');
  363. if (!path) return;
  364. switch(action) {
  365. case 'download':
  366. downloadFile(path);
  367. break;
  368. case 'view':
  369. viewFile(path);
  370. break;
  371. case 'properties':
  372. showFileProperties(path);
  373. break;
  374. case 'delete':
  375. if (confirm('Are you sure you want to delete "' + path + '"?')) {
  376. deleteFile(path);
  377. }
  378. break;
  379. }
  380. });
  381. // Initialize file manager event handlers from admin.js
  382. if (typeof setupFileManagerEventHandlers === 'function') {
  383. setupFileManagerEventHandlers();
  384. }
  385. });
  386. // File browser specific functions
  387. function downloadFile(path) {
  388. // Open download URL in new tab
  389. window.open('/api/files/download?path=' + encodeURIComponent(path), '_blank');
  390. }
  391. function viewFile(path) {
  392. // Open file viewer in new tab
  393. window.open('/api/files/view?path=' + encodeURIComponent(path), '_blank');
  394. }
  395. function showFileProperties(path) {
  396. // Fetch file properties and show in modal
  397. fetch('/api/files/properties?path=' + encodeURIComponent(path))
  398. .then(response => response.json())
  399. .then(data => {
  400. if (data.error) {
  401. alert('Error loading file properties: ' + data.error);
  402. } else {
  403. displayFileProperties(data);
  404. }
  405. })
  406. .catch(error => {
  407. console.error('Error fetching file properties:', error);
  408. alert('Error loading file properties: ' + error.message);
  409. });
  410. }
  411. function displayFileProperties(data) {
  412. // Create a comprehensive modal for file properties
  413. const modalHtml = '<div class="modal fade" id="filePropertiesModal" tabindex="-1">' +
  414. '<div class="modal-dialog modal-lg">' +
  415. '<div class="modal-content">' +
  416. '<div class="modal-header">' +
  417. '<h5 class="modal-title"><i class="fas fa-info-circle me-2"></i>Properties: ' + (data.name || 'Unknown') + '</h5>' +
  418. '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
  419. '</div>' +
  420. '<div class="modal-body">' +
  421. createFilePropertiesContent(data) +
  422. '</div>' +
  423. '<div class="modal-footer">' +
  424. '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +
  425. '</div>' +
  426. '</div>' +
  427. '</div>' +
  428. '</div>';
  429. // Remove existing modal if present
  430. const existingModal = document.getElementById('filePropertiesModal');
  431. if (existingModal) {
  432. existingModal.remove();
  433. }
  434. // Add modal to body and show
  435. document.body.insertAdjacentHTML('beforeend', modalHtml);
  436. const modal = new bootstrap.Modal(document.getElementById('filePropertiesModal'));
  437. modal.show();
  438. // Remove modal when hidden
  439. document.getElementById('filePropertiesModal').addEventListener('hidden.bs.modal', function() {
  440. this.remove();
  441. });
  442. }
  443. function createFilePropertiesContent(data) {
  444. let html = '<div class="row">' +
  445. '<div class="col-12">' +
  446. '<h6 class="text-primary"><i class="fas fa-file me-1"></i>Basic Information</h6>' +
  447. '<table class="table table-sm">' +
  448. '<tr><td style="width: 120px;"><strong>Name:</strong></td><td>' + (data.name || 'N/A') + '</td></tr>' +
  449. '<tr><td><strong>Full Path:</strong></td><td><code class="text-break">' + (data.full_path || 'N/A') + '</code></td></tr>' +
  450. '<tr><td><strong>Type:</strong></td><td>' + (data.is_directory ? 'Directory' : 'File') + '</td></tr>';
  451. if (!data.is_directory) {
  452. html += '<tr><td><strong>Size:</strong></td><td>' + (data.size_formatted || (data.size ? formatBytes(data.size) : 'N/A')) + '</td></tr>' +
  453. '<tr><td><strong>MIME Type:</strong></td><td>' + (data.mime_type || 'N/A') + '</td></tr>';
  454. }
  455. html += '</table>' +
  456. '</div>' +
  457. '</div>' +
  458. '<div class="row">' +
  459. '<div class="col-md-6">' +
  460. '<h6 class="text-primary"><i class="fas fa-clock me-1"></i>Timestamps</h6>' +
  461. '<table class="table table-sm">';
  462. if (data.modified_time) {
  463. html += '<tr><td><strong>Modified:</strong></td><td>' + data.modified_time + '</td></tr>';
  464. }
  465. if (data.created_time) {
  466. html += '<tr><td><strong>Created:</strong></td><td>' + data.created_time + '</td></tr>';
  467. }
  468. html += '</table>' +
  469. '</div>' +
  470. '<div class="col-md-6">' +
  471. '<h6 class="text-primary"><i class="fas fa-shield-alt me-1"></i>Permissions</h6>' +
  472. '<table class="table table-sm">';
  473. if (data.file_mode) {
  474. const rwxPermissions = formatPermissions(data.file_mode, data.is_directory);
  475. html += '<tr><td><strong>Permissions:</strong></td><td><code>' + rwxPermissions + '</code></td></tr>';
  476. }
  477. if (data.uid !== undefined) {
  478. html += '<tr><td><strong>User ID:</strong></td><td>' + data.uid + '</td></tr>';
  479. }
  480. if (data.gid !== undefined) {
  481. html += '<tr><td><strong>Group ID:</strong></td><td>' + data.gid + '</td></tr>';
  482. }
  483. html += '</table>' +
  484. '</div>' +
  485. '</div>';
  486. // Add advanced info
  487. html += '<div class="row">' +
  488. '<div class="col-12">' +
  489. '<h6 class="text-primary"><i class="fas fa-cog me-1"></i>Advanced</h6>' +
  490. '<table class="table table-sm">';
  491. if (data.chunk_count) {
  492. html += '<tr><td style="width: 120px;"><strong>Chunks:</strong></td><td>' + data.chunk_count + '</td></tr>';
  493. }
  494. if (data.ttl_formatted) {
  495. html += '<tr><td><strong>TTL:</strong></td><td>' + data.ttl_formatted + '</td></tr>';
  496. }
  497. html += '</table>' +
  498. '</div>' +
  499. '</div>';
  500. // Add chunk details if available (show top 5)
  501. if (data.chunks && data.chunks.length > 0) {
  502. const chunksToShow = data.chunks.slice(0, 5);
  503. html += '<div class="row mt-3">' +
  504. '<div class="col-12">' +
  505. '<h6 class="text-primary"><i class="fas fa-puzzle-piece me-1"></i>Chunk Details' +
  506. (data.chunk_count > 5 ? ' (Top 5 of ' + data.chunk_count + ')' : ' (' + data.chunk_count + ')') +
  507. '</h6>' +
  508. '<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">' +
  509. '<table class="table table-sm table-striped">' +
  510. '<thead>' +
  511. '<tr>' +
  512. '<th>File ID</th>' +
  513. '<th>Offset</th>' +
  514. '<th>Size</th>' +
  515. '<th>ETag</th>' +
  516. '</tr>' +
  517. '</thead>' +
  518. '<tbody>';
  519. chunksToShow.forEach(chunk => {
  520. html += '<tr>' +
  521. '<td><code class="small">' + (chunk.file_id || 'N/A') + '</code></td>' +
  522. '<td>' + formatBytes(chunk.offset || 0) + '</td>' +
  523. '<td>' + formatBytes(chunk.size || 0) + '</td>' +
  524. '<td><code class="small">' + (chunk.e_tag || 'N/A') + '</code></td>' +
  525. '</tr>';
  526. });
  527. html += '</tbody>' +
  528. '</table>' +
  529. '</div>' +
  530. '</div>' +
  531. '</div>';
  532. }
  533. // Add extended attributes if present
  534. if (data.extended && Object.keys(data.extended).length > 0) {
  535. html += '<div class="row">' +
  536. '<div class="col-12">' +
  537. '<h6 class="text-primary"><i class="fas fa-tags me-1"></i>Extended Attributes</h6>' +
  538. '<table class="table table-sm">';
  539. for (const [key, value] of Object.entries(data.extended)) {
  540. html += '<tr><td><strong>' + key + ':</strong></td><td>' + value + '</td></tr>';
  541. }
  542. html += '</table>' +
  543. '</div>' +
  544. '</div>';
  545. }
  546. return html;
  547. }
  548. function uploadFile() {
  549. const modal = new bootstrap.Modal(document.getElementById('uploadFileModal'));
  550. modal.show();
  551. }
  552. function toggleSelectAll() {
  553. const selectAllCheckbox = document.getElementById('selectAll');
  554. const checkboxes = document.querySelectorAll('.file-checkbox');
  555. checkboxes.forEach(checkbox => {
  556. checkbox.checked = selectAllCheckbox.checked;
  557. });
  558. updateDeleteSelectedButton();
  559. }
  560. function updateDeleteSelectedButton() {
  561. const checkboxes = document.querySelectorAll('.file-checkbox:checked');
  562. const deleteBtn = document.getElementById('deleteSelectedBtn');
  563. if (checkboxes.length > 0) {
  564. deleteBtn.style.display = 'inline-block';
  565. } else {
  566. deleteBtn.style.display = 'none';
  567. }
  568. }
  569. // Helper function to format bytes
  570. function formatBytes(bytes) {
  571. if (bytes === 0) return '0 Bytes';
  572. const k = 1024;
  573. const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  574. const i = Math.floor(Math.log(bytes) / Math.log(k));
  575. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  576. }
  577. // Helper function to format permissions in rwxrwxrwx format
  578. function formatPermissions(mode, isDirectory) {
  579. // Check if mode is already in rwxrwxrwx format (e.g., "drwxr-xr-x" or "-rw-r--r--")
  580. if (mode && (mode.startsWith('d') || mode.startsWith('-') || mode.startsWith('l')) && mode.length === 10) {
  581. return mode; // Already formatted
  582. }
  583. // Convert to number - could be octal string or decimal
  584. let permissions;
  585. if (typeof mode === 'string') {
  586. // Try parsing as octal first, then decimal
  587. if (mode.startsWith('0') && mode.length <= 4) {
  588. permissions = parseInt(mode, 8);
  589. } else {
  590. permissions = parseInt(mode, 10);
  591. }
  592. } else {
  593. permissions = parseInt(mode, 10);
  594. }
  595. if (isNaN(permissions)) {
  596. return isDirectory ? 'drwxr-xr-x' : '-rw-r--r--'; // Default fallback
  597. }
  598. // Handle Go's os.ModeDir conversion
  599. // Go's os.ModeDir is 0x80000000 (2147483648), but Unix S_IFDIR is 0o40000 (16384)
  600. let fileType = '-';
  601. // Check for Go's os.ModeDir flag
  602. if (permissions & 0x80000000) {
  603. fileType = 'd';
  604. }
  605. // Check for standard Unix file type bits
  606. else if ((permissions & 0xF000) === 0x4000) { // S_IFDIR (0o40000)
  607. fileType = 'd';
  608. } else if ((permissions & 0xF000) === 0x8000) { // S_IFREG (0o100000)
  609. fileType = '-';
  610. } else if ((permissions & 0xF000) === 0xA000) { // S_IFLNK (0o120000)
  611. fileType = 'l';
  612. } else if ((permissions & 0xF000) === 0x2000) { // S_IFCHR (0o020000)
  613. fileType = 'c';
  614. } else if ((permissions & 0xF000) === 0x6000) { // S_IFBLK (0o060000)
  615. fileType = 'b';
  616. } else if ((permissions & 0xF000) === 0x1000) { // S_IFIFO (0o010000)
  617. fileType = 'p';
  618. } else if ((permissions & 0xF000) === 0xC000) { // S_IFSOCK (0o140000)
  619. fileType = 's';
  620. }
  621. // Fallback to isDirectory parameter if file type detection fails
  622. else if (isDirectory) {
  623. fileType = 'd';
  624. }
  625. // Permission bits (always use the lower 12 bits for permissions)
  626. const owner = (permissions >> 6) & 7;
  627. const group = (permissions >> 3) & 7;
  628. const others = permissions & 7;
  629. // Convert number to rwx format
  630. function numToRwx(num) {
  631. const r = (num & 4) ? 'r' : '-';
  632. const w = (num & 2) ? 'w' : '-';
  633. const x = (num & 1) ? 'x' : '-';
  634. return r + w + x;
  635. }
  636. return fileType + numToRwx(owner) + numToRwx(group) + numToRwx(others);
  637. }
  638. function exportFileList() {
  639. // Simple CSV export of file list
  640. const rows = Array.from(document.querySelectorAll('#fileTable tbody tr')).map(row => {
  641. const cells = row.querySelectorAll('td');
  642. if (cells.length > 1) {
  643. return {
  644. name: cells[1].textContent.trim(),
  645. size: cells[2].textContent.trim(),
  646. type: cells[3].textContent.trim(),
  647. modified: cells[4].textContent.trim(),
  648. permissions: cells[5].textContent.trim()
  649. };
  650. }
  651. return null;
  652. }).filter(row => row !== null);
  653. const csvContent = "data:text/csv;charset=utf-8," +
  654. "Name,Size,Type,Modified,Permissions\n" +
  655. rows.map(r => '"' + r.name + '","' + r.size + '","' + r.type + '","' + r.modified + '","' + r.permissions + '"').join("\n");
  656. const encodedUri = encodeURI(csvContent);
  657. const link = document.createElement("a");
  658. link.setAttribute("href", encodedUri);
  659. link.setAttribute("download", "files.csv");
  660. document.body.appendChild(link);
  661. link.click();
  662. document.body.removeChild(link);
  663. }
  664. // Handle file checkbox changes
  665. document.addEventListener('change', function(e) {
  666. if (e.target.classList.contains('file-checkbox')) {
  667. updateDeleteSelectedButton();
  668. }
  669. });
  670. </script>
  671. }
  672. func countDirectories(entries []dash.FileEntry) int {
  673. count := 0
  674. for _, entry := range entries {
  675. if entry.IsDirectory {
  676. count++
  677. }
  678. }
  679. return count
  680. }
  681. func countFiles(entries []dash.FileEntry) int {
  682. count := 0
  683. for _, entry := range entries {
  684. if !entry.IsDirectory {
  685. count++
  686. }
  687. }
  688. return count
  689. }
  690. func getFileIcon(mime string) string {
  691. switch {
  692. case strings.HasPrefix(mime, "image/"):
  693. return "fa-image"
  694. case strings.HasPrefix(mime, "video/"):
  695. return "fa-video"
  696. case strings.HasPrefix(mime, "audio/"):
  697. return "fa-music"
  698. case strings.HasPrefix(mime, "text/"):
  699. return "fa-file-text"
  700. case mime == "application/pdf":
  701. return "fa-file-pdf"
  702. case mime == "application/zip" || strings.Contains(mime, "archive"):
  703. return "fa-file-archive"
  704. case mime == "application/json":
  705. return "fa-file-code"
  706. case strings.Contains(mime, "script") || strings.Contains(mime, "javascript"):
  707. return "fa-file-code"
  708. default:
  709. return "fa-file"
  710. }
  711. }
  712. func getMimeDisplayName(mime string) string {
  713. switch mime {
  714. case "text/plain":
  715. return "Text"
  716. case "text/html":
  717. return "HTML"
  718. case "application/json":
  719. return "JSON"
  720. case "application/pdf":
  721. return "PDF"
  722. case "image/jpeg":
  723. return "JPEG"
  724. case "image/png":
  725. return "PNG"
  726. case "image/gif":
  727. return "GIF"
  728. case "video/mp4":
  729. return "MP4"
  730. case "audio/mpeg":
  731. return "MP3"
  732. case "application/zip":
  733. return "ZIP"
  734. default:
  735. if strings.HasPrefix(mime, "image/") {
  736. return "Image"
  737. } else if strings.HasPrefix(mime, "video/") {
  738. return "Video"
  739. } else if strings.HasPrefix(mime, "audio/") {
  740. return "Audio"
  741. } else if strings.HasPrefix(mime, "text/") {
  742. return "Text"
  743. }
  744. return "File"
  745. }
  746. }