}
}
- $deleted = $this->imageService->deleteUnusedImages($checkRevisions, ['gallery', 'drawio'], $dryRun);
+ $deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($deleted);
if ($dryRun) {
<?php namespace BookStack\Http\Controllers;
+use BookStack\Services\ImageService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Setting;
public function index()
{
$this->checkPermission('settings-manage');
- $this->setPageTitle('Settings');
+ $this->setPageTitle(trans('settings.settings'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings');
}
+
+ /**
+ * Show the page for application maintenance.
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function showMaintenance()
+ {
+ $this->checkPermission('settings-manage');
+ $this->setPageTitle(trans('settings.maint'));
+
+ // Get application version
+ $version = trim(file_get_contents(base_path('version')));
+
+ return view('settings/maintenance', ['version' => $version]);
+ }
+
+ /**
+ * Action to clean-up images in the system.
+ * @param Request $request
+ * @param ImageService $imageService
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ */
+ public function cleanupImages(Request $request, ImageService $imageService)
+ {
+ $this->checkPermission('settings-manage');
+
+ $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
+ $dryRun = !($request->has('confirm'));
+
+ $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
+ $deleteCount = count($imagesToDelete);
+ if ($deleteCount === 0) {
+ session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
+ return redirect('/settings/maintenance')->withInput();
+ }
+
+ if ($dryRun) {
+ session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
+ } else {
+ session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
+ }
+
+ return redirect('/settings/maintenance#image-cleanup')->withInput();
+ }
}
*
* Returns the path of the images that would be/have been deleted.
* @param bool $checkRevisions
- * @param array $types
* @param bool $dryRun
+ * @param array $types
* @return array
*/
- public function deleteUnusedImages($checkRevisions = true, $types = ['gallery', 'drawio'], $dryRun = true)
+ public function deleteUnusedImages($checkRevisions = true, $dryRun = true, $types = ['gallery', 'drawio'])
{
$types = array_intersect($types, ['gallery', 'drawio']);
$deletedPaths = [];
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path clip-rule="evenodd" fill="none" d="M0 0h24v24H0z"/>
+ <path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/>
+</svg>
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
+ /**
+ * Maintenance settings
+ */
+
+ 'maint' => 'Maintenance',
+ 'maint_image_cleanup' => 'Cleanup Images',
+ 'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.",
+ 'maint_image_cleanup_ignore_revisions' => 'Ignore images in revisions',
+ 'maint_image_cleanup_run' => 'Run Cleanup',
+ 'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
+ 'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
+ 'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
+
/**
* Role settings
*/
--- /dev/null
+@extends('simple-layout')
+
+@section('toolbar')
+ @include('settings/navbar', ['selected' => 'maintenance'])
+@stop
+
+@section('body')
+<div class="container small">
+
+ <div class="text-right text-muted container">
+ <br>
+ BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
+ </div>
+
+
+ <div class="card" id="image-cleanup">
+ <h3>@icon('images') {{ trans('settings.maint_image_cleanup') }}</h3>
+ <div class="body">
+ <div class="row">
+ <div class="col-sm-6">
+ <p class="small muted">{{ trans('settings.maint_image_cleanup_desc') }}</p>
+ </div>
+ <div class="col-sm-6">
+ <form method="POST" action="{{ baseUrl('/settings/maintenance/cleanup-images') }}">
+ {!! csrf_field() !!}
+ <input type="hidden" name="_method" value="DELETE">
+ <div>
+ @if(session()->has('cleanup-images-warning'))
+ <p class="text neg">
+ {{ session()->get('cleanup-images-warning') }}
+ </p>
+ <input type="hidden" name="ignore_revisions" value="{{ session()->getOldInput('ignore_revisions', 'false') }}">
+ <input type="hidden" name="confirm" value="true">
+ @else
+ <label>
+ <input type="checkbox" name="ignore_revisions" value="true">
+ {{ trans('settings.maint_image_cleanup_ignore_revisions') }}
+ </label>
+ @endif
+ </div>
+ <button class="button outline">{{ trans('settings.maint_image_cleanup_run') }}</button>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+
+</div>
+@stop
<div class="col-md-12 setting-nav nav-tabs">
@if($currentUser->can('settings-manage'))
<a href="{{ baseUrl('/settings') }}" @if($selected == 'settings') class="selected text-button" @endif>@icon('settings'){{ trans('settings.settings') }}</a>
+ <a href="{{ baseUrl('/settings/maintenance') }}" @if($selected == 'maintenance') class="selected text-button" @endif>@icon('spanner'){{ trans('settings.maint') }}</a>
@endif
@if($currentUser->can('users-manage'))
<a href="{{ baseUrl('/settings/users') }}" @if($selected == 'users') class="selected text-button" @endif>@icon('users'){{ trans('settings.users') }}</a>
Route::get('/', 'SettingController@index')->name('settings');
Route::post('/', 'SettingController@update');
+ // Maintenance
+ Route::get('/maintenance', 'SettingController@showMaintenance');
+ Route::delete('/maintenance/cleanup-images', 'SettingController@cleanupImages');
+
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
use BookStack\Image;
use BookStack\Page;
+use BookStack\Repos\EntityRepo;
+use BookStack\Services\ImageService;
class ImageTest extends TestCase
{
]);
}
+ public function test_deleted_unused_images()
+ {
+ $page = Page::first();
+ $admin = $this->getAdmin();
+ $this->actingAs($admin);
+
+ $imageName = 'unused-image.png';
+ $relPath = $this->getTestImagePath('gallery', $imageName);
+ $this->deleteImage($relPath);
+
+ $upload = $this->uploadImage($imageName, $page->id);
+ $upload->assertStatus(200);
+ $image = Image::where('type', '=', 'gallery')->first();
+
+ $entityRepo = app(EntityRepo::class);
+ $entityRepo->updatePage($page, $page->book_id, [
+ 'name' => $page->name,
+ 'html' => $page->html . "<img src=\"{$image->url}\">",
+ 'summary' => ''
+ ]);
+
+ // Ensure no images are reported as deletable
+ $imageService = app(ImageService::class);
+ $toDelete = $imageService->deleteUnusedImages(true, true);
+ $this->assertCount(0, $toDelete);
+
+ // Save a revision of our page without the image;
+ $entityRepo->updatePage($page, $page->book_id, [
+ 'name' => $page->name,
+ 'html' => "<p>Hello</p>",
+ 'summary' => ''
+ ]);
+
+ // Ensure revision images are picked up okay
+ $imageService = app(ImageService::class);
+ $toDelete = $imageService->deleteUnusedImages(true, true);
+ $this->assertCount(0, $toDelete);
+ $toDelete = $imageService->deleteUnusedImages(false, true);
+ $this->assertCount(1, $toDelete);
+
+ // Check image is found when revisions are destroyed
+ $page->revisions()->delete();
+ $toDelete = $imageService->deleteUnusedImages(true, true);
+ $this->assertCount(1, $toDelete);
+
+ // Check the image is deleted
+ $absPath = public_path($relPath);
+ $this->assertTrue(file_exists($absPath), "Existing uploaded file at path {$absPath} exists");
+ $toDelete = $imageService->deleteUnusedImages(true, false);
+ $this->assertCount(1, $toDelete);
+ $this->assertFalse(file_exists($absPath));
+
+ $this->deleteImage($relPath);
+ }
+
}
\ No newline at end of file