should fix our issues?

This commit is contained in:
notCharles 2025-11-08 16:41:39 -05:00
parent 339efc754d
commit 227521b7bb
2 changed files with 364 additions and 305 deletions

View File

@ -1,279 +1,308 @@
<div <div
x-data="{ x-data="{
isUploading: false, isUploading: false,
uploadQueue: [], uploadQueue: [],
currentFileIndex: 0, currentFileIndex: 0,
totalFiles: 0, totalFiles: 0,
autoCloseTimer: 1000, autoCloseTimer: 1000,
async extractFilesFromItems(items) { async extractFilesFromItems(items) {
const filesWithPaths = []; const filesWithPaths = [];
const traversePromises = []; const traversePromises = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const entry = items[i].webkitGetAsEntry?.(); const entry = items[i].webkitGetAsEntry?.();
if (entry) { if (entry) {
traversePromises.push(this.traverseFileTree(entry, '', filesWithPaths)); traversePromises.push(this.traverseFileTree(entry, '', filesWithPaths));
} else if (items[i].kind === 'file') { } else if (items[i].kind === 'file') {
const file = items[i].getAsFile(); const file = items[i].getAsFile();
if (file) { if (file) {
filesWithPaths.push({ filesWithPaths.push({
file: file, file: file,
path: '', path: '',
}); });
}
}
} }
}
}
await Promise.all(traversePromises); await Promise.all(traversePromises);
return filesWithPaths; return filesWithPaths;
}, },
async traverseFileTree(entry, path, filesWithPaths) { async traverseFileTree(entry, path, filesWithPaths) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (entry.isFile) { if (entry.isFile) {
entry.file((file) => { entry.file((file) => {
filesWithPaths.push({ filesWithPaths.push({
file: file, file: file,
path: path, path: path,
}); });
resolve(); resolve();
});
} else if (entry.isDirectory) {
const reader = entry.createReader();
const readEntries = () => {
reader.readEntries(async (entries) => {
if (entries.length === 0) {
resolve();
return;
}
const subPromises = entries.map((e) =>
this.traverseFileTree(
e,
path ? `${path}/${entry.name}` : entry.name,
filesWithPaths
)
);
await Promise.all(subPromises);
readEntries();
});
};
readEntries();
} else {
resolve();
}
}); });
}, } else if (entry.isDirectory) {
const reader = entry.createReader();
async uploadFilesWithFolders(filesWithPaths) { const readEntries = () => {
this.isUploading = true; reader.readEntries(async (entries) => {
this.uploadQueue = []; if (entries.length === 0) {
this.totalFiles = filesWithPaths.length; resolve();
this.currentFileIndex = 0;
const uploadedFiles = [];
try {
const uploadSizeLimit = await $wire.getUploadSizeLimit();
for (const { file } of filesWithPaths) {
if (file.size > uploadSizeLimit) {
new window.FilamentNotification()
.title(`File ${file.name} exceeds the upload size limit of ${this.formatBytes(uploadSizeLimit)}`)
.danger()
.send();
this.isUploading = false;
return; return;
} }
const subPromises = entries.map((e) =>
this.traverseFileTree(
e,
path ? `${path}/${entry.name}` : entry.name,
filesWithPaths
)
);
await Promise.all(subPromises);
readEntries();
});
};
readEntries();
} else {
resolve();
}
});
},
async uploadFilesWithFolders(filesWithPaths) {
this.isUploading = true;
this.uploadQueue = [];
this.totalFiles = filesWithPaths.length;
this.currentFileIndex = 0;
const uploadedFiles = [];
try {
const uploadSizeLimit = await $wire.getUploadSizeLimit();
for (const {
file
}
of filesWithPaths) {
if (file.size > uploadSizeLimit) {
new window.FilamentNotification()
.title(`File ${file.name} exceeds the upload size limit of ${this.formatBytes(uploadSizeLimit)}`)
.danger()
.send();
this.isUploading = false;
return;
}
}
const folderPaths = new Set();
for (const {
path
}
of filesWithPaths) {
if (path) {
const parts = path.split('/').filter(Boolean);
let currentPath = '';
for (const part of parts) {
currentPath += part + '/';
folderPaths.add(currentPath);
} }
}
}
const folderPaths = new Set(); for (const folderPath of folderPaths) {
for (const { path } of filesWithPaths) { try {
if (path) { await $wire.createFolder(folderPath.slice(0, -1));
const parts = path.split('/').filter(Boolean); } catch (error) {
let currentPath = ''; console.warn(`Folder ${folderPath} already exists or failed to create.`);
for (const part of parts) { }
currentPath += part + '/'; }
folderPaths.add(currentPath);
}
}
}
for (const folderPath of folderPaths) { for (const f of filesWithPaths) {
try { this.uploadQueue.push({
await $wire.createFolder(folderPath.slice(0, -1)); file: f.file,
} catch (error) { name: f.file.name,
console.warn(`Folder ${folderPath} already exists or failed to create.`); path: f.path,
} size: f.file.size,
} progress: 0,
speed: 0,
uploadedBytes: 0,
status: 'pending',
error: null
});
}
for (const f of filesWithPaths) { const maxConcurrent = 3;
this.uploadQueue.push({ let activeUploads = [];
file: f.file, let completedCount = 0;
name: f.file.name,
path: f.path,
size: f.file.size,
progress: 0,
speed: 0,
uploadedBytes: 0,
status: 'pending',
error: null
});
}
const maxConcurrent = 3; for (let i = 0; i < this.uploadQueue.length; i++) {
let activeUploads = []; const uploadPromise = this.uploadFile(i)
let completedCount = 0; .then(() => {
completedCount++;
this.currentFileIndex = completedCount;
const item = this.uploadQueue[i];
const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
uploadedFiles.push(relativePath);
})
.catch(() => {
completedCount++;
this.currentFileIndex = completedCount;
});
for (let i = 0; i < this.uploadQueue.length; i++) { activeUploads.push(uploadPromise);
const uploadPromise = this.uploadFile(i)
.then(() => { completedCount++; this.currentFileIndex = completedCount;
const item = this.uploadQueue[i];
const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
uploadedFiles.push(relativePath);
})
.catch(() => { completedCount++; this.currentFileIndex = completedCount; });
activeUploads.push(uploadPromise); if (activeUploads.length >= maxConcurrent) {
await Promise.race(activeUploads);
activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
}
}
if (activeUploads.length >= maxConcurrent) { await Promise.allSettled(activeUploads);
await Promise.race(activeUploads);
activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
}
}
await Promise.allSettled(activeUploads); const failed = this.uploadQueue.filter(f => f.status === 'error');
await $wire.$refresh();
const failed = this.uploadQueue.filter(f => f.status === 'error');
await $wire.$refresh();
if (failed.length === 0) { if (failed.length === 0) {
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.success') }}').success().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.success') }}')
.success()
.send();
} else if (failed.length === this.totalFiles) { } else if (failed.length === this.totalFiles) {
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.error_all') }}').danger().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.failed') }}')
.danger()
.send();
} else { } else {
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.error_partial') }}').warning().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.failed') }}')
.danger()
.send();
} }
if (uploadedFiles.length > 0) { if (uploadedFiles.length > 0) {
this.$nextTick(() => { this.$nextTick(() => {
try { if (typeof $wire !== 'undefined' && $wire && typeof $wire.call === 'function') {
@this.call('logUploadedFiles', uploadedFiles); $wire.call('logUploadedFiles', uploadedFiles);
} catch (e) { } else if (typeof window.livewire !== 'undefined' && typeof window.livewire.call === 'function') {
$wire.call('logUploadedFiles', uploadedFiles); window.livewire.call('logUploadedFiles', uploadedFiles);
} } else if (typeof Livewire !== 'undefined' && typeof Livewire.call === 'function') {
}); Livewire.call('logUploadedFiles', uploadedFiles);
} else {
console.warn('Could not call Livewire method logUploadedFiles; Livewire not found.');
} }
});
}
if (this.autoCloseTimer) clearTimeout(this.autoCloseTimer); if (this.autoCloseTimer) clearTimeout(this.autoCloseTimer);
this.autoCloseTimer = setTimeout(() => { this.autoCloseTimer = setTimeout(() => {
this.isUploading = false; this.isUploading = false;
this.uploadQueue = []; this.uploadQueue = [];
},1000); }, 1000);
} catch (error) { } catch (error) {
console.error('Upload error:', error); console.error('Upload error:', error);
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.error') }}').danger().send(); new window.FilamentNotification()
this.isUploading = false; .title('{{ trans('server/file.actions.upload.error') }}')
} .danger()
}, .send();
this.isUploading = false;
}
},
async uploadFile(index) { async uploadFile(index) {
const fileData = this.uploadQueue[index]; const fileData = this.uploadQueue[index];
fileData.status = 'uploading'; fileData.status = 'uploading';
try { try {
const uploadUrl = await $wire.getUploadUrl(); const uploadUrl = await $wire.getUploadUrl();
const url = new URL(uploadUrl); const url = new URL(uploadUrl);
let basePath = @js($this->path); let basePath = @js($this->path);
if (fileData.path && fileData.path.trim() !== '') { if (fileData.path && fileData.path.trim() !== '') {
basePath = basePath.replace(/\/+$/, '') + '/' + fileData.path.replace(/^\/+/, ''); basePath = basePath.replace(/\/+$/, '') + '/' + fileData.path.replace(/^\/+/, '');
}
url.searchParams.append('directory', basePath);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('files', fileData.file);
let lastLoaded = 0;
let lastTime = Date.now();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
fileData.uploadedBytes = e.loaded;
fileData.progress = Math.round((e.loaded / e.total) * 100);
const now = Date.now();
const timeDiff = (now - lastTime) / 1000;
if (timeDiff > 0.1) {
const bytesDiff = e.loaded - lastLoaded;
fileData.speed = bytesDiff / timeDiff;
lastTime = now;
lastLoaded = e.loaded;
}
} }
});
url.searchParams.append('directory', basePath); xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
fileData.status = 'complete';
fileData.progress = 100;
resolve();
} else {
fileData.status = 'error';
fileData.error = `Upload failed (${xhr.status})`;
reject(new Error(fileData.error));
}
};
return new Promise((resolve, reject) => { xhr.onerror = () => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('files', fileData.file);
let lastLoaded = 0;
let lastTime = Date.now();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
fileData.uploadedBytes = e.loaded;
fileData.progress = Math.round((e.loaded / e.total) * 100);
const now = Date.now();
const timeDiff = (now - lastTime) / 1000;
if (timeDiff > 0.1) {
const bytesDiff = e.loaded - lastLoaded;
fileData.speed = bytesDiff / timeDiff;
lastTime = now;
lastLoaded = e.loaded;
}
}
});
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
fileData.status = 'complete';
fileData.progress = 100;
resolve();
} else {
fileData.status = 'error';
fileData.error = `Upload failed (${xhr.status})`;
reject(new Error(fileData.error));
}
};
xhr.onerror = () => {
fileData.status = 'error';
fileData.error = 'Network error';
reject(new Error('Network error'));
};
xhr.open('POST', url.toString());
xhr.send(formData);
});
} catch (err) {
fileData.status = 'error'; fileData.status = 'error';
fileData.error = 'Failed to get upload token'; fileData.error = 'Network error';
throw err; reject(new Error('Network error'));
} };
},
formatBytes(bytes) { xhr.open('POST', url.toString());
if (bytes === 0) return '0.00 B'; xhr.send(formData);
const k = 1024; });
const sizes = ['B', 'KB', 'MB', 'GB']; } catch (err) {
const i = Math.floor(Math.log(bytes) / Math.log(k)); fileData.status = 'error';
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; fileData.error = 'Failed to get upload token';
}, throw err;
formatSpeed(bytesPerSecond) { }
return this.formatBytes(bytesPerSecond) + '/s'; },
},
handleEscapeKey(e) { formatBytes(bytes) {
if (e.key === 'Escape' && this.isUploading) { if (bytes === 0) return '0.00 B';
this.isUploading = false; const k = 1024;
this.uploadQueue = []; const sizes = ['B', 'KB', 'MB', 'GB'];
} const i = Math.floor(Math.log(bytes) / Math.log(k));
}, return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
async handleFileSelect(e) { },
const files = Array.from(e.target.files); formatSpeed(bytesPerSecond) {
if (files.length > 0) { return this.formatBytes(bytesPerSecond) + '/s';
const filesWithPaths = files.map(f => ({ file: f, path: '' })); },
await this.uploadFilesWithFolders(filesWithPaths); handleEscapeKey(e) {
} if (e.key === 'Escape' && this.isUploading) {
}, this.isUploading = false;
triggerBrowse() { this.uploadQueue = [];
this.$refs.fileInput.click(); }
}, },
}" async handleFileSelect(e) {
class="relative" const files = Array.from(e.target.files);
> if (files.length > 0) {
const filesWithPaths = files.map(f => ({
file: f,
path: ''
}));
await this.uploadFilesWithFolders(filesWithPaths);
}
},
triggerBrowse() {
this.$refs.fileInput.click();
},
}"
class="relative">
<div class="p-4"> <div class="p-4">
<input type="file" x-ref="fileInput" class="hidden" multiple @change="handleFileSelect"> <input type="file" x-ref="fileInput" class="hidden" multiple @change="handleFileSelect">

View File

@ -44,7 +44,10 @@
} }
if (files && files.length > 0 && filesWithPaths.length === 0) { if (files && files.length > 0 && filesWithPaths.length === 0) {
filesWithPaths = Array.from(files).map(f => ({ file: f, path: '' })); filesWithPaths = Array.from(files).map(f => ({
file: f,
path: ''
}));
} }
if (filesWithPaths.length > 0) { if (filesWithPaths.length > 0) {
@ -53,67 +56,67 @@
}, },
async extractFilesFromItems(items) { async extractFilesFromItems(items) {
const filesWithPaths = []; const filesWithPaths = [];
const traversePromises = []; const traversePromises = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const entry = items[i].webkitGetAsEntry?.(); const entry = items[i].webkitGetAsEntry?.();
if (entry) { if (entry) {
traversePromises.push(this.traverseFileTree(entry, '', filesWithPaths)); traversePromises.push(this.traverseFileTree(entry, '', filesWithPaths));
} else if (items[i].kind === 'file') { } else if (items[i].kind === 'file') {
const file = items[i].getAsFile(); const file = items[i].getAsFile();
if (file) { if (file) {
filesWithPaths.push({ filesWithPaths.push({
file: file, file: file,
path: '', path: '',
}); });
}
} }
} }
}
await Promise.all(traversePromises); await Promise.all(traversePromises);
return filesWithPaths; return filesWithPaths;
}, },
async traverseFileTree(entry, path, filesWithPaths) { async traverseFileTree(entry, path, filesWithPaths) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (entry.isFile) { if (entry.isFile) {
entry.file((file) => { entry.file((file) => {
filesWithPaths.push({ filesWithPaths.push({
file: file, file: file,
path: path, path: path,
});
resolve();
}); });
} else if (entry.isDirectory) {
const reader = entry.createReader();
const readEntries = () => {
reader.readEntries(async (entries) => {
if (entries.length === 0) {
resolve();
return;
}
const subPromises = entries.map((e) =>
this.traverseFileTree(
e,
path ? `${path}/${entry.name}` : entry.name,
filesWithPaths
)
);
await Promise.all(subPromises);
readEntries();
});
};
readEntries();
} else {
resolve(); resolve();
}); }
} else if (entry.isDirectory) { });
const reader = entry.createReader(); },
const readEntries = () => {
reader.readEntries(async (entries) => {
if (entries.length === 0) {
resolve();
return;
}
const subPromises = entries.map((e) =>
this.traverseFileTree(
e,
path ? `${path}/${entry.name}` : entry.name,
filesWithPaths
)
);
await Promise.all(subPromises);
readEntries();
});
};
readEntries();
} else {
resolve();
}
});
},
async uploadFilesWithFolders(filesWithPaths) { async uploadFilesWithFolders(filesWithPaths) {
this.isUploading = true; this.isUploading = true;
this.uploadQueue = []; this.uploadQueue = [];
@ -124,7 +127,10 @@
try { try {
const uploadSizeLimit = await $wire.getUploadSizeLimit(); const uploadSizeLimit = await $wire.getUploadSizeLimit();
for (const { file } of filesWithPaths) { for (const {
file
}
of filesWithPaths) {
if (file.size > uploadSizeLimit) { if (file.size > uploadSizeLimit) {
new window.FilamentNotification() new window.FilamentNotification()
.title(`File ${file.name} exceeds the upload limit.`) .title(`File ${file.name} exceeds the upload limit.`)
@ -136,7 +142,10 @@
} }
const folderPaths = new Set(); const folderPaths = new Set();
for (const { path } of filesWithPaths) { for (const {
path
}
of filesWithPaths) {
if (path) { if (path) {
const parts = path.split('/').filter(Boolean); const parts = path.split('/').filter(Boolean);
let currentPath = ''; let currentPath = '';
@ -175,12 +184,17 @@
for (let i = 0; i < this.uploadQueue.length; i++) { for (let i = 0; i < this.uploadQueue.length; i++) {
const uploadPromise = this.uploadFile(i) const uploadPromise = this.uploadFile(i)
.then(() => { completedCount++; this.currentFileIndex = completedCount; .then(() => {
completedCount++;
this.currentFileIndex = completedCount;
const item = this.uploadQueue[i]; const item = this.uploadQueue[i];
const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name; const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
uploadedFiles.push(relativePath); uploadedFiles.push(relativePath);
}) })
.catch(() => { completedCount++; this.currentFileIndex = completedCount; }); .catch(() => {
completedCount++;
this.currentFileIndex = completedCount;
});
activeUploads.push(uploadPromise); activeUploads.push(uploadPromise);
@ -196,19 +210,32 @@
await $wire.$refresh(); await $wire.$refresh();
if (failed.length === 0) { if (failed.length === 0) {
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.success') }}').success().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.success') }}')
.success()
.send();
} else if (failed.length === this.totalFiles) { } else if (failed.length === this.totalFiles) {
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.failed') }}').danger().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.failed') }}')
.danger()
.send();
} else { } else {
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.failed') }}').danger().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.failed') }}')
.danger()
.send();
} }
if (uploadedFiles.length > 0) { if (uploadedFiles.length > 0) {
this.$nextTick(() => { this.$nextTick(() => {
try { if (typeof $wire !== 'undefined' && $wire && typeof $wire.call === 'function') {
@this.call('logUploadedFiles', uploadedFiles);
} catch (e) {
$wire.call('logUploadedFiles', uploadedFiles); $wire.call('logUploadedFiles', uploadedFiles);
} else if (typeof window.livewire !== 'undefined' && typeof window.livewire.call === 'function') {
window.livewire.call('logUploadedFiles', uploadedFiles);
} else if (typeof Livewire !== 'undefined' && typeof Livewire.call === 'function') {
Livewire.call('logUploadedFiles', uploadedFiles);
} else {
console.warn('Could not call Livewire method logUploadedFiles; Livewire not found.');
} }
}); });
} }
@ -217,10 +244,13 @@
this.autoCloseTimer = setTimeout(() => { this.autoCloseTimer = setTimeout(() => {
this.isUploading = false; this.isUploading = false;
this.uploadQueue = []; this.uploadQueue = [];
},1000); }, 1000);
} catch (error) { } catch (error) {
console.error('Upload error:', error); console.error('Upload error:', error);
new window.FilamentNotification().title('{{ trans('server/file.actions.upload.error') }}').danger().send(); new window.FilamentNotification()
.title('{{ trans('server/file.actions.upload.error') }}')
.danger()
.send();
this.isUploading = false; this.isUploading = false;
} }
}, },