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
HTML
<!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 }))
}