@cocreate/file
Version:
A versatile, configurable headless file uploader supporting local and server operations. Accessible via a JavaScript API and HTML5 attributes, it provides seamless file reading, writing, and uploading with fallbacks to the standard HTML5 file input API. I
1,035 lines (887 loc) • 28.6 kB
JavaScript
/********************************************************************************
* Copyright (C) 2023 CoCreate and Contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
********************************************************************************/
/**
* Commercial Licensing Information:
* For commercial use of this software without the copyleft provisions of the AGPLv3,
* you must obtain a commercial license from CoCreate LLC.
* For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
*/
import Observer from "@cocreate/observer";
import Crud from "@cocreate/crud-client";
import Elements from "@cocreate/elements";
import Actions from "@cocreate/actions";
import { render } from "@cocreate/render";
import { queryElements } from "@cocreate/utils";
import "@cocreate/element-prototype";
const inputs = new Map();
const Files = new Map();
/**
* Initializes file elements. If no parameter is provided, it queries and initializes all elements with type="file".
* It can also initialize a single element or an array of elements. Specifically focuses on elements of type 'file'.
*
* @param {(Element|Element[]|null)} [elements] - Optional. An element, an array of elements, or null.
* - If null or omitted, the function queries and initializes all elements in the document with type="file".
* - If a single element is provided, it initializes that element (assuming it is of type "file").
* - If an array of elements is provided, each element in the array is initialized.
*/
async function init(elements) {
if (!elements) elements = document.querySelectorAll('[type="file"]');
else if (!Array.isArray(elements)) elements = [elements];
for (let i = 0; i < elements.length; i++) {
let nestedInput,
isInput = elements[i].tagName === "INPUT";
if (!isInput) {
nestedInput = elements[i].querySelector('input[type="file"]');
}
elements[i].getValue = async () => await getFiles([elements[i]]);
elements[i].getFiles = async () => await getFiles([elements[i]]);
elements[i].setValue = (files) => setFiles(elements[i], files);
elements[i].renderValue = (files) => setFiles(elements[i], files);
// if (elements[i].renderValue) {
// let data = await elements[i].getValue()
// if (data)
// elements[i].setValue(data)
// }
if (elements[i].hasAttribute("directory")) {
if (!isInput && window.showDirectoryPicker)
elements[i].addEventListener("click", fileEvent);
else if ("webkitdirectory" in elements[i]) {
elements[i].webkitdirectory = true;
if (!isInput && !nestedInput) {
nestedInput = document.createElement("input");
nestedInput.type = "file";
nestedInput.setAttribute("hidden", "");
elements[i].appendChild(nestedInput);
nestedInput.fileElement = elements[i];
}
if (nestedInput) {
elements[i].addEventListener("click", function () {
nestedInput.click();
});
nestedInput.addEventListener("change", fileEvent);
} else elements[i].addEventListener("change", fileEvent);
} else
console.error(
"Directory selection not supported in this browser."
);
} else if (!isInput && window.showOpenFilePicker)
elements[i].addEventListener("click", fileEvent);
else {
if (!isInput && !nestedInput) {
nestedInput = document.createElement("input");
nestedInput.type = "file";
nestedInput.setAttribute("hidden", "");
elements[i].appendChild(nestedInput);
nestedInput.fileElement = elements[i];
}
if (nestedInput) {
elements[i].addEventListener("click", function () {
nestedInput.click();
});
nestedInput.addEventListener("change", fileEvent);
} else elements[i].addEventListener("change", fileEvent);
}
}
}
async function fileEvent(event) {
try {
let input = event.currentTarget;
let multiple = input.multiple;
// If 'multiple' is not explicitly set, check the attribute.
if (multiple !== true && multiple !== false) {
multiple = input.getAttribute("multiple");
multiple = multiple !== null && multiple !== "false";
input.multiple = multiple;
}
let selected = inputs.get(input) || new Map();
let files = input.files;
input = input.fileElement || input;
if (!files || !files.length) {
event.preventDefault();
if (input.hasAttribute("directory")) {
let handle = await window.showDirectoryPicker();
let file = {
name: handle.name,
directory: "/",
path: "/" + handle.name,
type: "text/directory",
"content-type": "text/directory"
};
file.input = input;
file.id = await getFileId(file);
if (selected.has(file.id)) {
console.log(
"Duplicate file has been selected. This could be in error as the browser does not provide a clear way of checking duplictaes"
);
}
file.handle = handle;
if (!multiple) {
for (let [id] of selected) {
Files.delete(id);
}
selected.clear();
}
selected.set(file.id, file);
Files.set(file.id, file);
files = await getDirectoryHandles(handle, handle.name);
} else {
files = await window.showOpenFilePicker({ multiple });
}
}
for (let i = 0; i < files.length; i++) {
const handle = files[i];
if (files[i].kind === "file") {
files[i] = await files[i].getFile();
files[i].handle = handle;
} else if (files[i].kind === "directory") {
files[i].handle = handle;
}
if (!files[i].src) await readFile(files[i]);
// if (!files[i].src)
// files[i].src = files[i]
// if (!files[i].src.name)
// files[i].src = files[i]
if (!files[i].size) files[i].size = handle.size;
files[i].directory = handle.directory || "/";
files[i].path = handle.path || "/";
files[i].pathname = handle.pathname || "/" + handle.name;
files[i]["content-type"] = files[i].type;
files[i].input = input;
files[i].id = await getFileId(files[i]);
if (selected.has(files[i].id)) {
console.log(
"Duplicate file has been selected. This could be in error as the browser does not provide a clear way of checking duplictaes"
);
}
if (!multiple) {
for (let [id] of selected) {
Files.delete(id);
}
selected.clear();
}
selected.set(files[i].id, files[i]);
Files.set(files[i].id, files[i]);
}
if (selected.size) {
inputs.set(input, selected);
// console.log("Files selected:", selected);
if (input.renderValue)
input.renderValue(Array.from(selected.values()));
const isImport = input.getAttribute("import");
const isRealtime = input.getAttribute("realtime");
if (isRealtime && isRealtime !== "false") {
if (isImport || isImport == "") {
Import(input);
} else if (input.save) input.save();
}
}
} catch (error) {
if (error.name !== "AbortError") {
console.error("Error selecting directory:", error);
}
}
}
async function getDirectoryHandles(handle, name) {
let handles = [];
for await (const entry of handle.values()) {
entry.directory = name;
entry.path = "/" + name + "/";
entry.pathname = "/" + name + "/" + entry.name;
if (!entry.webkitRelativePath) entry.webkitRelativePath = name;
if (entry.kind === "file") {
handles.push(entry);
} else if (entry.kind === "directory") {
entry.type = "text/directory";
handles.push(entry);
const entries = await getDirectoryHandles(
entry,
name + "/" + entry.name
);
handles = handles.concat(entries);
}
}
return handles;
}
async function getFileId(file) {
if ((file.id = file.pathname)) {
return file.id;
} else {
file.id = `${file.name}${file.size}${file.type}${file.lastModified}`;
return file.id;
}
}
async function getFiles(fileInputs, readAs) {
const files = [];
if (!Array.isArray(fileInputs)) fileInputs = [fileInputs];
for (let input of fileInputs) {
const selected = inputs.get(input);
if (selected) {
for (let file of Array.from(selected.values())) {
if (!file.src) {
// if (readAs === 'blob')
file.src = file;
// else
// await readFile(file, readAs)
}
let fileObject = { ...file };
fileObject.size = file.size;
await getCustomData(fileObject);
files.push(fileObject);
}
}
}
return files;
}
async function getCustomData(file) {
if (!file.id) file.id = file.pathname;
// TODO: Consider potential replacment of file_id, perhaps supporting selector
let form = document.querySelector(`[file_id="${file.id}"]`);
if (form) {
let elements = form.querySelectorAll("[file]");
for (let i = 0; i < elements.length; i++) {
let name = elements[i].getAttribute("file");
if (name) {
file[name] = await elements[i].getValue();
}
}
}
delete file.input;
return file;
}
// This function reads the file and returns its src
function readFile(file, readAs) {
return new Promise((resolve) => {
const fileType = file.type.split("/");
if (fileType[1] === "directory") {
return resolve(file);
} else if (readAs) {
if (readAs === "blob") return resolve(file);
} else if (fileType[0] === "image") {
readAs = "readAsDataURL";
} else if (fileType[0] === "video") {
readAs = "readAsDataURL";
} else if (fileType[0] === "audio") {
readAs = "readAsDataURL";
} else if (fileType[1] === "pdf") {
readAs = "readAsDataURL";
} else if (
["doc", "msword", "docx", "xlsx", "pptx"].includes(fileType[1])
) {
readAs = "readAsBinaryString";
} else {
readAs = "readAsText";
}
const reader = new FileReader();
reader[readAs](file);
reader.onload = () => {
file.src = reader.result;
if (["doc", "msword", "docx", "xlsx", "pptx"].includes(fileType)) {
file.src = btoa(file.src);
}
resolve(file);
};
});
}
function setFiles(element, files) {
if (!files || typeof files !== "object") return;
if (!Array.isArray(files)) files = [files];
else if (!files.length) return;
let selected = inputs.get(element) || new Map();
if (!element.multiple) {
for (let key of selected.keys()) {
selected.delete(key); // Remove the entry from the selected map
Files.delete(key); // Remove the corresponding entry from the Files map
}
}
for (let i = 0; i < files.length; i++) {
if (!files[i].id) files[i].id = files[i].pathname;
files[i].input = element;
selected.set(files[i].id, files[i]);
Files.set(files[i].id, files[i]);
}
inputs.set(element, selected);
if (element.renderValue)
render({
source: element,
data: Array.from(selected.values())
});
}
// TODO: Could this benifit from media processing to save results locally
async function save(element, action, data) {
try {
if (!data) data = [];
if (!Array.isArray(element)) element = [element];
for (let i = 0; i < element.length; i++) {
const inputs = [];
if (element[i].type === "file") inputs.push(element[i]);
else if (element[i].tagName === "form") {
let fileInputs =
element[i].querySelectorAll('input[type="file"]');
inputs.push(...fileInputs);
} else {
const form = element[i].closest("form");
if (form)
inputs.push(...form.querySelectorAll('input[type="file"]'));
}
for (let input of inputs) {
let files = await getFiles(input);
for (let i = 0; i < files.length; i++) {
if (!files[i].src) continue;
if (files[i].handle && action !== "download") {
if (action === "saveAs") {
if (files[i].kind === "file") {
const options = {
suggestedName: files[i].name,
types: [
{
description: "Text Files"
}
]
};
files[i].handle =
await window.showSaveFilePicker(options);
} else if (files[i].kind === "directory") {
// Create a new subdirectory
files[i].handle = await files[
i
].handle.getDirectoryHandle("new_directory", {
create: true
});
return;
}
}
if (files[i].handle.kind === "directory") continue;
const writable = await files[i].handle.createWritable();
await writable.write(files[i].src);
await writable.close();
} else {
const blob = new Blob([files[i].src], {
type: files[i].type
});
// Create a temporary <a> element to trigger the file download
const downloadLink = document.createElement("a");
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = files[i].name;
// Trigger the download
downloadLink.click();
}
}
}
let queryElements = queryElements({
element: element[i],
prefix: action
});
if (queryElements.length) {
save(queryElements, action, data);
}
}
return data;
} catch (error) {
if (error.name !== "AbortError") {
console.error("Error selecting files:", error);
}
}
}
async function upload(element, data) {
if (!data) data = [];
if (!Array.isArray(element)) element = [element];
for (let i = 0; i < element.length; i++) {
const fileInputs = [];
if (element[i].type === "file") fileInputs.push(element[i]);
else if (element[i].tagName === "form") {
fileInputs.push(
...element[i].querySelectorAll('input[type="file"]')
);
} else {
const form = element[i].closest("form");
if (form)
fileInputs.push(...form.querySelectorAll('input[type="file"]'));
}
for (let input of fileInputs) {
let Data = Elements.getObject(input);
let object = input.getAttribute("object") || "";
let key = input.getAttribute("key");
Data.broadcastBrowser = false;
Data.method = "object.update";
if (!Data.array) Data.array = "files";
let path = input.getAttribute("path");
let directory = "/";
if (path) {
directory = path.split("/");
directory = directory[directory.length - 1];
if (!path.endsWith("/")) path += "/";
} else path = directory = "/";
if (!Data.host) Data.host = ["*"];
if (!Data.public) Data.public = true;
if (input.getFilter) {
Data.$filter = await input.getFilter();
if (!Data.$filter.query) Data.$filter.query = {};
} else
Data.$filter = {
query: {}
};
// let files = await getFiles(input, 'blob')
let files;
const selected = inputs.get(input);
if (selected) {
files = Array.from(selected.values());
}
let segmentSize = 10 * 1024 * 1024;
for (let i = 0; i < files.length; i++) {
files[i].path = path;
files[i].pathname = path + files[i].name;
files[i].directory = directory;
// let fileObject = { ...file }
// fileObject.size = file.size
// await getCustomData(fileObject)
if (input.processFile && files[i].size > segmentSize) {
// let test = await input.processFile(files[i], null, segmentSize, null, null, null, input);
let { playlist, segments } = await input.processFile(
files[i],
null,
segmentSize
);
// Create a video element
const videoElement = document.createElement("video");
videoElement.setAttribute("controls", ""); // Add controls so you can play/pause
videoElement.style.width = "100%";
document.body.appendChild(videoElement);
const mediaSource = new MediaSource();
videoElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener("sourceopen", () => {
const sourceBuffer = mediaSource.addSourceBuffer(
'video/mp4; codecs="avc1.42E01E"'
);
sourceBuffer.addEventListener("updateend", () => {
console.log("Append operation completed.");
try {
console.log(
"Buffered ranges:",
sourceBuffer.buffered
);
// Append next segment here if applicable
} catch (e) {
console.error(
"Error accessing buffered property:",
e
);
}
});
function appendSegment(index) {
if (index >= segments.length) {
console.log("All segments have been appended.");
return;
}
if (!sourceBuffer.updating) {
segments[index].src
.arrayBuffer()
.then((arrayBuffer) => {
console.log(
`Appending segment ${index}`
);
sourceBuffer.appendBuffer(arrayBuffer);
// Next segment will be appended on 'updateend' event
})
.catch((error) => {
console.error(
`Error reading segment[${index}] as ArrayBuffer:`,
error
);
});
}
}
// Append the first segment to start
appendSegment(0);
});
// mediaSource.addEventListener('sourceopen', () => {
// const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E"'); // avc1.4D401E, avc1.4D401F, avc1.4D4028, avc1.4D4020, avc1.4D4029, avc1.4D402A
// // Append the first segment to start
// if (!sourceBuffer.updating) {
// segments[0].src.arrayBuffer().then(arrayBuffer => {
// sourceBuffer.appendBuffer(arrayBuffer);
// // Wait for 3 seconds before logging the sourceBuffer state
// setTimeout(() => {
// console.log(sourceBuffer);
// }, 3000); // 3000 milliseconds = 3 seconds
// sourceBuffer.addEventListener('updateend', () => {
// console.log('Append operation completed.');
// try {
// console.log('Buffered ranges:', sourceBuffer.buffered);
// } catch (e) {
// console.error('Error accessing buffered property:', e);
// }
// // Proceed with additional operations here
// });
// }).catch(error => {
// console.error('Error reading segment[0] as ArrayBuffer:', error);
// });
// // segments[0].src.arrayBuffer().then(arrayBuffer => {
// // sourceBuffer.appendBuffer(arrayBuffer);
// // })
// // let segmentLength = 0
// // sourceBuffer.addEventListener('updateend', () => {
// // segmentLength += 1
// // if (segments[segmentLength])
// // segments[segmentLength].src.arrayBuffer().then(arrayBuffer => {
// // console.log(sourceBuffer)
// // // sourceBuffer.appendBuffer(arrayBuffer);
// // })
// // });
// }
// });
}
// files[i].src = playlist
// for (let j = 0; j < segments.length; j++) {
// segments[j].path = path
// segments[j].pathname = path + segments[j].name
// segments[j].directory = directory
// segments[j] = { ...segments[j], ...await readFile(segments[j].src) }
// segments[j].public = true
// segments[j].host = ['*']
// playlist.segments[j].src = segments[j].pathname
// Data.$filter.query.pathname = segments[j].pathname
// Crud.send({
// ...Data,
// object: segments[j],
// upsert: true
// });
// }
// } else {
// files[i] = { ...files[i], ...await readFile(files[i].src) }
// }
// if (!key) {
// Data.object = { ...files[i] }
// } else {
// Data.object = { [key]: { ...files[i] } }
// }
// if (object) {
// Data.object._id = object // test
// }
// delete Data.object.input
// Data.$filter.query.pathname = files[i].pathname
// let response = await Crud.send({
// ...Data,
// upsert: true
// });
// console.log(response, 'tes')
// if (response && (!object || object !== response.object)) {
// Elements.setTypeValue(element, response);
// }
}
}
let queriedElements = queryElements({
element: element[i],
prefix: "upload"
});
if (queriedElements.length) {
upload(queriedElements, data);
}
}
return data;
}
async function Import(element, data) {
if (!data) data = [];
if (!Array.isArray(element)) element = [element];
for (let i = 0; i < element.length; i++) {
const inputs = [];
if (element[i].type === "file") inputs.push(element[i]);
else if (element[i].tagName === "form") {
let fileInputs = element[i].querySelectorAll('input[type="file"]');
inputs.push(...fileInputs);
} else {
const form = element[i].closest("form");
if (form)
inputs.push(...form.querySelectorAll('input[type="file"]'));
}
if (inputs.length) {
let Data = await getFiles(inputs);
Data.reduce((result, { src }) => {
try {
const parsedSrc = JSON.parse(src);
if (Array.isArray(parsedSrc)) data.push(...parsedSrc);
else data.push(parsedSrc);
} catch (error) {
console.error(`Error parsing JSON: ${error}`);
}
return result;
}, []);
}
if (element[i].type !== "file") {
let Data = Elements.getObject(element[i]);
if (Data.type) {
if (element[i].getFilter)
Data.$filter = await element[i].getFilter();
if (Data.type === "key") Data.type = "object";
data.push(Data);
}
}
if (data.length) {
for (let i = 0; i < data.length; i++) {
// TODO: if _id exist use update method
data[i].method = data[i].type + ".create";
data[i] = await Crud.send(data[i]);
}
}
let queriedElements = queryElements({
element: element[i],
prefix: "import"
});
if (queriedElements.length) {
Import(queriedElements, data);
}
}
return data;
}
// TODO: Export selected rows or entire table or entire array
async function Export(element, data) {
if (!data) data = [];
if (!Array.isArray(element)) element = [element];
for (let i = 0; i < element.length; i++) {
const inputs = [];
if (element[i].type === "file") inputs.push(element[i]);
else if (element[i].tagName === "form") {
let fileInputs = element[i].querySelectorAll('input[type="file"]');
inputs.push(...fileInputs);
} else {
const form = element[i].closest("form");
if (form)
inputs.push(...form.querySelectorAll('input[type="file"]'));
}
if (inputs.length) data.push(...getFiles(inputs));
let Data = Elements.getObject(element[i]);
if (Data.type) {
if (element[i].getFilter)
Data.$filter = await element[i].getFilter();
if (Data.type === "key") Data.type = "object";
Data.method = Data.type + ".read";
Data = await Crud.send(Data);
data.push(...Data[Data.type]);
}
let queriedElements = queryElements({
element: element[i],
prefix: "export"
});
if (queriedElements.length) {
Export(queriedElements, data);
}
}
if (data.length) exportFile(data);
return data;
}
async function exportFile(data) {
let name = data.type || "download";
let exportData = JSON.stringify(data, null, 2);
let blob = new Blob([exportData], { type: "application/json" });
let url = URL.createObjectURL(blob);
let link = document.createElement("a");
link.href = url;
link.download = name;
document.body.appendChild(link);
link.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window
})
);
URL.revokeObjectURL(url);
link.remove();
}
// TODO: handled by import? if value is a valid url get file by url?
async function importURL(action) {
try {
let element = action.element;
let url = element.getAttribute("url");
if (!url) {
element = action.form.querySelector("[import-url]");
if (!element) return;
url = element.getValue();
if (!url) return;
}
const urlObject = new URL(url);
const filename = urlObject.pathname.split("/").pop();
const file = {
src: url,
name: filename,
directory: "/",
path: "/",
pathname: "/" + filename
};
await getCustomData(file);
let data = await Crud.socket.send({
method: "importUrl",
file,
broadcast: false,
broadcastClient: false
});
let queriedElements = queryElements({ element, prefix: "import-url" });
if (queriedElements.length) {
for (let queriedElement of queriedElements)
queriedElement.setValue(data.file);
}
action.element.dispatchEvent(
new CustomEvent(action.name, {
detail: {}
})
);
} catch (error) {
console.error("Error importing file from URL:", error);
throw error;
}
}
async function fileRenderAction(action) {
const element = action.element;
let file_id = element.getAttribute("file_id");
if (!file_id) {
const closestElement = element.closest("[file_id]");
if (closestElement) file_id = closestElement.getAttribute("file_id");
}
let input = Files.get(file_id).input;
if (!file_id || !input) return;
let file = inputs.get(input).get(file_id);
if (!file) return;
if (action.name === "createFile") {
let name = element.getAttribute("value");
create(file, "file", name);
} else if (action.name === "deleteFile") Delete(file);
else if (action.name === "createDirectory") {
let name = element.getAttribute("value");
create(file, "directory", name);
} else if (action.name === "deleteDirectory") Delete(file);
action.element.dispatchEvent(
new CustomEvent(action.name, {
detail: {}
})
);
}
async function create(directory, type, name, src = "") {
try {
if (directory.handle && directory.input) {
if (!name) {
const name = prompt("Enter the file name:");
if (!name) {
console.log("Invalid file name.");
return;
}
}
let handle, file;
if (type === "directory") {
handle = await directory.handle.getDirectoryHandle(name, {
create: true
});
file = { name: handle.name, type: "text/directory" };
} else if (type === "file") {
handle = await directory.handle.getFileHandle(name, {
create: true
});
const writable = await handle.createWritable();
// Write data to the new file...
await writable.write(src);
await writable.close();
file = handle.getFile();
}
if (directory.input) {
file.directory = directory.name;
file.pathname = directory.path + "/" + file.name;
file.path = directory.path + "/" + file.name;
file.input = directory.input;
file.handle = handle;
file["content-type"] = file.type;
file.id = await getFileId(file);
if (inputs.get(directory.input).has(file.id)) {
console.log(
"Duplicate file has been selected. This could be in error as the browser does not provide a clear way of checking duplictaes"
);
}
inputs.get(directory.input).set(file.id, file);
}
}
} catch (error) {
console.log("Error adding file:", error);
}
}
async function Delete(file) {
try {
if (file.handle) {
await file.handle.remove();
if (file.input && file.id) inputs.get(file.input).delete(file.id);
}
} catch (error) {
console.log("Error deleting file:", error);
}
}
Observer.init({
name: "CoCreateFileAddedNodes",
types: ["addedNodes"],
selector: '[type="file"]',
callback: (mutation) => init(mutation.target)
});
Observer.init({
name: "CoCreateFileAttributes",
types: ["attributes"],
attributeFilter: ["type"],
selector: '[type="file"]',
callback: (mutation) => init(mutation.target)
});
Actions.init([
{
name: [
"upload",
"download",
"saveLocally",
"asveAs",
"import",
"export",
"importUrl"
],
callback: (action) => {
if (action.name === "upload") upload(action.element);
else if (
action.name === "saveLocally" ||
action.name === "saveAs"
) {
save(action.element);
} else if (action.name === "export") {
Export(action.element);
} else if (action.name === "import") {
Import(action.element);
} else if (action.name === "importUrl") {
importURL(action);
} else {
// Something...
}
action.element.dispatchEvent(
new CustomEvent(action.name, {
detail: {}
})
);
}
},
{
name: [
"createFile",
"deleteFile",
"createDirectory",
"deleteDirectory"
],
callback: (action) => {
fileRenderAction(action);
}
}
]);
init();
export default { inputs, getFiles, create, Delete };