mirror of
https://github.com/error311/FileRise.git
synced 2026-05-09 13:30:48 -05:00
75f2d6de69
- files(core): add internal authenticated file links (create/resolve token endpoints + ACL-checked deep-link open) - ui(files): add Link File in row context menu + toolbar single-selection action with copy-link modal - boot/auth: preserve fileLink return URL through login/OIDC, resolve link post-auth, focus/highlight linked file - shares: add upload-only File Request mode for shared folders with hide-listing support and dedicated drop-zone UI - shared-upload: add per-link validation/rate limits/quota tracking + safer relative-path handling for chunked uploads - admin: improve shared links listing with file-request grouping and created-by/source metadata
776 lines
25 KiB
JavaScript
776 lines
25 KiB
JavaScript
// sharedDropView.js
|
|
import { setLocale, t } from './i18n.js?v={{APP_QVER}}';
|
|
|
|
document.addEventListener('DOMContentLoaded', async function () {
|
|
try {
|
|
const saved = localStorage.getItem('language') || 'en';
|
|
await setLocale(saved);
|
|
} catch (e) {
|
|
await setLocale('en');
|
|
}
|
|
|
|
const tx = (key, placeholders, fallback) => {
|
|
const out = t(key, placeholders);
|
|
if (out === key && typeof fallback === 'string') {
|
|
return fallback;
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const dataEl = document.getElementById('shared-data');
|
|
if (!dataEl) return;
|
|
|
|
let payload = {};
|
|
try {
|
|
payload = JSON.parse(dataEl.textContent || '{}');
|
|
} catch (e) {
|
|
payload = {};
|
|
}
|
|
|
|
const mode = String(payload.mode || '').toLowerCase();
|
|
const hideListing = !!payload.hideListing;
|
|
if (mode !== 'drop' && !hideListing) {
|
|
return;
|
|
}
|
|
|
|
const token = String(payload.token || '');
|
|
const shareRoot = String(payload.shareRoot || 'root');
|
|
const currentPath = String(payload.path || '');
|
|
const allowSubfolders = !!payload.allowSubfolders;
|
|
const preserveFolderStructure = payload.preserveFolderStructure !== 0;
|
|
const maxFileSizeMb = Number.isFinite(payload.maxFileSizeMb) ? Number(payload.maxFileSizeMb) : 0;
|
|
const maxFileSizeBytes = maxFileSizeMb > 0 ? Math.round(maxFileSizeMb * 1024 * 1024) : 0;
|
|
const allowedTypes = Array.isArray(payload.allowedTypes)
|
|
? payload.allowedTypes.map((x) => String(x || '').trim().toLowerCase()).filter(Boolean)
|
|
: [];
|
|
const dailyFileLimit = Number.isFinite(payload.dailyFileLimit) ? Number(payload.dailyFileLimit) : 0;
|
|
const maxTotalMbPerDay = Number.isFinite(payload.maxTotalMbPerDay) ? Number(payload.maxTotalMbPerDay) : 0;
|
|
|
|
const form = document.getElementById('shareDropUploadForm');
|
|
const dropzone = document.getElementById('shareDropzone');
|
|
const fileInput = document.getElementById('shareDropFileInput');
|
|
const folderInput = document.getElementById('shareDropFolderInput');
|
|
const chooseFilesBtn = document.getElementById('shareChooseFilesBtn');
|
|
const chooseFolderBtn = document.getElementById('shareChooseFolderBtn');
|
|
const queueEl = document.getElementById('shareDropQueue');
|
|
const rulesEl = document.getElementById('shareDropRules');
|
|
const breadcrumbsEl = document.getElementById('shareBreadcrumbs');
|
|
const themeToggleBtn = document.getElementById('shareThemeToggle');
|
|
const dropUploadErrorId = 'shareDropUploadError';
|
|
|
|
if (!form || !dropzone || !fileInput || !folderInput || !queueEl) {
|
|
return;
|
|
}
|
|
|
|
function ensureDropUploadErrorEl() {
|
|
let el = document.getElementById(dropUploadErrorId);
|
|
if (el) return el;
|
|
|
|
el = document.createElement('div');
|
|
el.id = dropUploadErrorId;
|
|
el.className = 'fr-share-alert fr-share-alert-error fr-share-upload-error';
|
|
el.setAttribute('role', 'alert');
|
|
el.hidden = true;
|
|
|
|
if (form.parentNode) {
|
|
form.parentNode.insertBefore(el, form.nextSibling);
|
|
}
|
|
return el;
|
|
}
|
|
|
|
function clearDropUploadError() {
|
|
const el = document.getElementById(dropUploadErrorId);
|
|
if (!el) return;
|
|
el.hidden = true;
|
|
el.textContent = '';
|
|
}
|
|
|
|
function showDropUploadError(message, statusCode) {
|
|
const el = ensureDropUploadErrorEl();
|
|
if (!el) return;
|
|
const reason = String(message || tx('share_upload_failed', null, 'Upload failed.')).trim()
|
|
|| tx('share_upload_failed', null, 'Upload failed.');
|
|
const code = Number.isFinite(statusCode) && statusCode > 0 ? Math.trunc(statusCode) : 0;
|
|
el.textContent = code > 0
|
|
? tx('share_upload_failed_http', { code, reason }, 'Upload failed (HTTP ' + code + '): ' + reason)
|
|
: tx('share_upload_failed_message', { reason }, 'Upload failed: ' + reason);
|
|
el.hidden = false;
|
|
}
|
|
|
|
const THEME_KEY = 'fr_share_theme';
|
|
|
|
function getStoredTheme() {
|
|
try {
|
|
const t = localStorage.getItem(THEME_KEY);
|
|
return (t === 'light' || t === 'dark') ? t : 'auto';
|
|
} catch (e) {
|
|
return 'auto';
|
|
}
|
|
}
|
|
|
|
function setStoredTheme(theme) {
|
|
try {
|
|
localStorage.setItem(THEME_KEY, theme);
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function getSystemTheme() {
|
|
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
? 'dark'
|
|
: 'light';
|
|
}
|
|
|
|
function getActiveTheme(storedTheme) {
|
|
return (storedTheme === 'light' || storedTheme === 'dark') ? storedTheme : getSystemTheme();
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
if (theme === 'light' || theme === 'dark') {
|
|
document.documentElement.setAttribute('data-share-theme', theme);
|
|
} else {
|
|
document.documentElement.removeAttribute('data-share-theme');
|
|
}
|
|
}
|
|
|
|
function updateThemeLabel(storedTheme) {
|
|
if (!themeToggleBtn) return;
|
|
const active = getActiveTheme(storedTheme);
|
|
themeToggleBtn.textContent = active === 'dark' ? 'Light mode' : 'Dark mode';
|
|
}
|
|
|
|
if (themeToggleBtn) {
|
|
const storedTheme = getStoredTheme();
|
|
applyTheme(storedTheme);
|
|
updateThemeLabel(storedTheme);
|
|
|
|
themeToggleBtn.addEventListener('click', function () {
|
|
const currentStored = getStoredTheme();
|
|
const active = getActiveTheme(currentStored);
|
|
const next = active === 'dark' ? 'light' : 'dark';
|
|
setStoredTheme(next);
|
|
applyTheme(next);
|
|
updateThemeLabel(next);
|
|
});
|
|
}
|
|
|
|
function getBasePathFromLocation() {
|
|
try {
|
|
let p = String(window.location.pathname || '');
|
|
p = p.replace(/\/api\/folder\/shareFolder\.php$/i, '');
|
|
p = p.replace(/\/+$/, '');
|
|
if (!p || p === '/') return '';
|
|
if (!p.startsWith('/')) p = '/' + p;
|
|
return p;
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function withBasePath(path) {
|
|
const base = getBasePathFromLocation();
|
|
const s = String(path || '');
|
|
if (!base || !s.startsWith('/')) return s;
|
|
if (s === base || s.startsWith(base + '/')) return s;
|
|
return base + s;
|
|
}
|
|
|
|
function buildShareUrl(path) {
|
|
const urlParams = new URLSearchParams(window.location.search || '');
|
|
const pass = urlParams.get('pass') || '';
|
|
const passParam = pass ? '&pass=' + encodeURIComponent(pass) : '';
|
|
const p = path ? '&path=' + encodeURIComponent(path) : '';
|
|
return withBasePath('/api/folder/shareFolder.php?token=' + encodeURIComponent(token) + passParam + p);
|
|
}
|
|
|
|
function renderBreadcrumbs() {
|
|
if (!breadcrumbsEl) return;
|
|
while (breadcrumbsEl.firstChild) breadcrumbsEl.removeChild(breadcrumbsEl.firstChild);
|
|
|
|
const rootLabel = (shareRoot && shareRoot !== 'root')
|
|
? shareRoot.split('/').pop()
|
|
: tx('share_drop_root_label', null, 'Upload files');
|
|
|
|
const rootLink = document.createElement('a');
|
|
rootLink.href = buildShareUrl('');
|
|
rootLink.textContent = rootLabel;
|
|
breadcrumbsEl.appendChild(rootLink);
|
|
|
|
if (!allowSubfolders || !currentPath) return;
|
|
|
|
const parts = currentPath.split('/').filter(Boolean);
|
|
let acc = '';
|
|
parts.forEach((part) => {
|
|
acc = acc ? acc + '/' + part : part;
|
|
const sep = document.createElement('span');
|
|
sep.className = 'fr-share-breadcrumb-sep';
|
|
sep.textContent = '/';
|
|
breadcrumbsEl.appendChild(sep);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = buildShareUrl(acc);
|
|
link.textContent = part;
|
|
breadcrumbsEl.appendChild(link);
|
|
});
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
const n = Number(bytes || 0);
|
|
if (!Number.isFinite(n) || n <= 0) return '0 B';
|
|
if (n < 1024) return n + ' B';
|
|
if (n < 1048576) return (n / 1024).toFixed(2) + ' KB';
|
|
if (n < 1073741824) return (n / 1048576).toFixed(2) + ' MB';
|
|
return (n / 1073741824).toFixed(2) + ' GB';
|
|
}
|
|
|
|
function renderRules() {
|
|
if (!rulesEl) return;
|
|
rulesEl.textContent = '';
|
|
|
|
const parts = [];
|
|
if (maxFileSizeMb > 0) {
|
|
parts.push(tx('share_drop_rule_max_file_size', { mb: maxFileSizeMb }, 'Max file size: ' + maxFileSizeMb + ' MB'));
|
|
}
|
|
if (allowedTypes.length) {
|
|
const joined = allowedTypes.join(', ');
|
|
parts.push(tx('share_drop_rule_allowed_types', { types: joined }, 'Allowed types: ' + joined));
|
|
}
|
|
if (dailyFileLimit > 0) {
|
|
parts.push(tx('share_drop_rule_daily_file_limit', { count: dailyFileLimit }, 'Daily file limit: ' + dailyFileLimit));
|
|
}
|
|
if (maxTotalMbPerDay > 0) {
|
|
parts.push(tx('share_drop_rule_daily_size_limit', { mb: maxTotalMbPerDay }, 'Daily size limit: ' + maxTotalMbPerDay + ' MB'));
|
|
}
|
|
if (preserveFolderStructure) {
|
|
parts.push(tx('share_drop_rule_preserve_structure', null, 'Folder structure is preserved when available.'));
|
|
}
|
|
|
|
if (!parts.length) return;
|
|
|
|
parts.forEach((msg) => {
|
|
const tag = document.createElement('span');
|
|
tag.className = 'fr-share-rule-pill';
|
|
tag.textContent = msg;
|
|
rulesEl.appendChild(tag);
|
|
});
|
|
}
|
|
|
|
function getExt(name) {
|
|
const idx = name.lastIndexOf('.');
|
|
if (idx === -1) return '';
|
|
return name.slice(idx + 1).toLowerCase();
|
|
}
|
|
|
|
function sanitizeRelativePath(raw, fallbackName) {
|
|
let path = String(raw || '').replace(/\\/g, '/').trim();
|
|
path = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
if (!path) return fallbackName;
|
|
path = path.replace(/\/+/g, '/');
|
|
const parts = path.split('/').filter(Boolean);
|
|
const clean = [];
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const seg = parts[i].trim();
|
|
if (!seg || seg === '.' || seg === '..') {
|
|
continue;
|
|
}
|
|
clean.push(seg);
|
|
}
|
|
if (!clean.length) {
|
|
return fallbackName;
|
|
}
|
|
return clean.join('/');
|
|
}
|
|
|
|
function getRelativePathForItem(file) {
|
|
const raw = preserveFolderStructure
|
|
? (file.webkitRelativePath || file.relativePath || file.name)
|
|
: file.name;
|
|
return sanitizeRelativePath(raw, file.name);
|
|
}
|
|
|
|
function appendCommonFormData(formData) {
|
|
const tokenInput = form.querySelector('input[name="token"]');
|
|
const passInput = form.querySelector('input[name="pass"]');
|
|
const pathInput = form.querySelector('input[name="path"]');
|
|
const shareTokenInput = form.querySelector('input[name="share_upload_token"]');
|
|
|
|
if (tokenInput && tokenInput.value) formData.append('token', tokenInput.value);
|
|
if (passInput && passInput.value) formData.append('pass', passInput.value);
|
|
if (pathInput && pathInput.value) formData.append('path', pathInput.value);
|
|
if (shareTokenInput && shareTokenInput.value) formData.append('share_upload_token', shareTokenInput.value);
|
|
formData.append('response', 'json');
|
|
}
|
|
|
|
function xhrJson(url, formData, onProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', url, true);
|
|
if (typeof onProgress === 'function') {
|
|
xhr.upload.addEventListener('progress', onProgress);
|
|
}
|
|
xhr.addEventListener('load', function () {
|
|
let data = null;
|
|
let rawMessage = '';
|
|
try {
|
|
data = JSON.parse(xhr.responseText || '{}');
|
|
} catch (e) {
|
|
rawMessage = String(xhr.responseText || '').replace(/\s+/g, ' ').trim();
|
|
if (rawMessage.length > 180) {
|
|
rawMessage = rawMessage.slice(0, 180) + '...';
|
|
}
|
|
data = { error: rawMessage || tx('share_upload_failed', null, 'Upload failed.') };
|
|
}
|
|
if (xhr.status >= 200 && xhr.status < 300 && !data.error) {
|
|
resolve(data);
|
|
return;
|
|
}
|
|
const msg = data && data.error
|
|
? String(data.error)
|
|
: tx('share_upload_failed_http', {
|
|
code: xhr.status,
|
|
reason: tx('share_upload_failed', null, 'Upload failed.'),
|
|
}, 'Upload failed (HTTP ' + xhr.status + ').');
|
|
const err = new Error(msg);
|
|
err.status = xhr.status || 0;
|
|
reject(err);
|
|
});
|
|
xhr.addEventListener('error', function () {
|
|
const err = new Error(tx('share_upload_network_error', null, 'Network error. Please check your connection and try again.'));
|
|
err.status = 0;
|
|
reject(err);
|
|
});
|
|
xhr.send(formData);
|
|
});
|
|
}
|
|
|
|
const queue = [];
|
|
const rows = new Map();
|
|
let running = 0;
|
|
let uploadSequence = 0;
|
|
const MAX_CONCURRENT = 2;
|
|
const CHUNK_THRESHOLD = 8 * 1024 * 1024;
|
|
const CHUNK_SIZE = 2 * 1024 * 1024;
|
|
const uploadUrl = form.getAttribute('action') || withBasePath('/api/folder/uploadToSharedFolder.php');
|
|
|
|
function prependRow(parent, child) {
|
|
if (!parent || !child) return;
|
|
if (typeof parent.prepend === 'function') {
|
|
parent.prepend(child);
|
|
return;
|
|
}
|
|
if (parent.firstChild) {
|
|
parent.insertBefore(child, parent.firstChild);
|
|
} else {
|
|
parent.appendChild(child);
|
|
}
|
|
}
|
|
|
|
function isFileLike(file) {
|
|
return !!file && typeof file === 'object' && typeof file.name === 'string';
|
|
}
|
|
|
|
function ensureRow(item) {
|
|
if (rows.has(item.id)) return rows.get(item.id);
|
|
|
|
const row = document.createElement('div');
|
|
row.className = 'fr-share-queue-item';
|
|
|
|
const head = document.createElement('div');
|
|
head.className = 'fr-share-queue-head';
|
|
|
|
const nameEl = document.createElement('div');
|
|
nameEl.className = 'fr-share-queue-name';
|
|
nameEl.textContent = item.relativePath;
|
|
|
|
const statusEl = document.createElement('div');
|
|
statusEl.className = 'fr-share-queue-status';
|
|
statusEl.textContent = tx('share_drop_status_queued', null, 'Queued');
|
|
|
|
head.appendChild(nameEl);
|
|
head.appendChild(statusEl);
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'fr-share-queue-meta';
|
|
meta.textContent = isFileLike(item.file) ? formatBytes(item.file.size) : '-';
|
|
|
|
const progressWrap = document.createElement('div');
|
|
progressWrap.className = 'fr-share-queue-progress';
|
|
const progressFill = document.createElement('div');
|
|
progressFill.className = 'fr-share-queue-progress-fill';
|
|
progressFill.style.width = '0%';
|
|
progressWrap.appendChild(progressFill);
|
|
|
|
row.appendChild(head);
|
|
row.appendChild(meta);
|
|
row.appendChild(progressWrap);
|
|
prependRow(queueEl, row);
|
|
|
|
const refs = { row, statusEl, progressFill, meta };
|
|
rows.set(item.id, refs);
|
|
return refs;
|
|
}
|
|
|
|
function setItemStatus(item, status, pct, message) {
|
|
const refs = ensureRow(item);
|
|
item.status = status;
|
|
item.progress = Math.max(0, Math.min(100, Math.round(Number(pct || 0))));
|
|
refs.progressFill.style.width = item.progress + '%';
|
|
|
|
if (status === 'uploading') {
|
|
refs.row.classList.remove('is-error', 'is-done');
|
|
refs.statusEl.textContent = message || tx('share_uploading_progress', { pct: item.progress }, 'Uploading ' + item.progress + '%');
|
|
return;
|
|
}
|
|
if (status === 'done') {
|
|
refs.row.classList.remove('is-error');
|
|
refs.row.classList.add('is-done');
|
|
refs.statusEl.textContent = message || tx('share_drop_status_uploaded', null, 'Uploaded');
|
|
refs.progressFill.style.width = '100%';
|
|
return;
|
|
}
|
|
if (status === 'error') {
|
|
refs.row.classList.add('is-error');
|
|
refs.row.classList.remove('is-done');
|
|
refs.statusEl.textContent = message || tx('share_drop_status_failed', null, 'Failed');
|
|
return;
|
|
}
|
|
refs.statusEl.textContent = message || tx('share_drop_status_queued', null, 'Queued');
|
|
}
|
|
|
|
function validateQueueItem(item) {
|
|
if (!item || !isFileLike(item.file)) {
|
|
return tx('share_drop_error_invalid_file', null, 'Invalid file.');
|
|
}
|
|
if (!allowSubfolders && String(item.relativePath || '').indexOf('/') !== -1) {
|
|
return tx(
|
|
'share_drop_error_subfolders_disabled',
|
|
null,
|
|
'Skipped: subfolder uploads are not enabled for this share.'
|
|
);
|
|
}
|
|
if (maxFileSizeBytes > 0 && item.file.size > maxFileSizeBytes) {
|
|
return tx(
|
|
'share_drop_error_size_exceeded',
|
|
{ mb: maxFileSizeMb },
|
|
'Skipped: file is larger than ' + maxFileSizeMb + ' MB.'
|
|
);
|
|
}
|
|
if (allowedTypes.length) {
|
|
const ext = getExt(item.file.name || '');
|
|
if (!ext || !allowedTypes.includes(ext)) {
|
|
return tx('share_drop_error_type_not_allowed', null, 'Skipped: file type not allowed.');
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function makeUploadId() {
|
|
try {
|
|
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
|
|
return window.crypto.randomUUID().replace(/-/g, '');
|
|
}
|
|
if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
|
|
const arr = new Uint8Array(16);
|
|
window.crypto.getRandomValues(arr);
|
|
return Array.from(arr).map((n) => n.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
uploadSequence += 1;
|
|
return 'upl' + String(Date.now()) + '_' + String(uploadSequence);
|
|
}
|
|
|
|
async function uploadSingle(item) {
|
|
const formData = new FormData();
|
|
appendCommonFormData(formData);
|
|
const rel = getRelativePathForItem(item.file);
|
|
if (rel !== item.file.name) {
|
|
formData.append('relativePath', rel);
|
|
}
|
|
formData.append('fileToUpload', item.file, item.file.name);
|
|
|
|
await xhrJson(uploadUrl, formData, function (evt) {
|
|
if (!evt.lengthComputable) return;
|
|
const pct = Math.min(100, Math.max(0, Math.round((evt.loaded / evt.total) * 100)));
|
|
setItemStatus(item, 'uploading', pct);
|
|
});
|
|
}
|
|
|
|
async function uploadChunked(item) {
|
|
const rel = getRelativePathForItem(item.file);
|
|
const totalChunks = Math.max(1, Math.ceil(item.file.size / CHUNK_SIZE));
|
|
const uploadId = makeUploadId();
|
|
|
|
for (let index = 1; index <= totalChunks; index++) {
|
|
const start = (index - 1) * CHUNK_SIZE;
|
|
const end = Math.min(item.file.size, start + CHUNK_SIZE);
|
|
const blob = item.file.slice(start, end);
|
|
|
|
const formData = new FormData();
|
|
appendCommonFormData(formData);
|
|
formData.append('resumableChunkNumber', String(index));
|
|
formData.append('resumableTotalChunks', String(totalChunks));
|
|
formData.append('resumableIdentifier', uploadId);
|
|
formData.append('resumableFilename', item.file.name);
|
|
formData.append('resumableTotalSize', String(item.file.size));
|
|
formData.append('resumableCurrentChunkSize', String(blob.size));
|
|
if (rel) {
|
|
formData.append('resumableRelativePath', rel);
|
|
}
|
|
formData.append('file', blob, item.file.name + '.part' + index);
|
|
|
|
const maxAttempts = 3;
|
|
let attempt = 0;
|
|
let sent = false;
|
|
while (attempt < maxAttempts && !sent) {
|
|
attempt += 1;
|
|
try {
|
|
await xhrJson(uploadUrl, formData, function (evt) {
|
|
if (!evt.lengthComputable) return;
|
|
const chunkPct = evt.total > 0 ? (evt.loaded / evt.total) : 0;
|
|
const base = (index - 1) / totalChunks;
|
|
const pct = Math.round((base + (chunkPct / totalChunks)) * 100);
|
|
setItemStatus(item, 'uploading', pct);
|
|
});
|
|
sent = true;
|
|
} catch (err) {
|
|
if (attempt >= maxAttempts) {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
const donePct = Math.round((index / totalChunks) * 100);
|
|
setItemStatus(item, 'uploading', donePct);
|
|
}
|
|
}
|
|
|
|
async function uploadItem(item) {
|
|
const validationMsg = validateQueueItem(item);
|
|
if (validationMsg) {
|
|
setItemStatus(item, 'error', 0, validationMsg);
|
|
return;
|
|
}
|
|
|
|
setItemStatus(item, 'uploading', 0, tx('share_uploading_progress', { pct: 0 }, 'Uploading 0%'));
|
|
if (item.file.size >= CHUNK_THRESHOLD && item.file.size > CHUNK_SIZE) {
|
|
await uploadChunked(item);
|
|
} else {
|
|
await uploadSingle(item);
|
|
}
|
|
setItemStatus(item, 'done', 100, tx('share_drop_status_uploaded', null, 'Uploaded'));
|
|
}
|
|
|
|
function runQueue() {
|
|
while (running < MAX_CONCURRENT) {
|
|
const next = queue.find((q) => q.status === 'queued');
|
|
if (!next) break;
|
|
running += 1;
|
|
setItemStatus(next, 'uploading', 0, tx('share_uploading_progress', { pct: 0 }, 'Uploading 0%'));
|
|
uploadItem(next)
|
|
.catch((err) => {
|
|
const msg = err && err.message ? err.message : tx('share_upload_failed', null, 'Upload failed.');
|
|
const statusCode = (err && Number.isFinite(err.status)) ? Number(err.status) : 0;
|
|
setItemStatus(next, 'error', next.progress || 0, msg);
|
|
showDropUploadError(msg, statusCode);
|
|
})
|
|
.finally(() => {
|
|
running -= 1;
|
|
runQueue();
|
|
});
|
|
}
|
|
}
|
|
|
|
function enqueueFiles(items) {
|
|
if (!Array.isArray(items) || !items.length) return;
|
|
|
|
clearDropUploadError();
|
|
let added = 0;
|
|
items.forEach((it) => {
|
|
const file = it.file;
|
|
if (!isFileLike(file)) return;
|
|
|
|
const relativePath = sanitizeRelativePath(
|
|
preserveFolderStructure ? (it.relativePath || file.webkitRelativePath || file.name) : file.name,
|
|
file.name
|
|
);
|
|
|
|
const id = makeUploadId() + '_' + String(queue.length + 1);
|
|
const row = {
|
|
id,
|
|
file,
|
|
relativePath,
|
|
status: 'queued',
|
|
progress: 0
|
|
};
|
|
queue.push(row);
|
|
setItemStatus(row, 'queued', 0, tx('share_drop_status_queued', null, 'Queued'));
|
|
added += 1;
|
|
});
|
|
|
|
if (added === 0) {
|
|
const synthetic = {
|
|
id: makeUploadId() + '_invalid',
|
|
file: { name: 'upload', size: 0 },
|
|
relativePath: tx('share_upload_selection_unavailable', null, 'Selection unavailable'),
|
|
status: 'error',
|
|
progress: 0
|
|
};
|
|
const errMsg = tx('share_upload_selection_read_error', null, 'Could not read selected file.');
|
|
setItemStatus(synthetic, 'error', 0, errMsg);
|
|
showDropUploadError(errMsg, 0);
|
|
return;
|
|
}
|
|
|
|
runQueue();
|
|
}
|
|
|
|
function filesFromInputList(list) {
|
|
return Array.from(list || []).map((file) => ({
|
|
file,
|
|
relativePath: file.webkitRelativePath || file.name
|
|
}));
|
|
}
|
|
|
|
function readAllDirectoryEntries(reader) {
|
|
return new Promise((resolve) => {
|
|
const entries = [];
|
|
const readBatch = function () {
|
|
reader.readEntries(function (batch) {
|
|
if (!batch || !batch.length) {
|
|
resolve(entries);
|
|
return;
|
|
}
|
|
for (let i = 0; i < batch.length; i++) {
|
|
entries.push(batch[i]);
|
|
}
|
|
readBatch();
|
|
});
|
|
};
|
|
readBatch();
|
|
});
|
|
}
|
|
|
|
async function walkEntry(entry, prefix) {
|
|
if (!entry) return [];
|
|
|
|
if (entry.isFile) {
|
|
return new Promise((resolve) => {
|
|
entry.file(function (file) {
|
|
const rel = prefix ? (prefix + file.name) : file.name;
|
|
resolve([{ file, relativePath: rel }]);
|
|
}, function () {
|
|
resolve([]);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!entry.isDirectory) {
|
|
return [];
|
|
}
|
|
|
|
const reader = entry.createReader();
|
|
const children = await readAllDirectoryEntries(reader);
|
|
let out = [];
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i];
|
|
const childPrefix = prefix + entry.name + '/';
|
|
const nested = await walkEntry(child, childPrefix);
|
|
out = out.concat(nested);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function filesFromDropEvent(e) {
|
|
const dt = e.dataTransfer;
|
|
if (!dt) return [];
|
|
|
|
if (dt.items && dt.items.length && typeof dt.items[0].webkitGetAsEntry === 'function') {
|
|
let out = [];
|
|
const entries = [];
|
|
for (let i = 0; i < dt.items.length; i++) {
|
|
const entry = dt.items[i].webkitGetAsEntry();
|
|
if (entry) entries.push(entry);
|
|
}
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const entry = entries[i];
|
|
if (entry.isFile) {
|
|
const file = dt.items[i].getAsFile ? dt.items[i].getAsFile() : null;
|
|
if (file) {
|
|
out.push({ file, relativePath: file.name });
|
|
}
|
|
continue;
|
|
}
|
|
const nested = await walkEntry(entry, '');
|
|
out = out.concat(nested);
|
|
}
|
|
if (out.length) return out;
|
|
}
|
|
|
|
return filesFromInputList(dt.files || []);
|
|
}
|
|
|
|
if (chooseFilesBtn) {
|
|
chooseFilesBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
fileInput.click();
|
|
});
|
|
}
|
|
|
|
if (chooseFolderBtn) {
|
|
chooseFolderBtn.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
folderInput.click();
|
|
});
|
|
}
|
|
|
|
fileInput.addEventListener('change', function (e) {
|
|
const items = filesFromInputList(e.target.files || []);
|
|
enqueueFiles(items);
|
|
fileInput.value = '';
|
|
});
|
|
|
|
folderInput.addEventListener('change', function (e) {
|
|
const items = filesFromInputList(e.target.files || []);
|
|
enqueueFiles(items);
|
|
folderInput.value = '';
|
|
});
|
|
|
|
['dragenter', 'dragover'].forEach(function (name) {
|
|
dropzone.addEventListener(name, function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.add('is-dragover');
|
|
});
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(function (name) {
|
|
dropzone.addEventListener(name, function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.remove('is-dragover');
|
|
});
|
|
});
|
|
|
|
dropzone.addEventListener('drop', async function (e) {
|
|
const items = await filesFromDropEvent(e);
|
|
enqueueFiles(items);
|
|
});
|
|
|
|
dropzone.addEventListener('click', function () {
|
|
fileInput.click();
|
|
});
|
|
|
|
dropzone.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
fileInput.click();
|
|
}
|
|
});
|
|
|
|
renderBreadcrumbs();
|
|
renderRules();
|
|
});
|