mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: fix file deletion on save + trash policy + documents/ prefix + relink browser
1. note_intention: Delete old file only when a genuinely new upload arrives
(32-char hex file_id), not when the FilePond pool preserves an existing
file by sending its DB integer ID. Previously the DB integer ID
triggered $hasNewNote=true, which deleted the existing note_intention
from disk+DB, then handleFilePondSingleFile couldn't re-process it
because the regex requires a hex pattern. Same fix applied to cover.
2. All file deletions now use deleteThesisFileToTrash() which renames
files to tmp/_trash/ instead of unlinking. The trash preserves
original filenames prefixed with DB id for traceability. Skips
website URLs and PeerTube refs (no disk file).
3. Storage prefix changed from theses/ to documents/ to reflect that
the folder holds all document types (determined by file_type in DB).
MediaController visibility gate supports both prefixes for backward
compat with existing files.
4. File browser + relink feature for orphaned files:
- /admin/fragments/file-browser.php — HTMX tree browser for
storage/documents/ and storage/theses/
- /admin/actions/filepond/relink.php — POST endpoint that inserts
a thesis_files row pointing to existing on-disk file
- Per-pool "📂 Relier" buttons (edit mode only)
- JS: XamxamOpenFileBrowser / XamxamRelinkFile with FilePond integration
- CSS: .relink-modal dialog + .file-browser tree styles
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* Shared file-upload logic used by ThesisCreateController and
|
||||
* ThesisEditController. All on-disk files are stored under:
|
||||
*
|
||||
* theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
|
||||
* documents/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
|
||||
*
|
||||
* Filenames follow:
|
||||
*
|
||||
@@ -88,7 +88,7 @@ trait ThesisFileHandler
|
||||
*
|
||||
* @param int $thesisId
|
||||
* @param array|null $upload Single-file $_FILES entry.
|
||||
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "theses/2025/2025_SMITH_Mon_Titre/").
|
||||
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "documents/2025/2025_SMITH_Mon_Titre/").
|
||||
* @param string $filePrefix The prefix shared by all files in this folder (e.g. "2025_SMITH_Mon_Titre").
|
||||
*/
|
||||
protected function handleCoverUpload(int $thesisId, ?array $upload, string $folderPath, string $filePrefix): void
|
||||
@@ -802,7 +802,7 @@ trait ThesisFileHandler
|
||||
/**
|
||||
* Build the folder path and file prefix for a thesis.
|
||||
*
|
||||
* Folder: theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
|
||||
* Folder: documents/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
|
||||
* Prefix: {YYYY}_{AUTHORS}_{TITLE_SLUG}
|
||||
*
|
||||
* @return array{folderPath: string, filePrefix: string, folderName: string}
|
||||
@@ -814,7 +814,7 @@ trait ThesisFileHandler
|
||||
|
||||
$folderName = $year . '_' . $authorSlug . '_' . $titleSlug;
|
||||
$filePrefix = $folderName;
|
||||
$folderPath = 'theses/' . $year . '/' . $folderName . '/';
|
||||
$folderPath = 'documents/' . $year . '/' . $folderName . '/';
|
||||
|
||||
return [
|
||||
'folderPath' => $folderPath,
|
||||
@@ -1203,4 +1203,55 @@ trait ThesisFileHandler
|
||||
}
|
||||
@rmdir($tmpDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a thesis_files row, moving the on-disk file to the trash directory
|
||||
* instead of unlinking it. Website URLs and PeerTube references are only
|
||||
* removed from the database (no disk file).
|
||||
*/
|
||||
protected function deleteThesisFileToTrash(int $fileId, int $thesisId): void
|
||||
{
|
||||
if ($fileId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the file row to get the path before deleting the DB record
|
||||
$existingFiles = $this->db->getThesisFiles($thesisId);
|
||||
$fileRow = null;
|
||||
foreach ($existingFiles as $f) {
|
||||
if ((int)$f['id'] === $fileId) {
|
||||
$fileRow = $f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = $this->db->deleteThesisFile($fileId, $thesisId);
|
||||
|
||||
if ($filePath && defined('STORAGE_ROOT')) {
|
||||
// Skip filesystem for website URLs and PeerTube IDs (not real files)
|
||||
if (str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://')) {
|
||||
return;
|
||||
}
|
||||
if (str_starts_with($filePath, 'peertube_ids:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$abs = STORAGE_ROOT . '/' . $filePath;
|
||||
if (file_exists($abs)) {
|
||||
$trashDir = STORAGE_ROOT . '/tmp/_trash';
|
||||
if (!is_dir($trashDir)) {
|
||||
mkdir($trashDir, 0755, true);
|
||||
}
|
||||
// Keep original filename structure for traceability
|
||||
$trashName = $fileId . '_' . basename($filePath);
|
||||
$trashPath = $trashDir . '/' . $trashName;
|
||||
if (!rename($abs, $trashPath)) {
|
||||
// Fallback to unlink if rename fails (cross-device)
|
||||
@copy($abs, $trashPath);
|
||||
@unlink($abs);
|
||||
}
|
||||
error_log("ThesisFileHandler: file \$fileId moved to trash → \$trashName");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user