UNPKG

tspace-nfs

Version:

tspace-nfs is a Network File System (NFS) and provides both server and client capabilities for accessing files over a network.

1,150 lines (936 loc) 62.2 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdn.tailwindcss.com/3.4.14"></script> <title>{{title}}</title> {{logo.fav}} <style> .folder-toggle { cursor: pointer; } ul { list-style: none; padding-left: 20px; } svg { width: 16px; height: 16px; } .hidden { display: none; } #loading { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.8); opacity: 0.75; justify-content: center; align-items: center; z-index: 9999; } </style> </head> <body class="bg-gray-300 dark:bg-gray-900"> <nav class="sticky top-0 bg-gray-300 border-gray-200 dark:bg-gray-900"> <div class="w-screen-xl flex items-center justify-between mx-auto p-4"> <div class="cursor-pointer flex items-center space-x-3 rtl:space-x-reverse" onclick="window.location.reload()"> {{logo.index}} <h1 class="self-center text-2xl whitespace-nowrap text-gray-800 dark:text-white"> {{name}} </h1> </div> <div class="flex space-x-3 rtl:space-x-reverse"> <button id="themeToggle" type="button" class="rounded-lg text-sm px-4 py-2 text-center bg-gray-100 dark:bg-gray-800" > <svg id="toggleIcon" class="w-4 h-4 xl:w-6 xl:h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M11.675 2.015a.998.998 0 0 0-.403.011C6.09 2.4 2 6.722 2 12c0 5.523 4.477 10 10 10 4.356 0 8.058-2.784 9.43-6.667a1 1 0 0 0-1.02-1.33c-.08.006-.105.005-.127.005h-.001l-.028-.002A5.227 5.227 0 0 0 20 14a8 8 0 0 1-8-8c0-.952.121-1.752.404-2.558a.996.996 0 0 0 .096-.428V3a1 1 0 0 0-.825-.985Z" clip-rule="evenodd"/> </svg> </button> <button id="logout" class="text-gray-800 dark:text-white rounded-lg text-sm px-4 py-2 text-center text-lg bg-gray-100 dark:bg-gray-800 ml-3"> <svg class="w-4 h-4 xl:w-6 xl:h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H8m12 0-4 4m4-4-4-4M9 4H7a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h2"/> </svg> </button> </div> </div> </nav> <div id="notify-container" class="space-y-4 fixed right-0 top-0 m-2 items-end justify-end z-50 pointer-events-none"></div> <div class="h-[100vh]"> <div class="grid grid-cols-12 h-[75vh] overflow-y-auto"> <div class="col-span-5 xl:col-span-3 bg-white dark:bg-gray-800 shadow-md border-r p-4"> <div class="border-b mb-3"> <div class="flex items-center justify-between w-full text-left "> <h1 class="text-3xl mb-2 dark:text-gray-200">Buckets</h1> <svg id="bucketCreate" class="w-8 h-8 text-gray-800 dark:text-white cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7 7V5"/> </svg> </div> <h3 class="text-sm dark:text-gray-200">total : <span id="buckets"> </span> </h3> <h3 class="text-sm mb-2 dark:text-gray-200">storaged : <span id="storaged"></span></h3> </div> <ul id="bucket-container" class="space-y-2 dark:text-gray-200"></ul> </div> <div class="col-span-7 xl:col-span-9 pl-2 xl:pl-4"> <div id="dropZone" class="h-full bg-white dark:bg-gray-800 hover:border-blue-500 dark:hover:border-blue-400"> <div id="noBucketMessage" class="text-center text-3xl justify-center items-center text-gray-800 dark:text-white pt-[25%] xl:pt-[15%]"> No Buckets available </div> <div id="tableContainer" class="hidden"> <div class="flex flex-wrap items-center justify-between p-4 gap-4"> <div class="flex items-center space-x-3 flex-wrap sm:flex-nowrap"> <svg onclick="onReload(event)" class="w-6 h-6 text-gray-800 dark:text-white cursor-pointer" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.651 7.65a7.131 7.131 0 0 0-12.68 3.15M18.001 4v4h-4m-7.652 8.35a7.13 7.13 0 0 0 12.68-3.15M6 20v-4h4"/> </svg> <span class="font-semibold text-gray-800 dark:text-gray-200">Current Directory:</span> <span class="text-blue-600 px-3 py-1 font-mono w-full sm:w-auto"> <span id="currentPath"></span> </span> </div> <div class="flex flex-wrap items-center gap-2 sm:gap-3 justify-end"> <span class="font-semibold text-gray-800 dark:text-gray-200 w-full sm:w-auto text-center" id="currentFiles"></span> <div class="relative"> <svg id="download" class="cursor-pointer w-6 h-6 text-gray-800 dark:text-white" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M13 11.15V4a1 1 0 1 0-2 0v7.15L8.78 8.374a1 1 0 1 0-1.56 1.25l4 5a1 1 0 0 0 1.56 0l4-5a1 1 0 1 0-1.56-1.25L13 11.15Z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M9.657 15.874 7.358 13H5a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2.358l-2.3 2.874a3 3 0 0 1-4.685 0ZM17 16a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z" clip-rule="evenodd"/> </svg> <span id="download-count" class="absolute -top-1.5 -right-1.5 bg-red-600 text-white text-[12px] rounded-full px-1 py-[1px] leading-none">0</span> </div> <svg id="delete" class="cursor-pointer red-500 w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"/> </svg> </div> </div> <div class="relative max-w-sm mx-4 mb-2"> <input id="searchInput" type="text" placeholder="Search" class="w-full h-8 pl-8 text-gray-900 border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:ring-indigo-500 focus:border-indigo-500" autocomplete="off" /> <div class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400"> <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-4.35-4.35M17 11a6 6 0 11-12 0 6 6 0 0112 0z" /> </svg> </div> </div> <div class="overflow-x-auto"> <table id="dataTable" class="table-auto w-full bg-white dark:bg-gray-800"> <thead> <tr> <th class="cursor-pointer py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400"> <div class="flex items-center space-x-2"> <label class="cursor-pointer"> <input type="checkbox" id="filesSelectdAll" type="checkbox" class="hidden peer" /> <svg class="w-5 h-5 border-2 rounded peer-checked:border-blue-500 peer-checked:bg-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" > <path id="path-checked" class="hidden stroke-current text-white" stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /> </svg> </label> <span onclick="sortTable()"> File Name</span> </div> </th> <th onclick="sortTable()" class="cursor-pointer py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">File Size</th> <th onclick="sortTable()" class="cursor-pointer py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">File Type</th> <th onclick="sortTable()" class="cursor-pointer py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">Last Modified</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">Actions</th> </tr> </thead> <tbody id="fileTableBody" class="text-gray-700 dark:text-gray-300"></tbody> </table> </div> </div> </div> </div> </div> <div class="grid grid-cols-1 w-full h-[50vh] overflow-y-auto border-t"> <div class="mt-2"> <table class="table-auto"> <thead> <tr class="sticky top-0 bg-white dark:bg-gray-800"> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">Directory</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">File Name</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">File Size</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">File Type</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">Last Modified</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">Progress</th> <th class="py-2 px-4 text-left text-md font-medium text-gray-500 dark:text-gray-400">Upload (per/s)</th> </tr> </thead> <tbody id="fileProgressTableBody" class="text-gray-700 dark:text-gray-300"></tbody> </table> </div> </div> </div> <div id="loading" class="flex"> <div class="flex flex-col items-center"> <div class="animate-spin rounded-full h-20 w-20 border-t-4 border-b-4 border-blue-600"></div> <p class="text-gray-800 dark:text-gray-200 text-xl font-semibold mt-4">Loading...</p> </div> </div> <div id="removeModal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm dark:bg-gray-800"> <h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Confirm Remove</h3> <p class="mb-6 text-gray-600 dark:text-gray-300">Are you sure you want to remove this file?</p> <div class="flex justify-end space-x-4"> <button id="removeYesBtnModal" class="px-4 py-2 bg-gray-500 dark:bg-gray-900 text-gray-800 dark:text-white rounded-md">Ok</button> <button id="removeNoBtnModal" class="px-4 py-2 bg-gray-300 dark:bg-gray-700 rounded-md text-gray-800 dark:text-white">Cancel</button> </div> </div> </div> <div id="removesModal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm dark:bg-gray-800"> <h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Confirm Remove</h3> <p class="mb-6 text-gray-600 dark:text-gray-300"> <span id="removesCountModal"> </span> </p> <div class="flex justify-end space-x-4"> <button id="removesYesBtnModal" class="px-4 py-2 bg-gray-500 dark:bg-gray-900 text-gray-800 dark:text-white rounded-md">Ok</button> <button id="removesNoBtnModal" class="px-4 py-2 bg-gray-300 dark:bg-gray-700 rounded-md text-gray-800 dark:text-white">Cancel</button> </div> </div> </div> <div id="editModal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm dark:bg-gray-800"> <h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Confirm Rename</h3> <div class="mb-6"> <label for="rename" class="block text-sm font-medium text-gray-700 dark:text-gray-300">name</label> <input id="rename" name="rename" required class="w-full p-2 mt-1 text-gray-900 border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:ring-indigo-500 focus:border-indigo-500"> </div> <div class="flex justify-end space-x-4"> <button id="editYesBtnModal" class="px-4 py-2 bg-gray-500 dark:bg-gray-900 text-gray-800 dark:text-white rounded-md">Ok</button> <button id="editNoBtnModal" class="px-4 py-2 bg-gray-300 dark:bg-gray-700 rounded-md text-gray-800 dark:text-white">Cancel</button> </div> </div> </div> <div id="bucketModal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm dark:bg-gray-800"> <form id="bucketYesBtnModal" class="space-y-6" novalidate> <h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Create Bucket</h3> <div class="mb-6"> <label for="bucket" class="block text-sm font-medium text-gray-700 dark:text-gray-300">bucket name</label> <input id="bucket" name="bucket" required class="w-full p-2 mt-1 text-gray-900 border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:ring-indigo-500 focus:border-indigo-500"> </div> <div class="mb-6"> <label for="token" class="block text-sm font-medium text-gray-700 dark:text-gray-300">token</label> <input id="token" name="token" required class="w-full p-2 mt-1 text-gray-900 border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:ring-indigo-500 focus:border-indigo-500"> </div> <div class="mb-6"> <label for="secret" class="block text-sm font-medium text-gray-700 dark:text-gray-300">secret</label> <input id="secret" name="secret" required class="w-full p-2 mt-1 text-gray-900 border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:ring-indigo-500 focus:border-indigo-500"> </div> <div class="flex justify-end space-x-4"> <button type="submit" class="px-4 py-2 bg-gray-500 dark:bg-gray-900 text-gray-800 dark:text-white rounded-md">Ok</button> <button id="bucketNoBtnModal" type="button" class="px-4 py-2 bg-gray-300 dark:bg-gray-700 rounded-md text-gray-800 dark:text-white">Cancel</button> </div> <div id="errorMessage" class="text-red-500 text-sm hidden"></div> </form> </div> </div> </body> <script> const $query = (element) => document.querySelector(element); const $$query = (element) => document.querySelectorAll(element); const $state = { data : { url : `${window.location.protocol}//${window.location.host}`, theme : () => localStorage.getItem('theme') == null ? 'dark' : localStorage.getItem('theme'), fileUpdated : {}, path : { selected : localStorage.getItem('lastPathDirectory') ?? '', remove : '', edit : '' }, files : [] }, el : { currentFiles : $query('#currentFiles'), filesSelectdAll : $query('#filesSelectdAll'), filesSelectd : $$query('.filesSelectd'), removeModal : $query('#removeModal'), removeYesBtnModal : $query('#removeYesBtnModal'), removeNoBtnModal : $query('#removeNoBtnModal'), removesModal : $query('#removesModal'), removesYesBtnModal : $query('#removesYesBtnModal'), removesNoBtnModal : $query('#removesNoBtnModal'), removesCountModal : $query('#removesCountModal'), editModal : $query('#editModal'), editYesBtnModal : $query('#editYesBtnModal'), editNoBtnModal : $query('#editNoBtnModal'), bucketModal : $query('#bucketModal'), bucketYesBtnModal : $query('#bucketYesBtnModal'), bucketNoBtnModal : $query('#bucketNoBtnModal'), bucketCreate : $query('#bucketCreate'), rename : $query('#rename'), fileProgressTableBody : $query('#fileProgressTableBody'), fileTableBody : $query('#fileTableBody'), dropZone : $query('#dropZone'), bucketContainer : $query('#bucket-container'), tableContainer : $query('#tableContainer'), noBucketMessage : $query('#noBucketMessage'), loading : $query('#loading'), currentPath : $query('#currentPath'), themeToggle : $query('#themeToggle'), toggleIcon : $query('#toggleIcon'), logout : $query('#logout'), bucket : $query('#bucket'), token : $query('#token'), secret : $query('#secret'), buckets : $query('#buckets'), storaged : $query('#storaged'), notifyContainer : $query('#notify-container'), errorMessage : $query('#errorMessage'), download : $query('#download'), delete : $query('#delete'), downloadCount : $query('#download-count'), bucketToggle : $$query('.bucket-toggle'), folderToggle : $$query('.folder-toggle') } } function showTable() { $state.el.tableContainer.classList.remove('hidden'); $state.el.noBucketMessage.classList.add('hidden'); }; function hideTable() { $state.el.tableContainer.classList.add('hidden'); $state.el.noBucketMessage.classList.remove('hidden'); }; function checkTableStatus() { if ($state.el.fileTableBody.children.length === 0) { hideTable(); } }; function showLoading () { $state.el.loading.style.display = 'flex'; } function hideLoading () { $state.el.loading.style.display = 'none'; } async function sleep(ms = 500) { return await new Promise(r => setTimeout(r, ms)) } function addFolderToTable(folderName) { const rows = Array.from(fileTableBody.children) const find = rows.find(r => { return String(r.cells[0].textContent).trim() === folderName }) if(find) return showTable(); const row = document.createElement('tr'); row.innerHTML += ` <td class="px-4 py-2"> <input type="checkbox" disabled="disabled"> <label for="vehicle1"> ${folderName} </label><br> </td> <td class="px-4 py-2">-</td> <td class="px-4 py-2">-</td> <td class="px-4 py-2">${new Date().toLocaleDateString()}</td> <td class="flex gap-3 px-4 py-2"> <svg target="${$state.data.path.selected}/${folderName}" onclick="onEdit(event)"} class="w-5 h-5 cursor-pointer hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M11.32 6.176H5c-1.105 0-2 .949-2 2.118v10.588C3 20.052 3.895 21 5 21h11c1.105 0 2-.948 2-2.118v-7.75l-3.914 4.144A2.46 2.46 0 0 1 12.81 16l-2.681.568c-1.75.37-3.292-1.263-2.942-3.115l.536-2.839c.097-.512.335-.983.684-1.352l2.914-3.086Z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M19.846 4.318a2.148 2.148 0 0 0-.437-.692 2.014 2.014 0 0 0-.654-.463 1.92 1.92 0 0 0-1.544 0 2.014 2.014 0 0 0-.654.463l-.546.578 2.852 3.02.546-.579a2.14 2.14 0 0 0 .437-.692 2.244 2.244 0 0 0 0-1.635ZM17.45 8.721 14.597 5.7 9.82 10.76a.54.54 0 0 0-.137.27l-.536 2.84c-.07.37.239.696.588.622l2.682-.567a.492.492 0 0 0 .255-.145l4.778-5.06Z" clip-rule="evenodd"/> </svg> <svg target="${$state.data.path.selected}/${folderName}" onclick="onRemove(event)" } class="w-5 h-5 cursor-pointer hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/> </svg> </td> `; $state.el.fileTableBody.appendChild(row); themeToggleMode() }; function addFileToTable(file , checkFile = 0) { const rows = Array.from(fileTableBody.children) if(checkFile) { const find = rows.find(r => { return String(r.cells[0].textContent).trim() === file.name }) if(find) return } showTable(); const row = document.createElement('tr'); row.innerHTML = ` <td class="px-4 py-2"> <div class="space-x-3"> ${file.isFolder ? ` <label class="flex cursor-pointer"> <input type="checkbox" class="filesSelected hidden peer" value="${file.name}" target="${file.path}" onclick="onFileSelected(event)" /> <svg class="w-6 h-6 text-gray-800 dark:text-white peer-checked:border-blue-500 peer-checked:bg-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 8H4m0-2v13a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-5.032a1 1 0 0 1-.768-.36l-1.9-2.28a1 1 0 0 0-.768-.36H5a1 1 0 0 0-1 1Z"/> </svg> <div class="pl-1 cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-500" target="${file.path}" value="${file.name}" onclick="onFileSelected(event)" ondblclick="onLoadFile(event)" > ${file.name} </div> </label> ` : ` <label class="flex cursor-pointer"> <input type="checkbox" class="filesSelected hidden peer" value="${file.name}" target="${file.path}" onclick="onFileSelected(event)" /> <svg class="w-6 h-6 text-gray-800 dark:text-white peer-checked:border-blue-500 peer-checked:bg-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M10 3v4a1 1 0 0 1-1 1H5m14-4v16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7.914a1 1 0 0 1 .293-.707l3.914-3.914A1 1 0 0 1 9.914 3H18a1 1 0 0 1 1 1Z"/> </svg> <div class="pl-1 cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-500" target="${file.path}" value="${file.name}" 4 onclick="onFileSelected(event)" ondblclick="onPreview(event)" > ${file.name} </div> </label> ` } </div> </td> <td class="px-4 py-2">${file.size != null ? formatSize(file.size) : '-'} </td> <td class="px-4 py-2">${file.isFolder ? 'folder' : file.extension === '' || file.extension == null ? 'file' : file.extension}</td> <td class="px-4 py-2">${new Date(file.lastModified).toJSON()}</td> <td class="flex gap-3 px-4 py-2"> <svg target="${file.path}" onclick="${file.isFolder ? 'onLoadFile(event)' : 'onPreview(event)'}"} class="w-5 h-5 cursor-pointer hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M4.998 7.78C6.729 6.345 9.198 5 12 5c2.802 0 5.27 1.345 7.002 2.78a12.713 12.713 0 0 1 2.096 2.183c.253.344.465.682.618.997.14.286.284.658.284 1.04s-.145.754-.284 1.04a6.6 6.6 0 0 1-.618.997 12.712 12.712 0 0 1-2.096 2.183C17.271 17.655 14.802 19 12 19c-2.802 0-5.27-1.345-7.002-2.78a12.712 12.712 0 0 1-2.096-2.183 6.6 6.6 0 0 1-.618-.997C2.144 12.754 2 12.382 2 12s.145-.754.284-1.04c.153-.315.365-.653.618-.997A12.714 12.714 0 0 1 4.998 7.78ZM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" clip-rule="evenodd"/> </svg> ${file.isFolder ?` <svg target="${file.path}" class="cursor-not-allowed hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path d="M17.5 3a3.5 3.5 0 0 0-3.456 4.06L8.143 9.704a3.5 3.5 0 1 0-.01 4.6l5.91 2.65a3.5 3.5 0 1 0 .863-1.805l-5.94-2.662a3.53 3.53 0 0 0 .002-.961l5.948-2.667A3.5 3.5 0 1 0 17.5 3Z"/> </svg> ` :` <svg target="${file.path}" onclick="onShared(event)" class="cursor-pointer hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path d="M17.5 3a3.5 3.5 0 0 0-3.456 4.06L8.143 9.704a3.5 3.5 0 1 0-.01 4.6l5.91 2.65a3.5 3.5 0 1 0 .863-1.805l-5.94-2.662a3.53 3.53 0 0 0 .002-.961l5.948-2.667A3.5 3.5 0 1 0 17.5 3Z"/> </svg> ` } <svg target="${file.path}" onclick="onEdit(event)"} class="w-5 h-5 cursor-pointer hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M11.32 6.176H5c-1.105 0-2 .949-2 2.118v10.588C3 20.052 3.895 21 5 21h11c1.105 0 2-.948 2-2.118v-7.75l-3.914 4.144A2.46 2.46 0 0 1 12.81 16l-2.681.568c-1.75.37-3.292-1.263-2.942-3.115l.536-2.839c.097-.512.335-.983.684-1.352l2.914-3.086Z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M19.846 4.318a2.148 2.148 0 0 0-.437-.692 2.014 2.014 0 0 0-.654-.463 1.92 1.92 0 0 0-1.544 0 2.014 2.014 0 0 0-.654.463l-.546.578 2.852 3.02.546-.579a2.14 2.14 0 0 0 .437-.692 2.244 2.244 0 0 0 0-1.635ZM17.45 8.721 14.597 5.7 9.82 10.76a.54.54 0 0 0-.137.27l-.536 2.84c-.07.37.239.696.588.622l2.682-.567a.492.492 0 0 0 .255-.145l4.778-5.06Z" clip-rule="evenodd"/> </svg> <svg target="${file.path}" onclick="onRemove(event)" } class="w-5 h-5 cursor-pointer hover:scale-150 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M8.586 2.586A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4a2 2 0 0 1 .586-1.414ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/> </svg> </td> `; $state.el.fileTableBody.appendChild(row); }; async function sortTable() { showLoading(); const table = document.getElementById('dataTable'); const rows = Array.from(table.querySelectorAll('tbody tr')); const isAscending = table.getAttribute('data-sort-order') === 'asc'; table.setAttribute('data-sort-order', isAscending ? 'desc' : 'asc'); rows.sort((a, b) => { const aText = a.cells[0].innerText.trim(); const bText = b.cells[0].innerText.trim(); if (!isNaN(aText) && !isNaN(bText)) { return isAscending ? aText - bText : bText - aText; } return isAscending ? aText.localeCompare(bText) : bText.localeCompare(aText); }); const tbody = table.querySelector('tbody'); tbody.innerHTML = ''; rows.forEach(row => tbody.appendChild(row)); await sleep(200); hideLoading(); } function updateFileProgress({ file, progress, uploadSpeed }) { const rows = Array.from(fileProgressTableBody.children); const row = rows.find(r => { return String(r.cells[0].textContent).trim() === file.directory && String(r.cells[1].textContent).trim() === file.name; }) if(row) { row.cells[5].innerHTML = `<progress value="${progress}" max="100"></progress> ${progress}%`; row.cells[6].innerHTML = `${formatSize(uploadSpeed)}/s`; return } const tr = document.createElement('tr'); tr.innerHTML = ` <td class="px-4 py-2"> <button class="text-blue-500" type="button" target="${file.directory}" onclick="onLoadFile(event)"> ${file.directory} </button> </td> <td class="px-4 py-2"> <button class="cursor-pointer" type="button" target="${file.path}" onclick="onPreview(event)"> ${file.name} </button> </td> <td class="px-4 py-2">${file.size != null ? formatSize(file.size) : '-'} </td> <td class="px-4 py-2">${file.extension}</td> <td class="px-4 py-2">${new Date(file.lastModified).toJSON()}</td> <td class="px-4 py-2"> <progress value="${progress}" max="100"></progress> ${progress}% </td> <td class="px-4 py-2"> ${formatSize(uploadSpeed)}/s </td> `; $state.el.fileProgressTableBody.appendChild(tr); }; async function fetchDownloadFiles() { try { const files = $state.data.files.filter(v => v.selected) if(!files.length) { notify({ type : 'error' , message : 'Please specify which files to download.'}) return } showLoading(); const response = await fetch(`${$state.data.url}/studio/api/download`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ files }) }) if (!response.ok) { throw new Error('Network response was not ok'); } const contentDisposition = response.headers.get('Content-Disposition'); let filename = 'downloaded.zip'; if (contentDisposition && contentDisposition.includes('filename=')) { const match = contentDisposition.match(/filename="(.+?)"/); if (match && match[1]) { filename = match[1]; } } const blob = await response.blob(); await sleep(300) hideLoading() const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (err) { console.log(err) hideLoading() notify({ type : 'error' , message : err.message }) } } async function fetchUploadFile(file, path) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", `${$state.data.url}/studio/api/upload`, true); let lastTime = Date.now(); let lastLoaded = 0; xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const progress = ((e.loaded / e.total) * 100).toFixed(2); const currentTime = Date.now(); const elapsedTime = (currentTime - lastTime) / 1000; const bytesUploaded = e.loaded - lastLoaded; const uploadSpeed = (bytesUploaded) / elapsedTime; updateFileProgress({ file : { name: file.name, size: file.size, extension: String(file.name)?.split('.').pop(), lastModified: new Date().toJSON(), directory: path.replace(`/${file.name}`, ''), path : [path , file.name].join('/') }, progress : +progress, uploadSpeed: uploadSpeed.toFixed(4) }); lastTime = currentTime; lastLoaded = e.loaded; } }; xhr.onload = async () => { if (xhr.status === 200) { notify({ type: 'success', message: `The file '${file.name}' has been uploaded.` }); return resolve(true); } return reject(new Error(`Upload failed with status ${xhr.status}`)); }; xhr.onerror = () => { notify({ type: 'error', message: `The file '${file.name}' has failed to upload.` }); reject(new Error(`Network error during upload`)); }; const formData = new FormData(); formData.append("file", file); formData.append('path', path); xhr.send(formData); }); } function setTableFiles () { $state.el.fileTableBody.innerHTML = '' } async function fetchFiles(target) { if(target == null || target === '') return showLoading(); const path = normalizePath(target) const response = await fetch(`${$state.data.url}/studio/api/files/${path}`); if (!response.ok) { await sleep(300); showTable(); hideLoading(); localStorage.removeItem('lastPathDirectory') throw new Error('Network response was not ok'); } const res = await response.json(); const files = res.files; $state.data.files = files.map(file => ({...file , selected : false })) setTableFiles() await sleep(300) showTable(); for(const file of files) { addFileToTable(file); } themeToggleMode() currentTotalFiles() hideLoading() let link = [] let currentTarget = [] for(const t of target.split('/')) { currentTarget.push(t); link.push(`<button class="text-blue-500" target="${currentTarget.join('/')}" onclick="onLoadFile(event)"> ${t} </button>`); } $state.el.currentPath.innerHTML = link.join(' / '); $state.data.path.selected = target; localStorage.setItem('lastPathDirectory', target) } async function fetchBucket() { try { showLoading(); const response = await fetch(`${$state.data.url}/studio/api/buckets`); if (!response.ok) { throw new Error('Network response was not ok'); } const folderStructure = await response.json(); await sleep(300) hideLoading() $state.el.bucketContainer.innerHTML = generateBucketHTML(folderStructure.buckets); addToggleFunctionality() } catch (error) { hideLoading() notify({ type : 'error' , message :error.message }) } } async function fetchStorage() { try { showLoading(); const response = await fetch(`${$state.data.url}/studio/api/storage`); if (!response.ok) { throw new Error('Network response was not ok'); } const res = await response.json(); await sleep(300) hideLoading() $state.el.buckets.textContent = `${res.buckets ?? 0} bucket(s)` $state.el.storaged.textContent = (res.storage?.mb ?? 0) > 1024 ? `${res.storage?.gb} GB` : `${res.storage?.mb} MB` } catch (error) { hideLoading() notify({ type : 'error' , message :error.message }) } } async function fetchRemove(target) { const path = normalizePath(target) showLoading(); const response = await fetch(`${$state.data.url}/studio/api/files/${path}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Network response was not ok'); } return response } async function fetctEdit () { const path = normalizePath($state.data.path.edit) const response = await fetch(`${$state.data.url}/studio/api/files/${path}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ rename : $state.el.rename.value }), }); if (!response.ok) { throw new Error('Network response was not ok'); } } async function onShared (e) { try { let path = normalizePath(e.currentTarget.getAttribute('target')) if(path == '' || path == null) { path = $state.data.path.selected } const response = await fetch(`${$state.data.url}/studio/api/shared/${path}`); if (!response.ok) { throw new Error('Network response was not ok'); } const res = await response.json() navigator.clipboard.writeText(`${$state.data.url}/${res.path}`) .then(() => { notify({ type : 'success', message : `Copied the URL successfully!` }) }) .catch(err => { notify({ type : 'error' , message : 'Failed to copy text' }) }); } catch (error) { await notify({ type : 'error' , message : error.message }) } } async function onLoadFile (e) { try { let path = normalizePath(e.currentTarget.getAttribute('target')) if(path == '' || path == null) { path = $state.data.path.selected } await fetchFiles(path) await notify({ type : 'success', message : `The directory '${path}' has been loaded!` }) } catch (error) { await notify({ type : 'error' , message : error.message }) } } async function onReload (e) { try { let path = $state.data.path.selected await fetchFiles(path) await notify({ type : 'success', message : `The directory '${path}' has been loaded!` }) } catch (error) { await notify({ type : 'error' , message : error.message }) } } function onFileSelected(e) { const checked = e.target.checked const path = normalizePath(e.currentTarget.getAttribute('target')); $state.data.files = $state.data.files.map(file => { if(normalizePath(file.path) === path) { return {...file , selected : checked } } return file }) currentTotalFiles({ selected : true }) } function onPreview(e) { const path = normalizePath(e.currentTarget.getAttribute('target')); window.open(`${$state.data.url}/studio/preview/${path}`, '_blank'); } function onEdit(e) { const path = normalizePath(e.currentTarget.getAttribute('target')); $state.el.editModal.classList.remove('hidden'); $state.data.path.edit = path; } function onRemove(e) { const path = normalizePath(e.currentTarget.getAttribute('target')); $state.el.removeModal.classList.remove('hidden'); $state.data.path.remove = path; }; function generateBucketHTML(folderStructure) { let html = ''; folderStructure.forEach((dev , index) => { Object.keys(dev).forEach(key => { const credentials = dev[key].credentials ?? null const storage = dev[key].storage ?? 0 html += ` <li> <div class="grid grid-cols-12"> <div class="col-span-2 xl:col-span-1"> <button class="credentials"> <svg class="w-4 h-4 xl:w-6 xl:h-6 mr-2 cursor-copy text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path fill="currentColor" d="M6.94318 11h-.85227l.96023-2.90909h1.07954L9.09091 11h-.85227l-.63637-2.10795h-.02272L6.94318 11Zm-.15909-1.14773h1.60227v.59093H6.78409v-.59093ZM9.37109 11V8.09091h1.25571c.2159 0 .4048.04261.5667.12784.162.08523.2879.20502.3779.35937.0899.15436.1349.33476.1349.5412 0 .20833-.0464.38873-.1392.54119-.0918.15246-.2211.26989-.3878.35229-.1657.0824-.3593.1236-.5809.1236h-.75003v-.61367h.59093c.0928 0 .1719-.0161.2372-.0483.0663-.03314.1169-.08002.152-.14062.036-.06061.054-.13211.054-.21449 0-.08334-.018-.15436-.054-.21307-.0351-.05966-.0857-.10511-.152-.13636-.0653-.0322-.1444-.0483-.2372-.0483h-.2784V11h-.78981Zm3.41481-2.90909V11h-.7898V8.09091h.7898Z"/> <path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M8.31818 2c-.55228 0-1 .44772-1 1v.72878c-.06079.0236-.12113.04809-.18098.07346l-.55228-.53789c-.38828-.37817-1.00715-.37817-1.39543 0L3.30923 5.09564c-.19327.18824-.30229.44659-.30229.71638 0 .26979.10902.52813.30229.71637l.52844.51468c-.01982.04526-.03911.0908-.05785.13662H3c-.55228 0-1 .44771-1 1v2.58981c0 .5523.44772 1 1 1h.77982c.01873.0458.03802.0914.05783.1366l-.52847.5147c-.19327.1883-.30228.4466-.30228.7164 0 .2698.10901.5281.30228.7164l1.88026 1.8313c.38828.3781 1.00715.3781 1.39544 0l.55228-.5379c.05987.0253.12021.0498.18102.0734v.7288c0 .5523.44772 1 1 1h2.65912c.5523 0 1-.4477 1-1v-.7288c.1316-.0511.2612-.1064.3883-.1657l.5435.2614v.4339c0 .5523.4477 1 1 1H14v.0625c0 .5523.4477 1 1 1h.0909v.0625c0 .5523.4477 1 1 1h.6844l.4952.4823c1.1648 1.1345 3.0214 1.1345 4.1863 0l.2409-.2347c.1961-.191.3053-.454.3022-.7277-.0031-.2737-.1183-.5342-.3187-.7207l-6.2162-5.7847c.0173-.0398.0342-.0798.0506-.12h.7799c.5522 0 1-.4477 1-1V8.17969c0-.55229-.4478-1-1-1h-.7799c-.0187-.04583-.038-.09139-.0578-.13666l.5284-.51464c.1933-.18824.3023-.44659.3023-.71638 0-.26979-.109-.52813-.3023-.71637l-1.8803-1.8313c-.3883-.37816-1.0071-.37816-1.3954 0l-.5523.53788c-.0598-.02536-.1201-.04985-.1809-.07344V3c0-.55228-.4477-1-1-1H8.31818Z"/> </svg> <p class="hidden">${key}</p> <span class="hidden"> ${ credentials == null ? '' : ` {bucket} : ${credentials.bucket} {token} : ${credentials.token} {secret} : ${credentials.secret} ` } </span> </button> </div> <div class="col-span-10 xl:col-span-11"> <button class="flex items-center justify-between w-full text-left bucket-toggle"> <h2 class="text-xl" target="${key}">${key}</h2> <span class="text-xs xl:text-sm"> ${(storage?.mb ?? 0) > 1024 ? `${storage?.gb} GB` : `${storage?.mb} MB`} </span> </button> </div> </div> </li>`; }); }); return html; } function generateFoldersAndFilesHTML(folders) { let html = ''; folders.forEach(item => { if (item.isFolder) { const hasSubfolders = item.folders && item.folders.length > 0; html += ` <li> <button class="flex items-center justify-between w-full text-left folder-toggle"> <div class="text-md" target="${item.path}">${item.name}</div> ${hasSubfolders ? ` <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16"> <path d="M8 12.5l-5-5h10l-5 5z" class="arrow-down" /> </svg>` : ` <span></span>`} </button> <ul class="ml-4 mt-2 ${hasSubfolders ? 'hidden' : ''}"> ${generateFoldersAndFilesHTML(item.folders || [])} </ul> </li>`; } }); return html; } function addToggleFunctionality() { $$query('.bucket-toggle').forEach(button => { button.addEventListener('click', async function(e) { const target = e.target.getAttribute('target'); if(target === '' || target == null || target === 'null' || target === 'undefined') { return } await fetchFiles(target) await notify({ type : 'success', message : `The directory '${target}' has been loaded!` }) }) }) $$query('.folder-toggle').forEach(button => { button.addEventListener('click', async function(e) { const target = e.target.getAttribute('target'); if(target === '' || target == null || target === 'null' || target === 'undefined') { return } await fetchFiles(target); await notify({ type : 'success', message : `The directory '${target}' has been loaded!` }) const sublist = this.nextElementSibling; sublist.classList.toggle('hidden'); const arrowIcon = this.querySelector('.arrow-icon path'); if(arrowIcon) { if (sublist.classList.contains('hidden')) { arrowIcon.setAttribute('d', 'M8 12.5l-5-5h10l-5 5z') } else { arrowIcon.setAttribute('d', 'M8 3.5l5 5H3l5-5z') } } }) }) $$query('.credentials').forEach(button => { button.addEventListener('click', async function(e) { const bucket = e.currentTarget.querySelector('p') const credentials = e.currentTarget.querySelector('span') navigator.clipboard.writeText(credentials.textContent).then(() => { notify({ type : 'success', message : `The '${bucket.textContent}'' credentials copied to clipboard!` }) }).catch(err => { notify({ type : 'error', message : `Failed to copy text: '${err.message}'` }) }); }) }) } function handleDropItems(items) { const results = []; const promises = []; for (let i = 0; i < items.length; i++) { const item = items[i].webkitGetAsEntry(); if (item) { promises.push(traverseFileTree(item, results, '')) } } return Promise.all(promises).then(() => results); } function traverseFileTree(item, results, currentDirName) { return new Promise(resolve => { if (item.isFile) { item.file(file => { results.push({ file, directory: currentDirName }) resolve(); }); } else if (item.isDirectory) { const dirName = item.name; traverseDirectory(item, results, `${currentDirName}/${dirName}`).then(resolve); } else { resolve(); } }) } function traverseDirectory(directory, results, parentDirName) { return new Promise(resolve => { const dirReader = directory.createReader(); dirReader.readEntries(entries => { const promises = entries.map(entry => { if (entry.isFile) { return new Promise(fileResolve => { entry.file(file => { results.push({ file, directory: parentDirName }) fileResolve(); }); }); } else if (entry.isDirectory) { return traverseDirectory(entry, results, `${parentDirName}/${entry.name}`) } }); Promise.all(promises).then(resolve); }, error => { resolve(); }); }); } function formatSize (size) { const units = ["KB", "MB", "GB"]; let unitIndex = 0; size = Number.isNaN(Number(size)) ? 0 : Number(size) / 1024 while (size >= 1024 && unitIndex < units.length - 1) { size = size / 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; }; function currentTotalFiles ({ selected = false, forceAll = false , unForceAll = false } = {}) { if(forceAll) { $state.data.files = $state.data.files.map(file => ({...file, selected : true })) }