diff --git a/extend.php b/extend.php index c3855e44..2c3b2571 100644 --- a/extend.php +++ b/extend.php @@ -19,6 +19,7 @@ use Flarum\Gdpr\Extend\UserData; use Flarum\Post\Event\Posted; use Flarum\Post\Event\Revised; +use Flarum\Post\Post; use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Settings\Event\Deserializing; use Flarum\Settings\SettingsRepositoryInterface; @@ -132,6 +133,8 @@ ->listen(Revised::class, Listeners\LinkFilesToPostOnSave::class) ->listen(WillBeUploaded::class, Listeners\AddImageProcessor::class), + new Extend\ModelObserver(Post::class, Listeners\CleanUpFilesOnPostDelete::class), + (new Extend\Filesystem()) ->disk('private-shared', Extenders\PrivateSharedDiskConfig::class), @@ -162,7 +165,8 @@ ->default('fof-upload.svgAnimateAllowed', false) ->default('fof-upload.generateThumbnails', true) ->default('fof-upload.thumbnailWebp', true) - ->default('fof-upload.thumbnailMaxWidth', 1000), + ->default('fof-upload.thumbnailMaxWidth', 1000) + ->default('fof-upload.deleteFilesOnPostDelete', false), new Extenders\AddPostDownloadTags(), new Extenders\CreateStorageFolder('tmp'), diff --git a/js/src/admin/components/UploadPage.tsx b/js/src/admin/components/UploadPage.tsx index c5908810..27f751ef 100644 --- a/js/src/admin/components/UploadPage.tsx +++ b/js/src/admin/components/UploadPage.tsx @@ -120,6 +120,7 @@ export default class UploadPage extends ExtensionPage { 'addsWatermarks', 'disableHotlinkProtection', 'disableDownloadLogging', + 'deleteFilesOnPostDelete', 'awsS3UsePathStyleEndpoint', 'svgAnimateAllowed', ]; @@ -479,6 +480,13 @@ export default class UploadPage extends ExtensionPage { {app.translator.trans('fof-upload.admin.labels.disable-download-logging.toggle')} + {app.translator.trans('fof-upload.admin.labels.delete-files-on-post-delete.title')} +

{app.translator.trans('fof-upload.admin.help_texts.delete-files-on-post-delete')}

+
+ + {app.translator.trans('fof-upload.admin.labels.delete-files-on-post-delete.toggle')} + +
{!this.uploadLocalCdnSetByEnv && ( diff --git a/resources/locale/en.yml b/resources/locale/en.yml index a624c349..34f2ea90 100644 --- a/resources/locale/en.yml +++ b/resources/locale/en.yml @@ -23,6 +23,10 @@ fof-upload: title: Upload description: Set up uploading services and preferences. help_texts: + delete-files-on-post-delete: | + When enabled, files exclusively attached to a deleted post are immediately removed from storage and the database. + Files referenced by multiple posts are not affected. When disabled (default), orphaned files are only removed + when the cleanup command is run manually: php flarum fof:upload --cleanup disable-download-logging: | Disable logging every download made by users of your forum. Keeping it enabled allows you to view the number of downloads and other metrics in the nearby future. disable-hotlink-protection: | @@ -67,6 +71,9 @@ fof-upload: use_path_style_endpoint: Use path style endpoint acl: Access Control List (ACL) custom_url: Custom S3 URL + delete-files-on-post-delete: + title: File cleanup on post deletion + toggle: Delete uploaded files when the containing post is deleted disable-download-logging: title: Disable logging downloads toggle: Disable diff --git a/src/Listeners/CleanUpFilesOnPostDelete.php b/src/Listeners/CleanUpFilesOnPostDelete.php new file mode 100644 index 00000000..14135a6e --- /dev/null +++ b/src/Listeners/CleanUpFilesOnPostDelete.php @@ -0,0 +1,58 @@ +settings->get('fof-upload.deleteFilesOnPostDelete', false)) { + return; + } + + // Load files now — before the cascade wipes the fof_upload_file_posts rows. + $files = $post->files() + ->where('shared', false) + ->get(); + + foreach ($files as $file) { + // Only delete if this post is the file's sole association. + // posts()->count() is still accurate here because the cascade hasn't run yet. + if ($file->posts()->count() !== 1) { + // File is referenced by other posts — leave it; cascade removes only this pivot row. + continue; + } + + $adapter = $this->manager->instantiate($file->upload_method); + + if ($adapter->delete($file)) { + $file->delete(); + } + // If storage deletion fails we leave the DB record intact — no silent data loss. + } + } +}