mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
refactor: session-based incremental TFE upload via HTMX, drop SortableJS
Replace the client-side FileArray + Sortable drag-to-reorder with a
server-side session-based upload flow:
- New endpoints: /partage/upload-tfe-file, /partage/remove-tfe-file
(and /admin/ variants) — single-file incremental upload via HTMX
multipart/form-data with progress bar support
- Session storage: uploaded files go to STORAGE_ROOT/uploads/{session_id}/
with metadata in $_SESSION['tfe_uploads']
- file-upload-queue.js reduced to single-file previews only (couverture,
note_intention, annexes thumbnails)
- ThesisFileHandler gains handleTfeFilesFromSession + writeTfeFileFromSrc
+ cleanupSessionUploads for final commit from session temp
- Sortable.min.js removed from all script tags; drag handles and ghost
CSS removed
- No file_orders[]/file_labels[] hidden field injection needed
- Upload queue survives page refresh (server-owned list)
This eliminates the SortableJS dependency entirely while keeping the
same UX: pick files, see them in a queue, remove individual files.
This commit is contained in:
@@ -196,7 +196,13 @@ class ThesisCreateController
|
||||
|
||||
$this->handleCoverUpload($thesisId, $files['couverture'] ?? null, $folderPath, $filePrefix);
|
||||
$this->handleNoteIntentionUpload($thesisId, $files['note_intention'] ?? null, $folderPath, $filePrefix);
|
||||
$nextNum = $this->handleTfeFiles($thesisId, $files['files'] ?? null, $folderPath, $filePrefix, $post, 1);
|
||||
|
||||
// TFE files come from session temp (incremental upload via HTMX)
|
||||
$sessionUploads = $_SESSION['tfe_uploads'] ?? [];
|
||||
$nextNum = $this->handleTfeFilesFromSession($thesisId, $sessionUploads, $folderPath, $filePrefix, 1);
|
||||
// Clear session uploads after successful commit
|
||||
$this->cleanupSessionUploads();
|
||||
|
||||
$this->handleAnnexeFiles($thesisId, $files['annexes'] ?? null, $folderPath, $filePrefix, $post);
|
||||
// PeerTube file rows don't go on disk, but the uploads themselves are processed separately
|
||||
|
||||
|
||||
@@ -411,8 +411,9 @@ class ThesisEditController
|
||||
}
|
||||
}
|
||||
|
||||
// ── New TFE files upload ─────────────────────────────────────────────
|
||||
if (!empty($files['files']['name'][0])) {
|
||||
// ── New TFE files upload (from session via HTMX incremental upload) ──
|
||||
$sessionUploads = $_SESSION['tfe_uploads'] ?? [];
|
||||
if (!empty($sessionUploads)) {
|
||||
// Count existing TFE files to determine starting number
|
||||
$tfeCount = 0;
|
||||
foreach ($existingFiles as $f) {
|
||||
@@ -420,9 +421,9 @@ class ThesisEditController
|
||||
&& !str_starts_with($f['file_path'] ?? '', 'http')) {
|
||||
$tfeCount++;
|
||||
}
|
||||
// Don't count captions as separate TFE entries — they'll be renumbered
|
||||
}
|
||||
$this->handleTfeFiles($thesisId, $files['files'], $folderPath, $filePrefix, $post, $tfeCount + 1);
|
||||
$this->handleTfeFilesFromSession($thesisId, $sessionUploads, $folderPath, $filePrefix, $tfeCount + 1);
|
||||
$this->cleanupSessionUploads();
|
||||
}
|
||||
|
||||
// ── New annexe files upload ────────────────────────────────────────────
|
||||
|
||||
@@ -303,6 +303,102 @@ trait ThesisFileHandler
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process TFE file uploads from session-stored temp paths.
|
||||
*
|
||||
* Used when files are uploaded incrementally via HTMX fragments (upload-tfe-file.php)
|
||||
* rather than submitted in a single multipart form.
|
||||
*
|
||||
* @param int $thesisId
|
||||
* @param array $uploads Array of ['orig_name', 'size', 'mime', 'tmp_path']
|
||||
* @param string $folderPath
|
||||
* @param string $filePrefix
|
||||
* @param int $startNum
|
||||
*/
|
||||
protected function handleTfeFilesFromSession(int $thesisId, array $uploads, string $folderPath, string $filePrefix, int $startNum = 1): int
|
||||
{
|
||||
if (empty($uploads)) {
|
||||
return $startNum;
|
||||
}
|
||||
|
||||
$dir = STORAGE_ROOT . '/' . $folderPath;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$files = [];
|
||||
foreach ($uploads as $f) {
|
||||
$mimeType = $f['mime'];
|
||||
$absPath = STORAGE_ROOT . '/' . $f['tmp_path'];
|
||||
|
||||
if (!file_exists($absPath)) {
|
||||
error_log("ThesisFileHandler: session temp file missing {$f['tmp_path']}, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($f['orig_name'], PATHINFO_EXTENSION));
|
||||
|
||||
if ($mimeType === 'text/plain' && $ext === 'vtt') {
|
||||
$mimeType = 'text/vtt';
|
||||
}
|
||||
|
||||
$files[] = [
|
||||
'mimeType' => $mimeType,
|
||||
'ext' => $ext,
|
||||
'size' => $f['size'],
|
||||
'origName' => $f['orig_name'],
|
||||
'label' => '',
|
||||
'sortOrder' => null,
|
||||
'hierarchy' => $this->tfeHierarchyRank($mimeType, $ext),
|
||||
'fileType' => $this->detectFileType($mimeType, $ext),
|
||||
// Pass the absolute path so writeTfeFile knows where to copy from
|
||||
'srcPath' => $absPath,
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by hierarchy rank
|
||||
usort($files, fn($a, $b) => $a['hierarchy'] - $b['hierarchy']);
|
||||
|
||||
$videoCount = 0;
|
||||
$vttQueue = [];
|
||||
|
||||
foreach ($files as $f) {
|
||||
if ($f['fileType'] === 'video') {
|
||||
$videoCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$num = $startNum;
|
||||
|
||||
foreach ($files as $f) {
|
||||
if ($f['fileType'] === 'caption') {
|
||||
$vttQueue[] = $f;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($f['fileType'] === 'video') {
|
||||
$this->writeTfeFileFromSrc($f, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
|
||||
if (!empty($vttQueue)) {
|
||||
$vtt = array_shift($vttQueue);
|
||||
$this->writeTfeFileFromSrc($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
}
|
||||
} else {
|
||||
$this->writeTfeFileFromSrc($f, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($vttQueue as $vtt) {
|
||||
$this->writeTfeFileFromSrc($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
}
|
||||
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process annex file uploads.
|
||||
*
|
||||
@@ -421,6 +517,61 @@ trait ThesisFileHandler
|
||||
error_log("ThesisFileHandler: TFE uploaded → $targetName ({$f['fileType']})");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a single TFE file from a source path (session temp) to the thesis folder.
|
||||
* Used by handleTfeFilesFromSession instead of move_uploaded_file.
|
||||
*/
|
||||
protected function writeTfeFileFromSrc(array $f, int $thesisId, string $dir, string $folderPath, string $filePrefix, int $num): void
|
||||
{
|
||||
$padded = sprintf('%02d', $num);
|
||||
$targetName = $filePrefix . '_TFE_' . $padded . '.' . $f['ext'];
|
||||
$targetPath = $dir . $targetName;
|
||||
|
||||
if (!rename($f['srcPath'], $targetPath)) {
|
||||
// Fallback: copy + unlink
|
||||
if (!copy($f['srcPath'], $targetPath)) {
|
||||
error_log("ThesisFileHandler: failed to move session TFE {$f['origName']}");
|
||||
return;
|
||||
}
|
||||
unlink($f['srcPath']);
|
||||
}
|
||||
|
||||
chmod($targetPath, 0644);
|
||||
$relPath = $folderPath . $targetName;
|
||||
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId, $f['fileType'],
|
||||
$relPath,
|
||||
basename($f['origName']),
|
||||
$f['size'],
|
||||
$f['mimeType'],
|
||||
$f['label'] !== '' ? $f['label'] : null,
|
||||
$f['sortOrder']
|
||||
);
|
||||
error_log("ThesisFileHandler: TFE (session) moved → $targetName ({$f['fileType']})");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up session upload temp files and clear the session entry.
|
||||
* Call after successful commit of TFE files.
|
||||
*/
|
||||
protected function cleanupSessionUploads(): void
|
||||
{
|
||||
$sessionId = session_id();
|
||||
$tempDir = STORAGE_ROOT . '/uploads/' . $sessionId;
|
||||
|
||||
// Remove any remaining files in the temp dir
|
||||
if (is_dir($tempDir)) {
|
||||
$files = glob($tempDir . '/*');
|
||||
foreach ($files as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
@rmdir($tempDir);
|
||||
}
|
||||
|
||||
unset($_SESSION['tfe_uploads']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a hierarchy rank for sorting TFE files.
|
||||
* Lower = earlier in the sequence.
|
||||
|
||||
@@ -17,8 +17,18 @@ $adminMode = ($_POST['admin_mode'] ?? '0') === '1';
|
||||
$fieldName = $_POST['field_name'] ?? '';
|
||||
|
||||
// Read file from the field-name-specific key (e.g., $_FILES['couverture'], $_FILES['annexes'])
|
||||
// For multi-file inputs (name ends with []), the first file is validated.
|
||||
// Fall back to the first file in $_FILES if the specific key is empty
|
||||
// (handles PeerTube inputs where name differs from field_name).
|
||||
$rawFile = $_FILES[$fieldName] ?? null;
|
||||
if (!$rawFile || empty($rawFile['name'])) {
|
||||
// Try any uploaded file
|
||||
foreach ($_FILES as $v) {
|
||||
if (!empty($v['name'])) {
|
||||
$rawFile = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($rawFile && is_array($rawFile['name'] ?? null)) {
|
||||
// Multi-file: flatten first entry
|
||||
$file = [
|
||||
|
||||
Reference in New Issue
Block a user