diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php
index d74f2f195..59d8077a4 100644
--- a/app/Entities/Tools/Cloner.php
+++ b/app/Entities/Tools/Cloner.php
@@ -7,8 +7,12 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Entities\Repos\PageRepo;
+use BookStack\Uploads\Image;
+use BookStack\Uploads\ImageService;
+use Illuminate\Http\UploadedFile;
 
 class Cloner
 {
@@ -23,10 +27,22 @@ class Cloner
      */
     protected $chapterRepo;
 
-    public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
+    /**
+     * @var BookRepo
+     */
+    protected $bookRepo;
+
+    /**
+     * @var ImageService
+     */
+    protected $imageService;
+
+    public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService)
     {
         $this->pageRepo = $pageRepo;
         $this->chapterRepo = $chapterRepo;
+        $this->bookRepo = $bookRepo;
+        $this->imageService = $imageService;
     }
 
     /**
@@ -66,6 +82,55 @@ class Cloner
         return $copyChapter;
     }
 
+    /**
+     * Clone the given book.
+     * Clones all child chapters & pages.
+     */
+    public function cloneBook(Book $original, string $newName): Book
+    {
+        $bookDetails = $original->getAttributes();
+        $bookDetails['name'] = $newName;
+        $bookDetails['tags'] = $this->entityTagsToInputArray($original);
+
+        $copyBook = $this->bookRepo->create($bookDetails);
+
+        $directChildren = $original->getDirectChildren();
+        foreach ($directChildren as $child) {
+
+            if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) {
+                $this->cloneChapter($child, $copyBook, $child->name);
+            }
+
+            if ($child instanceof Page && !$child->draft && userCan('page-create', $copyBook)) {
+                $this->clonePage($child, $copyBook, $child->name);
+            }
+        }
+
+        if ($original->cover) {
+            try {
+                $tmpImgFile = tmpfile();
+                $uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile);
+                $this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false);
+            } catch (\Exception $exception) {
+            }
+        }
+
+        return $copyBook;
+    }
+
+    /**
+     * Convert an image instance to an UploadedFile instance to mimic
+     * a file being uploaded.
+     */
+    protected function imageToUploadedFile(Image $image, &$tmpFile): ?UploadedFile
+    {
+        $imgData = $this->imageService->getImageData($image);
+        $tmpImgFilePath = stream_get_meta_data($tmpFile)['uri'];
+        file_put_contents($tmpImgFilePath, $imgData);
+
+        return new UploadedFile($tmpImgFilePath, basename($image->path));
+    }
+
     /**
      * Convert the tags on the given entity to the raw format
      * that's used for incoming request data.
diff --git a/app/Facades/Activity.php b/app/Facades/Activity.php
index 76493efd7..6c279a057 100644
--- a/app/Facades/Activity.php
+++ b/app/Facades/Activity.php
@@ -4,6 +4,9 @@ namespace BookStack\Facades;
 
 use Illuminate\Support\Facades\Facade;
 
+/**
+ * @see \BookStack\Actions\ActivityLogger
+ */
 class Activity extends Facade
 {
     /**
diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php
index 5434afaf8..bc403c6d0 100644
--- a/app/Http/Controllers/BookController.php
+++ b/app/Http/Controllers/BookController.php
@@ -2,16 +2,18 @@
 
 namespace BookStack\Http\Controllers;
 
-use Activity;
 use BookStack\Actions\ActivityQueries;
 use BookStack\Actions\ActivityType;
 use BookStack\Actions\View;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Tools\Cloner;
 use BookStack\Entities\Tools\PermissionsUpdater;
 use BookStack\Entities\Tools\ShelfContext;
 use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Facades\Activity;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 use Throwable;
@@ -225,4 +227,39 @@ class BookController extends Controller
 
         return redirect($book->getUrl());
     }
+
+    /**
+     * Show the view to copy a book.
+     *
+     * @throws NotFoundException
+     */
+    public function showCopy(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-view', $book);
+
+        session()->flashInput(['name' => $book->name]);
+
+        return view('books.copy', [
+            'book' => $book,
+        ]);
+    }
+
+    /**
+     * Create a copy of a book within the requested target destination.
+     *
+     * @throws NotFoundException
+     */
+    public function copy(Request $request, Cloner $cloner, string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-view', $book);
+        $this->checkPermission('book-create-all');
+
+        $newName = $request->get('name') ?: $book->name;
+        $bookCopy = $cloner->cloneBook($book, $newName);
+        $this->showSuccessNotification(trans('entities.books_copy_success'));
+
+        return redirect($bookCopy->getUrl());
+    }
 }
diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php
index 085285fc6..16f0779ca 100644
--- a/app/Http/Controllers/ChapterController.php
+++ b/app/Http/Controllers/ChapterController.php
@@ -210,7 +210,7 @@ class ChapterController extends Controller
     }
 
     /**
-     * Create a copy of a page within the requested target destination.
+     * Create a copy of a chapter within the requested target destination.
      *
      * @throws NotFoundException
      * @throws Throwable
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index 665e833f4..7a6930546 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -143,6 +143,8 @@ return [
     'books_sort_chapters_last' => 'Chapters Last',
     'books_sort_show_other' => 'Show Other Books',
     'books_sort_save' => 'Save New Order',
+    'books_copy' => 'Copy Book',
+    'books_copy_success' => 'Book successfully copied',
 
     // Chapters
     'chapter' => 'Chapter',
diff --git a/resources/views/books/copy.blade.php b/resources/views/books/copy.blade.php
new file mode 100644
index 000000000..4f01f55e2
--- /dev/null
+++ b/resources/views/books/copy.blade.php
@@ -0,0 +1,38 @@
+@extends('layouts.simple')
+
+@section('body')
+
+    <div class="container small">
+
+        <div class="my-s">
+            @include('entities.breadcrumbs', ['crumbs' => [
+                $book,
+                $book->getUrl('/copy') => [
+                    'text' => trans('entities.books_copy'),
+                    'icon' => 'copy',
+                ]
+            ]])
+        </div>
+
+        <div class="card content-wrap auto-height">
+
+            <h1 class="list-heading">{{ trans('entities.books_copy') }}</h1>
+
+            <form action="{{ $book->getUrl('/copy') }}" method="POST">
+                {!! csrf_field() !!}
+
+                <div class="form-group title-input">
+                    <label for="name">{{ trans('common.name') }}</label>
+                    @include('form.text', ['name' => 'name'])
+                </div>
+
+                <div class="form-group text-right">
+                    <a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
+                    <button type="submit" class="button">{{ trans('entities.books_copy') }}</button>
+                </div>
+            </form>
+
+        </div>
+    </div>
+
+@stop
diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php
index 25a6f69fa..5263bc810 100644
--- a/resources/views/books/show.blade.php
+++ b/resources/views/books/show.blade.php
@@ -110,6 +110,12 @@
                     <span>{{ trans('common.sort') }}</span>
                 </a>
             @endif
+            @if(userCan('book-create-all'))
+                <a href="{{ $book->getUrl('/copy') }}" class="icon-list-item">
+                    <span>@icon('copy')</span>
+                    <span>{{ trans('common.copy') }}</span>
+                </a>
+            @endif
             @if(userCan('restrictions-manage', $book))
                 <a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item">
                     <span>@icon('lock')</span>
diff --git a/routes/web.php b/routes/web.php
index 13cf2909b..73cc3dc66 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -80,6 +80,8 @@ Route::middleware('auth')->group(function () {
     Route::get('/books/{bookSlug}/permissions', [BookController::class, 'showPermissions']);
     Route::put('/books/{bookSlug}/permissions', [BookController::class, 'permissions']);
     Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
+    Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']);
+    Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']);
     Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
     Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
     Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php
index 2894fbb98..7f102a17e 100644
--- a/tests/Entity/BookTest.php
+++ b/tests/Entity/BookTest.php
@@ -3,10 +3,15 @@
 namespace Tests\Entity;
 
 use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Repos\BookRepo;
 use Tests\TestCase;
+use Tests\Uploads\UsesImages;
 
 class BookTest extends TestCase
 {
+    use UsesImages;
+
     public function test_create()
     {
         $book = Book::factory()->make([
@@ -204,4 +209,88 @@ class BookTest extends TestCase
 
         $this->assertEquals('parta-partb-partc', $book->slug);
     }
+
+    public function test_show_view_has_copy_button()
+    {
+        /** @var Book $book */
+        $book = Book::query()->first();
+        $resp = $this->asEditor()->get($book->getUrl());
+
+        $resp->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy');
+    }
+
+    public function test_copy_view()
+    {
+        /** @var Book $book */
+        $book = Book::query()->first();
+        $resp = $this->asEditor()->get($book->getUrl('/copy'));
+
+        $resp->assertOk();
+        $resp->assertSee('Copy Book');
+        $resp->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]");
+    }
+
+    public function test_copy()
+    {
+        /** @var Book $book */
+        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
+        $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
+
+        /** @var Book $copy */
+        $copy = Book::query()->where('name', '=', 'My copy book')->first();
+
+        $resp->assertRedirect($copy->getUrl());
+        $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
+    }
+
+    public function test_copy_does_not_copy_non_visible_content()
+    {
+        /** @var Book $book */
+        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
+
+        // Hide child content
+        /** @var BookChild $page */
+        foreach ($book->getDirectChildren() as $child) {
+            $child->restricted = true;
+            $child->save();
+            $this->regenEntityPermissions($child);
+        }
+
+        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
+        /** @var Book $copy */
+        $copy = Book::query()->where('name', '=', 'My copy book')->first();
+
+        $this->assertEquals(0, $copy->getDirectChildren()->count());
+    }
+
+    public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
+    {
+        /** @var Book $book */
+        $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
+        $viewer = $this->getViewer();
+        $this->giveUserPermissions($viewer, ['book-create-all']);
+
+        $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
+        /** @var Book $copy */
+        $copy = Book::query()->where('name', '=', 'My copy book')->first();
+
+        $this->assertEquals(0, $copy->pages()->count());
+        $this->assertEquals(0, $copy->chapters()->count());
+    }
+
+    public function test_copy_clones_cover_image_if_existing()
+    {
+        /** @var Book $book */
+        $book = Book::query()->first();
+        $bookRepo = $this->app->make(BookRepo::class);
+        $coverImageFile = $this->getTestImage('cover.png');
+        $bookRepo->updateCoverImage($book, $coverImageFile);
+
+        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
+
+        /** @var Book $copy */
+        $copy = Book::query()->where('name', '=', 'My copy book')->first();
+        $this->assertNotNull($copy->cover);
+        $this->assertNotEquals($book->cover->id, $copy->cover->id);
+    }
 }