lufi/themes/default/public/js/lufi-upload.js

562 lines
15 KiB
JavaScript

import {
lufi,
errAsync,
okAsync,
ResultAsync,
isSecureContext,
CryptoAlgorithm,
} from "./lufi.js";
import { filesize } from "./filesize.esm.min.js";
document.addEventListener("DOMContentLoaded", () => {
const deleteAfterDaysDOM = document.getElementById("delete-after-days");
const deleteOnFirstViewDOM = document.getElementById("delete-on-first-view");
const dropZoneDOM = document.querySelector(".drop-zone");
const fileCardsDOM = document.querySelector(".file-cards");
const maxFileSizeDOM = document.querySelector(".max-file-size");
const messagesZoneDOM = document.querySelector(".messages-zone");
const mustZipDOM = document.getElementById("must-zip");
const passwordDOM = document.getElementById("password");
const uploadButtonDOM = document.querySelector("#upload-button");
const uploadedZoneDOM = document.querySelector(".uploaded-zone");
const uploadZipDOM = document.querySelector(".action-upload-zip");
const zipNameDOM = document.getElementById("zip-name");
const zipZoneDOM = document.querySelector(".zip-zone");
const inputZipNameDOM = document.querySelector(".input-zip-name");
// Global zip objects for currently created zip file
let zipSize = 0;
// Init the list of files (used by LDAP invitation feature)
let filesURLs = [];
let archiveEntries;
/**
* Add item to localStorage
*
* @param {string} name
* @param {string} url
* @param {number} size
* @param {boolean} del_at_first_view
* @param {number} created_at
* @param {number} delay
* @param {string} serverKey
* @param {string} actionToken
*/
const addItem = (
name,
url,
size,
del_at_first_view,
created_at,
delay,
serverKey,
actionToken
) => {
let files = JSON.parse(localStorage.getItem(`${prefix}files`)) || [];
files.push({
name,
short: serverKey,
url,
size,
del_at_first_view,
created_at,
delay,
token: actionToken,
});
localStorage.setItem(`${prefix}files`, JSON.stringify(files));
};
const clearZip = () => {
zipZoneDOM.classList.add("hidden");
zipZoneDOM.querySelector(".action-upload-zip").classList.remove("hidden");
zipZoneDOM.querySelector(".zip-compressing").classList.add("hidden");
zipZoneDOM.querySelector(".files-list").replaceChildren();
inputZipNameDOM.classList.add("hidden");
archiveEntries = undefined;
mustZipDOM.checked = false;
};
const copyLinksToClipboard = async () => {
const inputs = document.querySelectorAll(".download-input");
const textArray = [];
inputs.forEach((node) => textArray.push(node.value));
try {
await navigator.clipboard.writeText(textArray.join("\n")).then(() => {
addToast(i18n.copySuccess, "success");
});
} catch (error) {
alert(i18n.hits);
}
};
/**
* Create a new File Card element
*
* @param {LufiJob} job
* @param {string} type Can be error, success or ongoing
* @param {Node} existingCard Existing card to be replaced with the new one
* @returns
*/
const createFileCard = (job, type, existingCard = undefined) => {
const { lufiFile } = job;
const cardDOM = document
.querySelector(`.template.file-card.${type}`)
.cloneNode(true);
cardDOM.classList.remove("template");
cardDOM.id = `file-card-${lufiFile.keys.client}`;
cardDOM.querySelector(".file-name").innerText = escapeHtml(lufiFile.name);
cardDOM.querySelector(".file-size").innerText = filesize(lufiFile.size);
cardDOM.querySelector(".action-close").onclick = () => {
lufi
.cancel(job)
.map(() => {
removeFileCard(job.lufiFile.keys.server, cardDOM);
})
.mapErr((error) => {
showErrorOnCard(job, ongoingFileCardDOM, error.msg);
});
};
switch (type) {
case "success":
cardDOM.querySelector(
".action-mail"
).href = `${actionURL}m?links=${encodeURIComponent(
`["${lufiFile.keys.server}"]`
)}`;
const expirationDate =
lufiFile.delay === 0
? i18n.noLimit
: `${i18n.expiration} ${formatDate(
lufiFile.delay * 86400 + lufiFile.createdAt
)}`;
cardDOM.querySelector(".file-expiration").innerText = expirationDate;
cardDOM.querySelector(".download-button").href = lufiFile.downloadUrl();
cardDOM.querySelector(".delete-button").href = lufiFile.removeUrl();
cardDOM.querySelector(".copy-button").onclick = async (event) => {
event.preventDefault();
try {
await navigator.clipboard
.writeText(lufiFile.downloadUrl())
.then(() => {
addToast(i18n.copySuccess, "success");
});
} catch (error) {
showMessage(error.message, "error");
}
};
cardDOM.querySelector(".download-input").value = lufiFile.downloadUrl();
cardDOM.querySelector(".delete-input").value = lufiFile.removeUrl();
updateMailLinksButton(lufiFile.keys.server);
break;
case "ongoing":
cardDOM.querySelector(".progress-bar").max = lufiFile.totalChunks;
break;
}
if (existingCard) {
existingCard.replaceWith(cardDOM);
} else {
fileCardsDOM.prepend(cardDOM);
}
if (fileCardsDOM.querySelectorAll(".file-card.success").length > 0) {
uploadedZoneDOM.querySelector(".buttons").classList.remove("hidden");
}
return cardDOM;
};
const handleZipEvents = () => {
zipZoneDOM.querySelector(".action-close").onclick = () => {
clearZip();
};
mustZipDOM.onchange = () => {
console.debug(zipNameDOM.classList);
if (!mustZipDOM.checked) {
clearZip();
} else {
inputZipNameDOM.classList.remove("hidden");
}
};
zipNameDOM.oninput = () => {
let name = zipNameDOM.value || "documents.zip";
if (!name.endsWith(".zip")) {
name += name.endsWith(".") ? "zip" : ".zip";
}
name = escapeHtml(name);
zipZoneDOM.querySelector(".file-name").innerText = name;
};
};
/**
* Decide what to do with files passed to upload zone
*
* @param {FileList} files
*/
const handleFiles = (files) => {
files = Array.from(files) || [];
document.body.style.cursor = "wait";
const { deleteDays, shouldDeleteOnFirstView, password, mustZip } =
retrieveUploadParams();
if (!mustZip) {
files.forEach((file) => {
addToast(i18n.enqueued.replace("XXX", file.name), "success");
});
document.body.style.cursor = "auto";
startUpload(
files,
deleteDays,
shouldDeleteOnFirstView,
mustZip,
undefined,
password
);
} else {
zipZoneDOM.classList.remove("hidden");
lufi
.addFilesToArchive(files, archiveEntries)
.andThen((entries) => {
archiveEntries = entries;
const listDOM = zipZoneDOM.querySelector(".files-list");
listDOM.replaceChildren();
for (const [name, file] of Object.entries(archiveEntries)) {
zipSize += file.length;
const listItemDOM = document.createElement("li");
listItemDOM.innerText = `${escapeHtml(name)} (${filesize(
file.length
)})`;
listDOM.appendChild(listItemDOM);
}
zipZoneDOM.querySelector(".file-size").textContent =
filesize(zipSize);
document.body.style.cursor = "auto";
return okAsync(undefined);
})
.orElse((error) => console.error(error.message));
}
};
const handleUploadEvents = () => {
dropZoneDOM.addEventListener("drop", (event) => {
handleFiles(event.dataTransfer.files);
});
dropZoneDOM.addEventListener("dragover", (event) => {
event.dataTransfer.dropEffect = "copy";
});
uploadButtonDOM.addEventListener("change", (event) => {
handleFiles(event.target.files);
});
uploadZipDOM.onclick = uploadZip;
};
/**
* Remove a File Card element and hide Uploaded Zone if there is no File Card left
*
* @param {Node} card
*/
const removeFileCard = (serverKey, card) => {
card.remove();
updateMailLinksButton(serverKey, true);
if (fileCardsDOM.children.length === 0) {
uploadedZoneDOM.classList.add("hidden");
}
if (fileCardsDOM.querySelectorAll(".file-card.success").length === 0) {
uploadedZoneDOM.querySelector(".buttons").classList.add("hidden");
}
};
const retrieveUploadParams = () => {
const mustZip = document.querySelector("#must-zip").checked;
const deleteDays = deleteAfterDaysDOM.value;
const shouldDeleteOnFirstView = deleteOnFirstViewDOM.checked;
const password = passwordDOM?.value || "";
const zipName = zipNameDOM.value;
return { deleteDays, shouldDeleteOnFirstView, password, mustZip, zipName };
};
/**
* Show a notification at the top of the page
*
* @param {string} text
* @param {string} type Can be error or success
*/
const showMessage = (text, type) => {
const messageDOM = document.createElement("div");
messageDOM.innerText = text;
messageDOM.classList.add("message-card", type);
messagesZoneDOM.appendChild(messageDOM);
};
/**
* Show an error message inside the File Card.
*
* @param {LufiJob} job
* @param {Node} cardDOM
* @param {string} text
*/
const showErrorOnCard = (job, cardDOM, text) => {
const errorCardDOM = createFileCard(job, "error", cardDOM);
errorCardDOM.querySelector(".upload-error").innerText = text;
};
/**
* [Invitation feature] Send URLs of files to server
*/
const sendFilesURLs = () => {
if (filesURLs.length > 0) {
fetch(sendFilesURLsURL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
urls: filesURLs,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
addToast(data.msg, "success");
} else {
addToast(data.msg, "error");
}
})
.catch((error) => {
console.error("Error:", error);
});
}
};
/**
* Start the upload of provided files
*
* @param {File[]} files
* @param {number} delay
* @param {boolean} delAtFirstView
* @param {boolean} isZipped
* @param {string} zipName
* @param {string} password
* @returns
*/
const startUpload = (
files,
delay,
delAtFirstView,
isZipped,
zipName,
password
) => {
uploadedZoneDOM.classList.remove("hidden");
const serverUrl = new URL(ws_url.replace("/upload", ""));
serverUrl.protocol = serverUrl.protocol === "ws:" ? "http:" : "https:";
return lufi
.upload(
serverUrl.href,
files,
delay,
delAtFirstView,
isZipped,
zipName,
password,
isSecureContext() ? CryptoAlgorithm.WebCrypto : CryptoAlgorithm.Sjcl
)
.andThen((jobs) =>
ResultAsync.combine(
jobs.map((job) => {
const ongoingFileCardDOM = createFileCard(job, "ongoing");
job.onProgress(() => {
updateProgressBar(job.lufiFile, ongoingFileCardDOM);
});
return job
.waitForCompletion()
.andThen((job) => {
notify(i18n.fileUploaded, job.lufiFile.name);
createFileCard(
job,
isGuest ? "guest" : "success",
ongoingFileCardDOM
);
// Add the file to localStorage
if (!isGuest) {
addItem(
job.lufiFile.name,
job.lufiFile.downloadUrl(),
job.lufiFile.size,
delAtFirstView,
job.lufiFile.createdAt,
delay,
job.lufiFile.keys.server,
job.lufiFile.actionToken
);
}
if (isGuest && job.lufiFile.keys.server !== null) {
filesURLs.push(
JSON.stringify({
name: job.lufiFile.name,
short: job.lufiFile.keys.server,
url: job.lufiFile.downloadUrl(),
size: job.lufiFile.size,
created_at: job.lufiFile.createdAt,
delay,
token: job.lufiFile.actionToken,
})
);
}
if (isGuest) {
sendFilesURLs();
}
return okAsync(job);
})
.orElse((error) => {
showErrorOnCard(job, ongoingFileCardDOM, error.message);
if (isGuest) {
sendFilesURLs();
}
return errAsync(error);
});
})
)
)
.orElse((error) => console.error(error));
};
/**
* Update the "send all links by mail" button.
*
* @param {string} serverKey
* @param {boolean} remove If we should remove the serverKey from the links
*/
const updateMailLinksButton = (serverKey, remove = false) => {
const buttonDOM = document.querySelector(".action-mail-links");
const url = new URL(buttonDOM.href);
let links = JSON.parse(url.searchParams.get("links") || "[]");
if (remove) {
links = links.filter((item) => item !== serverKey);
} else {
links.push(serverKey);
}
url.searchParams.set("links", JSON.stringify(links));
buttonDOM.href = url;
};
/**
* Update the progress bar of the File Card
*
* @param {LufiFile} lufiFile
* @param {Node} cardDOM
*/
const updateProgressBar = (lufiFile, cardDOM) => {
const percent = Math.round(
(lufiFile.chunksReady * 100) / lufiFile.totalChunks
);
cardDOM.querySelector(".progress-bar").style.width = `${percent}%`;
cardDOM.querySelector(".progress-percent").innerText = percent;
};
const uploadZip = () => {
document.body.style.cursor = "wait";
zipZoneDOM.querySelector(".action-upload-zip").classList.add("hidden");
zipZoneDOM.querySelector(".zip-compressing").classList.remove("hidden");
const { zipName, deleteDays, shouldDeleteOnFirstView, password } =
retrieveUploadParams();
lufi
.compress(archiveEntries, zipName)
.andThen((job) => {
document.body.style.cursor = "auto";
return job.waitForCompletion();
})
.andThen((job) => {
// if '.zip-zone' is hidden, the zipping has been aborted
if (!zipZoneDOM.classList.contains("hidden")) {
addToast(i18n.enqueued.replace("XXX", zipName), "success");
clearZip();
return startUpload(
[job.archiveFile],
deleteDays,
shouldDeleteOnFirstView,
true,
zipName,
password
);
}
})
.mapErr((error) => showMessage(error.message));
};
if (maxSize) {
maxFileSizeDOM.style.display = "block";
maxFileSizeDOM.textContent = i18n.maxSize.replace("XXX", filesize(maxSize));
}
handleUploadEvents();
handleZipEvents();
document.querySelector(".action-copy-links").onclick = () => {
copyLinksToClipboard();
};
});