From 7538b6a88405c5903e14f7fa7832c560f39c7fe5 Mon Sep 17 00:00:00 2001 From: Nikhil M Warrier Date: Sat, 5 Sep 2026 20:03:28 +0530 Subject: Initial Commit Add LICENSE Ignore .DS_Store --- assets/index.js | 1020 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1020 insertions(+) create mode 100644 assets/index.js (limited to 'assets/index.js') diff --git a/assets/index.js b/assets/index.js new file mode 100644 index 0000000..f95bd6e --- /dev/null +++ b/assets/index.js @@ -0,0 +1,1020 @@ +/** + * @typedef {object} PathItem + * @property {"Dir"|"SymlinkDir"|"File"|"SymlinkFile"} path_type + * @property {string} name + * @property {number} mtime + * @property {number} size + */ + +/** + * @typedef {object} DATA + * @property {string} href + * @property {string} uri_prefix + * @property {"Index" | "Edit" | "View"} kind + * @property {PathItem[]} paths + * @property {boolean} allow_upload + * @property {boolean} allow_delete + * @property {boolean} allow_search + * @property {boolean} allow_archive + * @property {boolean} auth + * @property {string} user + * @property {boolean} dir_exists + * @property {string} editable + */ + +var DUFS_MAX_UPLOADINGS = 1; + +/** + * @type {DATA} DATA + */ +var DATA; + +/** + * @type {string} + */ +var DIR_EMPTY_NOTE; + +/** + * @type {PARAMS} + * @typedef {object} PARAMS + * @property {string} q + * @property {string} sort + * @property {string} order + */ +const PARAMS = Object.fromEntries(new URLSearchParams(window.location.search).entries()); + +const IFRAME_FORMATS = [ + ".pdf", + ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", + ".mp4", ".mov", ".avi", ".wmv", ".flv", ".webm", + ".mp3", ".ogg", ".wav", ".m4a", +]; + +const MAX_SUBPATHS_COUNT = 1000; + +const ICONS = { + dir: ``, + symlinkFile: ``, + symlinkDir: ``, + file: ``, + download: ``, + move: ``, + edit: ``, + delete: ``, + view: ``, +} + +/** + * @type Map + */ +const failUploaders = new Map(); + +/** + * @type Element + */ +let $pathsTable; +/** + * @type Element + */ +let $pathsTableHead; +/** + * @type Element + */ +let $pathsTableBody; +/** + * @type Element + */ +let $uploadersTable; +/** + * @type Element + */ +let $emptyFolder; +/** + * @type Element + */ +let $editor; +/** + * @type Element + */ +let $loginBtn; +/** + * @type Element + */ +let $logoutBtn; +/** + * @type Element + */ +let $userName; + +// manage unload event to prevent leaving with uploads in progress +const beforeUnloadHandler = (event) => { + if (Uploader.queues.length > 0 || Uploader.runnings > 0) { + event.preventDefault(); + event.returnValue = ''; + return ''; // for some browsers + } +}; + +// Produce table when window loads +window.addEventListener("DOMContentLoaded", async () => { + const $indexData = document.getElementById('index-data'); + if (!$indexData) { + alert("No data"); + return; + } + + DATA = JSON.parse(decodeBase64($indexData.innerHTML)); + DIR_EMPTY_NOTE = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded'; + + await ready(); +}); + +async function ready() { + $pathsTable = document.querySelector(".paths-table"); + $pathsTableHead = document.querySelector(".paths-header"); + $pathsTableBody = document.querySelector(".paths-body"); + $uploadersTable = document.querySelector(".uploaders-table"); + $emptyFolder = document.querySelector(".empty-folder"); + $editor = document.querySelector(".editor"); + $loginBtn = document.querySelector(".login-btn"); + $logoutBtn = document.querySelector(".logout-btn"); + $userName = document.querySelector(".user-name"); + + window.addEventListener('beforeunload', beforeUnloadHandler); + + document.addEventListener('click', e => { + if (e.target.closest('.menu, .cell-more')) return; + document.querySelectorAll('.menu-toggle, .more-toggle').forEach(cb => cb.checked = false); + }); + + addBreadcrumb(DATA.href, DATA.uri_prefix); + + if (DATA.kind === "Index") { + document.title = `Index of ${DATA.href} - Dufs`; + document.querySelector(".index-page").classList.remove("hidden"); + + await setupIndexPage(); + } else if (DATA.kind === "Edit") { + document.title = `Edit ${DATA.href} - Dufs`; + document.querySelector(".editor-page").classList.remove("hidden"); + + await setupEditorPage(); + } else if (DATA.kind === "View") { + document.title = `View ${DATA.href} - Dufs`; + document.querySelector(".editor-page").classList.remove("hidden"); + + await setupEditorPage(); + } +} + +class Uploader { + /** + * + * @param {File} file + * @param {string[]} pathParts + */ + constructor(file, pathParts) { + /** + * @type Element + */ + this.$uploadStatus = null + this.uploaded = 0; + this.uploadOffset = 0; + this.lastUptime = 0; + this.name = [...pathParts, file.name].join("/"); + this.idx = Uploader.globalIdx++; + this.file = file; + this.url = newUrl(this.name); + } + + upload() { + const { idx, name, url } = this; + const encodedName = encodedStr(name); + $uploadersTable.insertAdjacentHTML("beforeend", ` + + + ${getPathSvg()} + + + ${encodedName} + + + `); + $uploadersTable.classList.remove("hidden"); + $emptyFolder.classList.add("hidden"); + this.$uploadStatus = document.getElementById(`uploadStatus${idx}`); + this.$uploadStatus.innerHTML = '-'; + this.$uploadStatus.addEventListener("click", e => { + const nodeId = e.target.id; + const matches = /^retry(\d+)$/.exec(nodeId); + if (matches) { + const id = parseInt(matches[1]); + let uploader = failUploaders.get(id); + if (uploader) uploader.retry(); + } + }); + Uploader.queues.push(this); + Uploader.runQueue(); + } + + ajax() { + const { url } = this; + + this.uploaded = 0; + this.lastUptime = Date.now(); + + const ajax = new XMLHttpRequest(); + ajax.upload.addEventListener("progress", e => this.progress(e), false); + ajax.addEventListener("readystatechange", () => { + if (ajax.readyState === 4) { + if (ajax.status >= 200 && ajax.status < 300) { + this.complete(); + } else { + if (ajax.status != 0) { + this.fail(`${ajax.status} ${ajax.statusText}`); + } + } + } + }) + ajax.addEventListener("error", () => this.fail(), false); + ajax.addEventListener("abort", () => this.fail(), false); + if (this.uploadOffset > 0) { + ajax.open("PATCH", url); + ajax.setRequestHeader("X-Update-Range", "append"); + ajax.send(this.file.slice(this.uploadOffset)); + } else { + ajax.open("PUT", url); + ajax.send(this.file); + // setTimeout(() => ajax.abort(), 3000); + } + } + + async retry() { + const { url } = this; + let res = await fetch(url, { + method: "HEAD", + }); + let uploadOffset = 0; + if (res.status == 200) { + let value = res.headers.get("content-length"); + uploadOffset = parseInt(value) || 0; + } + this.uploadOffset = uploadOffset; + this.ajax(); + } + + progress(event) { + const now = Date.now(); + const elapsed = now - this.lastUptime; + if (elapsed < 300) return; // throttle update for safari + const speed = (event.loaded - this.uploaded) / elapsed * 1000; + const [speedValue, speedUnit] = formatFileSize(speed); + const speedText = `${speedValue} ${speedUnit}/s`; + const progress = formatPercent(((event.loaded + this.uploadOffset) / this.file.size) * 100); + const duration = formatDuration((event.total - event.loaded) / speed); + this.$uploadStatus.innerHTML = `${speedText}${progress} ${duration}`; + this.uploaded = event.loaded; + this.lastUptime = now; + } + + complete() { + const $uploadStatusNew = this.$uploadStatus.cloneNode(true); + $uploadStatusNew.innerHTML = `✓`; + this.$uploadStatus.parentNode.replaceChild($uploadStatusNew, this.$uploadStatus); + this.$uploadStatus = null; + failUploaders.delete(this.idx); + Uploader.runnings--; + Uploader.runQueue(); + } + + fail(reason = "") { + this.$uploadStatus.innerHTML = `✗↻`; + failUploaders.set(this.idx, this); + Uploader.runnings--; + Uploader.runQueue(); + } +} + +Uploader.globalIdx = 0; + +Uploader.runnings = 0; + +Uploader.auth = false; + +/** + * @type Uploader[] + */ +Uploader.queues = []; + + +Uploader.runQueue = async () => { + if (Uploader.runnings >= DUFS_MAX_UPLOADINGS) return; + if (Uploader.queues.length == 0) return; + Uploader.runnings++; + let uploader = Uploader.queues.shift(); + if (!Uploader.auth) { + Uploader.auth = true; + try { + await checkAuth(); + } catch { + Uploader.auth = false; + } + } + uploader.ajax(); +} + +/** + * Add breadcrumb + * @param {string} href + * @param {string} uri_prefix + */ +function addBreadcrumb(href, uri_prefix) { + const $breadcrumb = document.querySelector(".breadcrumb"); + let parts = []; + if (href === "/") { + parts = [""]; + } else { + parts = href.split("/"); + } + const len = parts.length; + let path = uri_prefix; + for (let i = 0; i < len; i++) { + const name = parts[i]; + if (i > 0) { + if (!path.endsWith("/")) { + path += "/"; + } + path += encodeURIComponent(name); + } + const encodedName = encodedStr(name); + if (i === 0) { + $breadcrumb.insertAdjacentHTML("beforeend", ``); + } else if (i === len - 1) { + $breadcrumb.insertAdjacentHTML("beforeend", `${encodedName}`); + } else { + $breadcrumb.insertAdjacentHTML("beforeend", `${encodedName}`); + } + if (i !== len - 1) { + $breadcrumb.insertAdjacentHTML("beforeend", `/`); + } + } +} + +async function setupIndexPage() { + if (DATA.allow_archive) { + const $download = document.querySelector(".download"); + $download.href = baseUrl() + "?zip"; + $download.title = "Download folder as a .zip file"; + $download.classList.add("dlwt"); + $download.classList.remove("hidden"); + } + + if (DATA.allow_upload) { + setupDropzone(); + setupUploadFile(); + setupNewFolder(); + setupNewFile(); + } + + if (DATA.auth) { + await setupAuth(); + } + + if (DATA.allow_search) { + setupSearch(); + } + + renderPathsTableHead(); + renderPathsTableBody(); + + if (DATA.user) { + setupDownloadWithToken(); + } +} + +/** + * Render path table thead + */ +function renderPathsTableHead() { + const headerItems = [ + { + name: "name", + cls: "h-name", + text: "Name", + }, + { + name: "mtime", + cls: "h-mtime", + text: "Last Modified", + }, + { + name: "size", + cls: "h-size", + text: "Size", + } + ]; + $pathsTableHead.insertAdjacentHTML("beforeend", ` +
+ + ${headerItems.map(item => { + let svg = ``; + let order = "desc"; + if (PARAMS.sort === item.name) { + if (PARAMS.order === "desc") { + order = "asc"; + svg = `` + } else { + svg = `` + } + } + const qs = new URLSearchParams({ ...PARAMS, order, sort: item.name }).toString(); + const icon = `${svg}` + return `${item.text}${icon}` + }).join("\n")} + Actions +
+ `); +} + +/** + * Render path table tbody + */ +function renderPathsTableBody() { + if (DATA.paths && DATA.paths.length > 0) { + const len = DATA.paths.length; + if (len > 0) { + $pathsTable.classList.remove("hidden"); + } + for (let i = 0; i < len; i++) { + addPath(DATA.paths[i], i); + } + } else { + $emptyFolder.textContent = DIR_EMPTY_NOTE; + $emptyFolder.classList.remove("hidden"); + } +} + +/** + * Add pathitem + * @param {PathItem} file + * @param {number} index + */ +function addPath(file, index) { + const encodedName = encodedStr(file.name); + let url = newUrl(file.name); + let actionDelete = ""; + let actionDownload = ""; + let actionMove = ""; + let actionEdit = ""; + let actionView = ""; + let isDir = file.path_type.endsWith("Dir"); + if (isDir) { + url += "/"; + if (DATA.allow_archive) { + actionDownload = ` + `; + } + } else { + actionDownload = ` + `; + } + if (DATA.allow_delete) { + if (DATA.allow_upload) { + actionMove = `
${ICONS.move}
`; + if (!isDir) { + actionEdit = `${ICONS.edit}`; + } + } + actionDelete = ` +
${ICONS.delete}
`; + } + if (!actionEdit && !isDir) { + actionView = `${ICONS.view}`; + } + let actionBtns = ` + ${actionDownload} + ${actionView} + ${actionMove} + ${actionDelete} + ${actionEdit}`; + + const mtimeText = formatMtime(file.mtime); + const sizeText = isDir ? formatDirSize(file.size) : formatFileSize(file.size).join(" "); + + $pathsTableBody.insertAdjacentHTML("beforeend", ` +
+ + ${getPathSvg(file.path_type)} + + + ${encodedName} + + ${mtimeText} + ${sizeText} + ${actionBtns} + + + +
+
Modified${mtimeText}
+
Size${sizeText}
+
${actionBtns}
+
+
+
`); +} + +function setupDropzone() { + ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => { + document.addEventListener(name, e => { + e.preventDefault(); + e.stopPropagation(); + }); + }); + document.addEventListener("drop", async e => { + if (!e.dataTransfer.items[0].webkitGetAsEntry) { + const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0); + for (const file of files) { + new Uploader(file, []).upload(); + } + } else { + const entries = []; + const len = e.dataTransfer.items.length; + for (let i = 0; i < len; i++) { + entries.push(e.dataTransfer.items[i].webkitGetAsEntry()); + } + addFileEntries(entries, []); + } + }); +} + +async function setupAuth() { + if (DATA.user) { + $logoutBtn.classList.remove("hidden"); + $logoutBtn.addEventListener("click", logout); + $userName.textContent = DATA.user; + } else { + $loginBtn.classList.remove("hidden"); + $loginBtn.addEventListener("click", async () => { + try { + await checkAuth("login"); + } catch { } + location.reload(); + }); + } +} + +function setupDownloadWithToken() { + document.querySelectorAll("a.dlwt").forEach(link => { + link.addEventListener("click", async e => { + e.preventDefault(); + try { + const link = e.currentTarget || e.target; + const originalHref = link.getAttribute("href"); + const tokengenUrl = new URL(originalHref); + tokengenUrl.searchParams.set("tokengen", ""); + const res = await fetch(tokengenUrl); + if (!res.ok) throw new Error("Failed to fetch token"); + const token = await res.text(); + const downloadUrl = new URL(originalHref); + downloadUrl.searchParams.set("token", token); + const tempA = document.createElement("a"); + tempA.href = downloadUrl.toString(); + tempA.download = ""; + document.body.appendChild(tempA); + tempA.click(); + document.body.removeChild(tempA); + } catch (err) { + alert(`Failed to download, ${err.message}`); + } + }); + }); +} + +function setupSearch() { + const $searchbar = document.querySelector(".searchbar"); + $searchbar.classList.remove("hidden"); + $searchbar.addEventListener("submit", event => { + event.preventDefault(); + const formData = new FormData($searchbar); + const q = formData.get("q"); + let href = baseUrl(); + if (q) { + href += "?q=" + q; + } + location.href = href; + }); + if (PARAMS.q) { + document.getElementById('search').value = PARAMS.q; + } +} + +function setupUploadFile() { + document.querySelector(".upload-file").classList.remove("hidden"); + document.getElementById("file").addEventListener("change", async e => { + const files = e.target.files; + for (let file of files) { + new Uploader(file, []).upload(); + } + }); +} + +function setupNewFolder() { + const $newFolder = document.querySelector(".new-folder"); + $newFolder.classList.remove("hidden"); + $newFolder.addEventListener("click", () => { + const name = prompt("Enter folder name"); + if (name) createFolder(name); + }); +} + +function setupNewFile() { + const $newFile = document.querySelector(".new-file"); + $newFile.classList.remove("hidden"); + $newFile.addEventListener("click", () => { + const name = prompt("Enter file name"); + if (name) createFile(name); + }); +} + +async function setupEditorPage() { + const url = baseUrl(); + + const $download = document.querySelector(".download"); + $download.classList.remove("hidden"); + $download.href = url; + + if (DATA.kind == "Edit") { + const $moveFile = document.querySelector(".move-file"); + $moveFile.classList.remove("hidden"); + $moveFile.addEventListener("click", async () => { + const query = location.href.slice(url.length); + const newFileUrl = await doMovePath(url); + if (newFileUrl) { + location.href = newFileUrl + query; + } + }); + + const $deleteFile = document.querySelector(".delete-file"); + $deleteFile.classList.remove("hidden"); + $deleteFile.addEventListener("click", async () => { + const url = baseUrl(); + const name = baseName(url); + await doDeletePath(name, url, () => { + location.href = location.href.split("/").slice(0, -1).join("/"); + }); + }); + + if (DATA.editable) { + const $saveBtn = document.querySelector(".save-btn"); + $saveBtn.classList.remove("hidden"); + $saveBtn.addEventListener("click", saveChange); + } + } else if (DATA.kind == "View") { + $editor.readonly = true; + } + + if (!DATA.editable) { + const $notEditable = document.querySelector(".not-editable"); + const url = baseUrl(); + const ext = extName(baseName(url)); + if (IFRAME_FORMATS.find(v => v === ext)) { + $notEditable.insertAdjacentHTML("afterend", ``); + } else { + $notEditable.classList.remove("hidden"); + $notEditable.textContent = "Cannot edit because file is too large or binary."; + } + return; + } + + $editor.classList.remove("hidden"); + try { + const res = await fetch(baseUrl()); + await assertResOK(res); + const encoding = getEncoding(res.headers.get("content-type")); + if (encoding === "utf-8") { + $editor.value = await res.text(); + } else { + const bytes = await res.arrayBuffer(); + const dataView = new DataView(bytes); + const decoder = new TextDecoder(encoding); + $editor.value = decoder.decode(dataView); + } + } catch (err) { + alert(`Failed to get file, ${err.message}`); + } +} + +/** + * Delete path + * @param {number} index + * @returns + */ +async function deletePath(index) { + const file = DATA.paths[index]; + if (!file) return; + await doDeletePath(file.name, newUrl(file.name), () => { + document.getElementById(`addPath${index}`)?.remove(); + DATA.paths[index] = null; + if (!DATA.paths.find(v => !!v)) { + $pathsTable.classList.add("hidden"); + $emptyFolder.textContent = DIR_EMPTY_NOTE; + $emptyFolder.classList.remove("hidden"); + } + }); +} + +async function doDeletePath(name, url, cb) { + if (!confirm(`Delete \`${name}\`?`)) return; + try { + await checkAuth(); + const res = await fetch(url, { + method: "DELETE", + }); + await assertResOK(res); + cb(); + } catch (err) { + alert(`Cannot delete \`${file.name}\`, ${err.message}`); + } +} + +/** + * Move path + * @param {number} index + * @returns + */ +async function movePath(index) { + const file = DATA.paths[index]; + if (!file) return; + const fileUrl = newUrl(file.name); + const newFileUrl = await doMovePath(fileUrl); + if (newFileUrl) { + location.href = newFileUrl.split("/").slice(0, -1).join("/"); + } +} + +async function doMovePath(fileUrl) { + const fileUrlObj = new URL(fileUrl); + + const prefix = DATA.uri_prefix.slice(0, -1); + + const filePath = decodeURIComponent(fileUrlObj.pathname.slice(prefix.length)); + + let newPath = prompt("Enter new path", filePath); + if (!newPath) return; + if (!newPath.startsWith("/")) newPath = "/" + newPath; + if (filePath === newPath) return; + const newFileUrl = fileUrlObj.origin + prefix + newPath.split("/").map(encodeURIComponent).join("/"); + + try { + await checkAuth(); + const res1 = await fetch(newFileUrl, { + method: "HEAD", + }); + if (res1.status === 200) { + if (!confirm("Override existing file?")) { + return; + } + } + const res2 = await fetch(fileUrl, { + method: "MOVE", + headers: { + "Destination": newFileUrl, + } + }); + await assertResOK(res2); + return newFileUrl; + } catch (err) { + alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`); + } +} + + +/** + * Save editor change + */ +async function saveChange() { + try { + await fetch(baseUrl(), { + method: "PUT", + body: $editor.value, + }); + location.reload(); + } catch (err) { + alert(`Failed to save file, ${err.message}`); + } +} + +async function checkAuth(variant) { + if (!DATA.auth) return; + const qs = variant ? `?${variant}` : ""; + const res = await fetch(baseUrl() + qs, { + method: "CHECKAUTH", + }); + await assertResOK(res); + $loginBtn.classList.add("hidden"); + $logoutBtn.classList.remove("hidden"); + $userName.textContent = await res.text(); +} + +function logout() { + if (!DATA.auth) return; + const url = baseUrl(); + const xhr = new XMLHttpRequest(); + xhr.open("LOGOUT", url, true, DATA.user); + xhr.onload = () => { + location.href = url; + } + xhr.send(); +} + +/** + * Create a folder + * @param {string} name + */ +async function createFolder(name) { + const url = newUrl(name); + try { + await checkAuth(); + const res = await fetch(url, { + method: "MKCOL", + }); + await assertResOK(res); + location.href = url; + } catch (err) { + alert(`Cannot create folder \`${name}\`, ${err.message}`); + } +} + +async function createFile(name) { + const url = newUrl(name); + try { + await checkAuth(); + const res = await fetch(url, { + method: "PUT", + body: "", + }); + await assertResOK(res); + location.href = url + "?edit"; + } catch (err) { + alert(`Cannot create file \`${name}\`, ${err.message}`); + } +} + +async function addFileEntries(entries, dirs) { + for (const entry of entries) { + if (entry.isFile) { + entry.file(file => { + new Uploader(file, dirs).upload(); + }); + } else if (entry.isDirectory) { + const dirReader = entry.createReader(); + + const successCallback = entries => { + if (entries.length > 0) { + addFileEntries(entries, [...dirs, entry.name]); + dirReader.readEntries(successCallback); + } + }; + + dirReader.readEntries(successCallback); + } + } +} + + +function newUrl(name) { + let url = baseUrl(); + if (!url.endsWith("/")) url += "/"; + url += name.split("/").map(encodeURIComponent).join("/"); + return url; +} + +function baseUrl() { + return location.href.split(/[?#]/)[0]; +} + +function baseName(url) { + return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]); +} + +function extName(filename) { + const dotIndex = filename.lastIndexOf('.'); + + if (dotIndex === -1 || dotIndex === 0 || dotIndex === filename.length - 1) { + return ''; + } + + return filename.substring(dotIndex); +} + +function getPathSvg(path_type) { + switch (path_type) { + case "Dir": + return ICONS.dir; + case "SymlinkFile": + return ICONS.symlinkFile; + case "SymlinkDir": + return ICONS.symlinkDir; + default: + return ICONS.file; + } +} + +function formatMtime(mtime) { + if (!mtime) return ""; + const date = new Date(mtime); + const year = date.getFullYear(); + const month = padZero(date.getMonth() + 1, 2); + const day = padZero(date.getDate(), 2); + const hours = padZero(date.getHours(), 2); + const minutes = padZero(date.getMinutes(), 2); + return `${year}-${month}-${day} ${hours}:${minutes}`; +} + +function padZero(value, size) { + return ("0".repeat(size) + value).slice(-1 * size); +} + +function formatDirSize(size) { + const unit = size === 1 ? "item" : "items"; + const num = size >= MAX_SUBPATHS_COUNT ? `>${MAX_SUBPATHS_COUNT - 1}` : `${size}`; + return ` ${num} ${unit}`; +} + +function formatFileSize(size) { + if (size == null) return [0, "B"]; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (size == 0) return [0, "B"]; + const i = parseInt(Math.floor(Math.log(size) / Math.log(1024))); + const raw = size / Math.pow(1024, i); + let value; + if (i > 0 && raw < 999.95) { + value = Math.round(raw * 10) / 10; + } else { + value = Math.round(raw); + } + return [value, sizes[i]]; +} + +function formatDuration(seconds) { + seconds = Math.ceil(seconds); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds - h * 3600) / 60); + const s = seconds - h * 3600 - m * 60; + return `${padZero(h, 2)}:${padZero(m, 2)}:${padZero(s, 2)}`; +} + +function formatPercent(percent) { + if (percent > 10) { + return percent.toFixed(1) + "%"; + } else { + return percent.toFixed(2) + "%"; + } +} + +function encodedStr(rawStr) { + return rawStr.replace(/[\u00A0-\u9999<>\&]/g, function (i) { + return '&#' + i.charCodeAt(0) + ';'; + }); +} + +async function assertResOK(res) { + if (!(res.status >= 200 && res.status < 300)) { + throw new Error(await res.text() || `Invalid status ${res.status}`); + } +} + +function getEncoding(contentType) { + const charset = contentType?.split(";")[1]; + if (/charset/i.test(charset)) { + let encoding = charset.split("=")[1]; + if (encoding) { + return encoding.toLowerCase(); + } + } + return 'utf-8'; +} + +// Parsing base64 strings with Unicode characters +function decodeBase64(base64String) { + const binString = atob(base64String); + const len = binString.length; + const bytes = new Uint8Array(len); + const arr = new Uint32Array(bytes.buffer, 0, Math.floor(len / 4)); + let i = 0; + for (; i < arr.length; i++) { + arr[i] = binString.charCodeAt(i * 4) | + (binString.charCodeAt(i * 4 + 1) << 8) | + (binString.charCodeAt(i * 4 + 2) << 16) | + (binString.charCodeAt(i * 4 + 3) << 24); + } + for (i = i * 4; i < len; i++) { + bytes[i] = binString.charCodeAt(i); + } + return new TextDecoder().decode(bytes); +} -- cgit v1.2.3