Implement reusable upload platform
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
export function addFileWithDedup(state, file, cfg) {
|
||||
const originalName = file.name;
|
||||
const existing = state.fileMeta.get(originalName);
|
||||
let name = originalName;
|
||||
|
||||
if (existing) {
|
||||
if (existing.size === file.size) return false;
|
||||
const dot = originalName.lastIndexOf('.');
|
||||
const base = dot > 0 ? originalName.slice(0, dot) : originalName;
|
||||
const extension = dot > 0 ? originalName.slice(dot) : '';
|
||||
let suffix = 2;
|
||||
while (state.fileMeta.has(`${base}_${suffix}${extension}`)) suffix += 1;
|
||||
name = `${base}_${suffix}${extension}`;
|
||||
}
|
||||
|
||||
const storedFile = new File([file], name, { lastModified: file.lastModified });
|
||||
state.fileMeta.set(name, { size: storedFile.size });
|
||||
const includedBytes = state.files.reduce(
|
||||
(total, item) => total + (state.overNames.has(item.name) ? 0 : item.size), 0,
|
||||
);
|
||||
if (storedFile.size > cfg.maxFileBytes
|
||||
|| includedBytes + storedFile.size > cfg.maxSessionBytes) {
|
||||
state.overNames.add(name);
|
||||
}
|
||||
state.files.push(storedFile);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function esc(value) {
|
||||
return String(value).replace(/[&<>"']/g, (character) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
}[character]));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function fs(bytes) {
|
||||
return bytes < 1024 ? `${bytes} B`
|
||||
: bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB`
|
||||
: `${(bytes / 1048576).toFixed(1)} MB`;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { addFiles } from './on_files_change.js';
|
||||
import { onFilesChange } from './on_files_change.js';
|
||||
import { onFolderChange } from './on_folder_change.js';
|
||||
import { render } from './render.js';
|
||||
import { setStatus } from './set_status.js';
|
||||
|
||||
export function initUploadTable(cfg) {
|
||||
const elements = {
|
||||
fileInputEl: cfg.fileInputEl,
|
||||
folderInputEl: cfg.folderInputEl,
|
||||
tableBodyEl: cfg.tableBodyEl,
|
||||
countEl: cfg.countEl,
|
||||
uploadBtnEl: cfg.uploadBtnEl,
|
||||
};
|
||||
const state = { files: [], fileMeta: new Map(), overNames: new Set(), busy: false };
|
||||
elements.fileInputEl.addEventListener('change', onFilesChange(state, cfg, elements));
|
||||
elements.folderInputEl.addEventListener('change', onFolderChange(state, cfg, elements));
|
||||
|
||||
const api = {
|
||||
pickFiles: () => elements.fileInputEl.click(),
|
||||
pickFolder: () => elements.folderInputEl.click(),
|
||||
addFiles: (files) => addFiles(state, cfg, files, elements),
|
||||
getFiles: () => state.files
|
||||
.filter((file) => !state.overNames.has(file.name))
|
||||
.map((file) => ({ path: file.name, name: file.name, size: file.size, file })),
|
||||
getOverNames: () => new Set(state.overNames),
|
||||
setStatus: (path, html) => setStatus(path, html, state, elements),
|
||||
render: () => render(state, elements),
|
||||
clear: () => {
|
||||
state.files = [];
|
||||
state.fileMeta.clear();
|
||||
state.overNames.clear();
|
||||
render(state, elements);
|
||||
},
|
||||
setBusy: (busy) => {
|
||||
state.busy = busy;
|
||||
elements.fileInputEl.disabled = busy;
|
||||
elements.folderInputEl.disabled = busy;
|
||||
elements.uploadBtnEl.disabled = busy || api.getFiles().length === 0;
|
||||
},
|
||||
};
|
||||
api.render();
|
||||
return api;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { listZipFiles } from '../zip/list_zip_files.js';
|
||||
import { addFileWithDedup } from './add_file_with_dedup.js';
|
||||
import { render } from './render.js';
|
||||
|
||||
export async function addFiles(state, cfg, files, elements) {
|
||||
for (const file of Array.from(files)) {
|
||||
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||||
addFileWithDedup(state, file, cfg);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const extracted = await listZipFiles(file, cfg.allowedExt);
|
||||
if (extracted.length) {
|
||||
extracted.forEach((item) => addFileWithDedup(state, item, cfg));
|
||||
} else {
|
||||
addFileWithDedup(state, file, cfg);
|
||||
}
|
||||
} catch (error) {
|
||||
addFileWithDedup(state, file, cfg);
|
||||
}
|
||||
}
|
||||
render(state, elements);
|
||||
}
|
||||
|
||||
export function onFilesChange(state, cfg, elements) {
|
||||
return () => {
|
||||
if (!state.busy) addFiles(state, cfg, elements.fileInputEl.files, elements);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { listZipFiles } from '../zip/list_zip_files.js';
|
||||
import { addFileWithDedup } from './add_file_with_dedup.js';
|
||||
import { render } from './render.js';
|
||||
|
||||
export function onFolderChange(state, cfg, elements) {
|
||||
return async () => {
|
||||
if (state.busy) return;
|
||||
for (const file of Array.from(elements.folderInputEl.files)) {
|
||||
const parts = (file.webkitRelativePath || file.name).split('/');
|
||||
const relativePath = parts.slice(1).join('/') || file.name;
|
||||
const lowerPath = relativePath.toLowerCase();
|
||||
const directory = relativePath.includes('/')
|
||||
? relativePath.slice(0, relativePath.lastIndexOf('/')) : '';
|
||||
if (lowerPath.endsWith('.zip')) {
|
||||
try {
|
||||
const extracted = await listZipFiles(file, cfg.allowedExt);
|
||||
if (extracted.length) {
|
||||
extracted.forEach((item) => addFileWithDedup(state, new File([item],
|
||||
directory ? `${directory}/${item.name}` : item.name,
|
||||
{ lastModified: item.lastModified }), cfg));
|
||||
} else {
|
||||
addFileWithDedup(state, new File([file], relativePath,
|
||||
{ lastModified: file.lastModified }), cfg);
|
||||
}
|
||||
} catch (error) {
|
||||
addFileWithDedup(state, new File([file], relativePath,
|
||||
{ lastModified: file.lastModified }), cfg);
|
||||
}
|
||||
} else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) {
|
||||
addFileWithDedup(state, new File([file], relativePath,
|
||||
{ lastModified: file.lastModified }), cfg);
|
||||
}
|
||||
}
|
||||
elements.folderInputEl.value = '';
|
||||
render(state, elements);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { esc } from './esc.js';
|
||||
import { fs } from './fs.js';
|
||||
|
||||
export function render(state, elements) {
|
||||
if (!state.files.length) {
|
||||
elements.tableBodyEl.innerHTML = '<tr><td colspan="3">Нет выбранных файлов</td></tr>';
|
||||
} else {
|
||||
elements.tableBodyEl.innerHTML = state.files.map((file, index) => {
|
||||
const over = state.overNames.has(file.name);
|
||||
const status = over ? 'не учитывается (лимит)' : 'готов';
|
||||
return `<tr id="row-${index}"><td>${esc(file.name)}</td>`
|
||||
+ `<td>${fs(file.size)}</td><td id="st-${index}">${status}</td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
const included = state.files.filter((file) => !state.overNames.has(file.name));
|
||||
const overCount = state.files.length - included.length;
|
||||
const size = included.reduce((total, file) => total + file.size, 0);
|
||||
elements.countEl.textContent = `${included.length} учитываются`
|
||||
+ (overCount ? ` + ${overCount} свыше лимита` : '')
|
||||
+ ` · ${fs(size)}`;
|
||||
elements.uploadBtnEl.disabled = included.length === 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function setStatus(path, html, state, elements) {
|
||||
const index = state.files.findIndex((file) => file.name === path);
|
||||
const cell = elements.tableBodyEl.querySelector(`#st-${index}`);
|
||||
if (cell) cell.innerHTML = html;
|
||||
}
|
||||
Reference in New Issue
Block a user