unziponline / script.js
Felguk's picture
Create a unzip online using JSZIP library
228425f verified
"use strict";
(function () {
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
const dropZone = $("#dropZone");
const fileInput = $("#fileInput");
const searchInput = $("#searchInput");
const clearBtn = $("#clearBtn");
const downloadAllBtn = $("#downloadAllBtn");
const statusEl = $("#status");
const progressEl = $("#progress");
const progressBar = $("#progressBar");
const fileNameEl = $("#fileName");
const fileSizeEl = $("#fileSize");
const fileListEl = $("#fileList");
const countEl = $("#count");
const previewSection = $("#previewSection");
const previewContent = $("#previewContent");
const closePreviewBtn = $("#closePreviewBtn");
const state = {
zip: null,
entries: [], // { name, file, size, type, lastModDate, url? }
objectUrls: [],
totalBytes: 0,
loadedBytes: 0,
currentZipFile: null
};
// Utils
function bytesToSize(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const val = parseFloat((bytes / Math.pow(k, i)).toFixed(2));
return `${val} ${sizes[i]}`;
}
function sanitizeName(name) {
// Avoid path traversal and invalid characters
const cleaned = name.replace(/^(\.*\/)+/, "").replace(/\.\./g, "");
return cleaned || "file";
}
function setStatus(text, type = "info") {
statusEl.textContent = text;
statusEl.className = "status";
if (type === "error") {
statusEl.style.color = "#ef4444";
} else if (type === "success") {
statusEl.style.color = "#22c55e";
} else {
statusEl.style.color = "var(--muted)";
}
}
function setProgress(percent) {
if (percent <= 0 || percent >= 100) {
progressEl.style.display = "none";
progressEl.setAttribute("aria-hidden", "true");
} else {
progressEl.style.display = "block";
progressEl.setAttribute("aria-hidden", "false");
}
progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`;
}
function clearObjectUrls() {
state.objectUrls.forEach((url) => URL.revokeObjectURL(url));
state.objectUrls = [];
}
function resetAll() {
state.zip = null;
state.entries = [];
state.totalBytes = 0;
state.loadedBytes = 0;
state.currentZipFile = null;
clearObjectUrls();
fileListEl.innerHTML = "";
countEl.textContent = "0";
fileNameEl.textContent = "";
fileSizeEl.textContent = "";
setStatus("No file loaded.");
setProgress(0);
clearBtn.disabled = true;
downloadAllBtn.disabled = true;
hidePreview();
}
function hidePreview() {
previewSection.hidden = true;
previewContent.innerHTML = "";
}
function showPreviewFor(entry) {
previewSection.hidden = false;
previewContent.innerHTML = "";
const type = (entry.type || "").toLowerCase();
const name = entry.name;
// Text preview
if (type.startsWith("text/") || /\.(txt|md|csv|json|js|ts|jsx|tsx|html|css|scss|sass|less|yml|yaml|xml|log)$/i.test(name)) {
const pre = document.createElement("pre");
pre.textContent = "Loading preview...";
entry.file.text().then((txt) => {
pre.textContent = txt;
}).catch(() => {
pre.textContent = "Unable to preview this text file.";
});
previewContent.appendChild(pre);
return;
}
// Image preview
if (type.startsWith("image/") || /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(name)) {
const img = document.createElement("img");
img.alt = name;
const url = URL.createObjectURL(entry.file);
state.objectUrls.push(url);
img.src = url;
img.onload = () => {
// Keep object URL for preview lifetime; will be revoked on clear/reset
};
previewContent.appendChild(img);
return;
}
// PDF preview
if (type === "application/pdf" || /\.pdf$/i.test(name)) {
const embed = document.createElement("embed");
embed.type = "application/pdf";
const url = URL.createObjectURL(entry.file);
state.objectUrls.push(url);
embed.src = url;
previewContent.appendChild(embed);
return;
}
// Fallback message
const p = document.createElement("p");
p.textContent = "Preview is not available for this file type.";
previewContent.appendChild(p);
}
function renderList(entries) {
fileListEl.innerHTML = "";
const frag = document.createDocumentFragment();
entries.forEach((entry) => {
const li = document.createElement("li");
li.className = "file";
const nameCol = document.createElement("div");
nameCol.className = "file__name";
const badge = document.createElement("span");
badge.className = "badge";
badge.textContent = entry.type || "file";
const path = document.createElement("span");
path.className = "path";
path.textContent = entry.name;
nameCol.appendChild(badge);
nameCol.appendChild(path);
const size = document.createElement("div");
size.className = "file__size";
size.textContent = bytesToSize(entry.size);
const actions = document.createElement("div");
actions.className = "file__actions";
const previewBtn = document.createElement("button");
previewBtn.className = "btn btn-secondary";
previewBtn.textContent = "Preview";
previewBtn.addEventListener("click", () => showPreviewFor(entry));
const downloadBtn = document.createElement("button");
downloadBtn.className = "btn btn-primary";
downloadBtn.textContent = "Download";
downloadBtn.addEventListener("click", () => {
const a = document.createElement("a");
const url = URL.createObjectURL(entry.file);
state.objectUrls.push(url);
a.href = url;
a.download = entry.name.split("/").pop();
document.body.appendChild(a);
a.click();
a.remove();
});
actions.appendChild(previewBtn);
actions.appendChild(downloadBtn);
li.appendChild(nameCol);
li.appendChild(size);
li.appendChild(actions);
frag.appendChild(li);
});
fileListEl.appendChild(frag);
countEl.textContent = String(entries.length);
}
async function handleZipFile(file) {
resetAll();
state.currentZipFile = file;
fileNameEl.textContent = file.name || "ZIP archive";
fileSizeEl.textContent = bytesToSize(file.size);
setStatus("Loading ZIP...");
setProgress(2);
try {
const zip = await JSZip.loadAsync(file);
state.zip = zip;
const entries = [];
let processed = 0;
const total = Object.keys(zip.files).length;
for (const [path, entry] of Object.entries(zip.files)) {
// Skip directories
if (entry.dir) {
processed++;
setProgress(Math.round((processed / total) * 100));
continue;
}
const safeName = sanitizeName(path);
// Obtain as Blob
const blob = await entry.async("blob");
const fileObj = new File([blob], safeName, { type: blob.type || "application/octet-stream", lastModified: entry.date?.getTime?.() || Date.now() });
entries.push({
name: safeName,
file: fileObj,
size: fileObj.size,
type: fileObj.type || "application/octet-stream",
lastModDate: entry.date
});
processed++;
if (processed % 5 === 0) {
setProgress(Math.round((processed / total) * 100));
await new Promise((r) => setTimeout(r, 0)); // Yield to UI
}
}
// Sort by path then size desc for consistent order
entries.sort((a, b) => a.name.localeCompare(b.name) || b.size - a.size);
state.entries = entries;
state.totalBytes = entries.reduce((acc, e) => acc + e.size, 0);
state.loadedBytes = state.totalBytes;
renderList(entries);
setStatus(`Loaded ${entries.length} files from "${file.name}".`);
setProgress(100);
clearBtn.disabled = false;
downloadAllBtn.disabled = entries.length === 0;
} catch (err) {
console.error(err);
setStatus("Failed to read the ZIP file. It may be corrupted or unsupported.", "error");
setProgress(0);
}
}
function filterEntries(query) {
const q = query.trim().toLowerCase();
if (!q) return state.entries;
return state.entries.filter((e) => e.name.toLowerCase().includes(q));
}
async function downloadAll() {
if (!state.entries.length) return;
const zip = new JSZip();
for (const e of state.entries) {
zip.file(e.name, e.file);
}
setStatus("Preparing ZIP for download...");
setProgress(3);
try {
const blob = await zip.generateAsync(
{ type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 } },
(metadata) => {
setProgress(Math.max(3, Math.min(99, Math.round(metadata.percent))));
}
);
const a = document.createElement("a");
const url = URL.createObjectURL(blob);
a.href = url;
a.download = (state.currentZipFile?.name?.replace(/\.zip$/i, "") || "extracted") + "-extracted.zip";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setStatus("ZIP ready for download.", "success");
setProgress(100);
} catch (err) {
console.error(err);
setStatus("Failed to create ZIP for download.", "error");
setProgress(0);
}
}
// Event handlers
dropZone.addEventListener("click", () => fileInput.click());
dropZone.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
fileInput.click();
}
});
["dragenter", "dragover"].forEach((evt) => {
dropZone.addEventListener(evt, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add("is-dragover");
});
});
["dragleave", "drop"].forEach((evt) => {
dropZone.addEventListener(evt, (e) => {
e.preventDefault();
e.stopPropagation();
if (evt === "drop") {
const dt = e.dataTransfer;
const file = dt?.files?.[0];
if (file) handleZipFile(file);
}
dropZone.classList.remove("is-dragover");
});
});
fileInput.addEventListener("change", () => {
const file = fileInput.files?.[0];
if (file) handleZipFile(file);
});
searchInput.addEventListener("input", () => {
renderList(filterEntries(searchInput.value));
});
clearBtn.addEventListener("click", () => {
fileInput.value = "";
resetAll();
});
downloadAllBtn.addEventListener("click", () => {
downloadAll();
});
closePreviewBtn.addEventListener("click", () => {
hidePreview();
});
// Init
resetAll();
})();