diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php
index 7c4b280a8..0c62a13fc 100644
--- a/app/Entities/Repos/BookRepo.php
+++ b/app/Entities/Repos/BookRepo.php
@@ -91,6 +91,7 @@ class BookRepo
     {
         $book = new Book();
         $this->baseRepo->create($book, $input);
+        $this->baseRepo->updateCoverImage($book, $input['image']);
         Activity::add(ActivityType::BOOK_CREATE, $book);
 
         return $book;
@@ -102,6 +103,11 @@ class BookRepo
     public function update(Book $book, array $input): Book
     {
         $this->baseRepo->update($book, $input);
+
+        if (isset($input['image'])) {
+            $this->baseRepo->updateCoverImage($book, $input['image'], $input['image'] === null);
+        }
+
         Activity::add(ActivityType::BOOK_UPDATE, $book);
 
         return $book;
diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php
index ceabba59a..03e7804d5 100644
--- a/app/Entities/Repos/BookshelfRepo.php
+++ b/app/Entities/Repos/BookshelfRepo.php
@@ -89,6 +89,7 @@ class BookshelfRepo
     {
         $shelf = new Bookshelf();
         $this->baseRepo->create($shelf, $input);
+        $this->baseRepo->updateCoverImage($shelf, $input['image']);
         $this->updateBooks($shelf, $bookIds);
         Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
 
@@ -106,14 +107,17 @@ class BookshelfRepo
             $this->updateBooks($shelf, $bookIds);
         }
 
+        if (isset($input['image'])) {
+            $this->baseRepo->updateCoverImage($shelf, $input['image'], $input['image'] === null);
+        }
+
         Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
 
         return $shelf;
     }
 
     /**
-     * Update which books are assigned to this shelf by
-     * syncing the given book ids.
+     * Update which books are assigned to this shelf by syncing the given book ids.
      * Function ensures the books are visible to the current user and existing.
      */
     protected function updateBooks(Bookshelf $shelf, array $bookIds)
@@ -132,17 +136,6 @@ class BookshelfRepo
         $shelf->books()->sync($syncData);
     }
 
-    /**
-     * Update the given shelf cover image, or clear it.
-     *
-     * @throws ImageUploadException
-     * @throws Exception
-     */
-    public function updateCoverImage(Bookshelf $shelf, ?UploadedFile $coverImage, bool $removeImage = false)
-    {
-        $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
-    }
-
     /**
      * Copy down the permissions of the given shelf to all child books.
      */
diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php
index b4923b90a..3553a9db3 100644
--- a/app/Entities/Tools/Cloner.php
+++ b/app/Entities/Tools/Cloner.php
@@ -50,11 +50,8 @@ class Cloner
     public function clonePage(Page $original, Entity $parent, string $newName): Page
     {
         $copyPage = $this->pageRepo->getNewDraftPage($parent);
-        $pageData = $original->getAttributes();
-
-        // Update name & tags
+        $pageData = $this->entityToInputData($original);
         $pageData['name'] = $newName;
-        $pageData['tags'] = $this->entityTagsToInputArray($original);
 
         return $this->pageRepo->publishDraft($copyPage, $pageData);
     }
@@ -65,9 +62,8 @@ class Cloner
      */
     public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
     {
-        $chapterDetails = $original->getAttributes();
+        $chapterDetails = $this->entityToInputData($original);
         $chapterDetails['name'] = $newName;
-        $chapterDetails['tags'] = $this->entityTagsToInputArray($original);
 
         $copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
 
@@ -87,9 +83,8 @@ class Cloner
      */
     public function cloneBook(Book $original, string $newName): Book
     {
-        $bookDetails = $original->getAttributes();
+        $bookDetails = $this->entityToInputData($original);
         $bookDetails['name'] = $newName;
-        $bookDetails['tags'] = $this->entityTagsToInputArray($original);
 
         $copyBook = $this->bookRepo->create($bookDetails);
 
@@ -104,16 +99,26 @@ class Cloner
             }
         }
 
-        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 entity to a raw data array of input data.
+     * @return array<string, mixed>
+     */
+    public function entityToInputData(Entity $entity): array
+    {
+        $inputData = $entity->getAttributes();
+        $inputData['tags'] = $this->entityTagsToInputArray($entity);
+
+        // Add a cover to the data if existing on the original entity
+        if ($entity->cover instanceof Image) {
+            $tmpImgFile = tmpfile();
+            $uploadedFile = $this->imageToUploadedFile($entity->cover, $tmpImgFile);
+            $inputData['image'] = $uploadedFile;
         }
 
-        return $copyBook;
+        return $inputData;
     }
 
     /**
diff --git a/app/Entities/Tools/HierarchyTransformer.php b/app/Entities/Tools/HierarchyTransformer.php
new file mode 100644
index 000000000..17e153e05
--- /dev/null
+++ b/app/Entities/Tools/HierarchyTransformer.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
+
+class HierarchyTransformer
+{
+    protected BookRepo $bookRepo;
+    protected BookshelfRepo $shelfRepo;
+    protected Cloner $cloner;
+    protected TrashCan $trashCan;
+
+    // TODO - Test setting book cover image from API
+    //   Ensure we can update without resetting image accidentally
+    //   Ensure api docs correct.
+    // TODO - As above but for shelves.
+
+    public function transformChapterToBook(Chapter $chapter): Book
+    {
+        // TODO - Check permissions before call
+        //   Permissions: edit-chapter, delete-chapter, create-book
+        $inputData = $this->cloner->entityToInputData($chapter);
+        $book = $this->bookRepo->create($inputData);
+
+        // TODO - Copy permissions
+
+        /** @var Page $page */
+        foreach ($chapter->pages as $page) {
+            $page->chapter_id = 0;
+            $page->changeBook($book->id);
+        }
+
+        $this->trashCan->destroyEntity($chapter);
+
+        // TODO - Log activity for change
+        return $book;
+    }
+
+    public function transformBookToShelf(Book $book): Bookshelf
+    {
+        // TODO - Check permissions before call
+        //   Permissions: edit-book, delete-book, create-shelf
+        $inputData = $this->cloner->entityToInputData($book);
+        $shelf = $this->shelfRepo->create($inputData, []);
+
+        // TODO - Copy permissions?
+
+        $shelfBookSyncData = [];
+
+        /** @var Chapter $chapter */
+        foreach ($book->chapters as $index => $chapter) {
+            $newBook = $this->transformChapterToBook($chapter);
+            $shelfBookSyncData[$newBook->id] = ['order' => $index];
+        }
+
+        $shelf->books()->sync($shelfBookSyncData);
+
+        if ($book->directPages->count() > 0) {
+            $book->name .= ' ' . trans('entities.pages');
+        } else {
+            $this->trashCan->destroyEntity($book);
+        }
+
+        // TODO - Log activity for change
+        return $shelf;
+    }
+}
\ No newline at end of file
diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php
index 1e130c9e1..abec2e2d5 100644
--- a/app/Entities/Tools/TrashCan.php
+++ b/app/Entities/Tools/TrashCan.php
@@ -344,7 +344,7 @@ class TrashCan
      *
      * @throws Exception
      */
-    protected function destroyEntity(Entity $entity): int
+    public function destroyEntity(Entity $entity): int
     {
         if ($entity instanceof Page) {
             return $this->destroyPage($entity);
diff --git a/app/Http/Controllers/Api/BookApiController.php b/app/Http/Controllers/Api/BookApiController.php
index 2b6e7a2e1..73cac6318 100644
--- a/app/Http/Controllers/Api/BookApiController.php
+++ b/app/Http/Controllers/Api/BookApiController.php
@@ -11,19 +11,6 @@ class BookApiController extends ApiController
 {
     protected $bookRepo;
 
-    protected $rules = [
-        'create' => [
-            'name'        => ['required', 'string', 'max:255'],
-            'description' => ['string', 'max:1000'],
-            'tags'        => ['array'],
-        ],
-        'update' => [
-            'name'        => ['string', 'min:1', 'max:255'],
-            'description' => ['string', 'max:1000'],
-            'tags'        => ['array'],
-        ],
-    ];
-
     public function __construct(BookRepo $bookRepo)
     {
         $this->bookRepo = $bookRepo;
@@ -97,4 +84,21 @@ class BookApiController extends ApiController
 
         return response('', 204);
     }
+
+    protected function rules(): array {
+        return [
+            'create' => [
+                'name'        => ['required', 'string', 'max:255'],
+                'description' => ['string', 'max:1000'],
+                'tags'        => ['array'],
+                'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
+            ],
+            'update' => [
+                'name'        => ['string', 'min:1', 'max:255'],
+                'description' => ['string', 'max:1000'],
+                'tags'        => ['array'],
+                'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
+            ],
+        ];
+    }
 }
diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php
index 2720d1db2..400dff977 100644
--- a/app/Http/Controllers/Api/BookshelfApiController.php
+++ b/app/Http/Controllers/Api/BookshelfApiController.php
@@ -13,21 +13,6 @@ class BookshelfApiController extends ApiController
 {
     protected BookshelfRepo $bookshelfRepo;
 
-    protected $rules = [
-        'create' => [
-            'name'        => ['required', 'string', 'max:255'],
-            'description' => ['string', 'max:1000'],
-            'books'       => ['array'],
-            'tags'        => ['array'],
-        ],
-        'update' => [
-            'name'        => ['string', 'min:1', 'max:255'],
-            'description' => ['string', 'max:1000'],
-            'books'       => ['array'],
-            'tags'        => ['array'],
-        ],
-    ];
-
     /**
      * BookshelfApiController constructor.
      */
@@ -117,4 +102,24 @@ class BookshelfApiController extends ApiController
 
         return response('', 204);
     }
+
+    protected function rules(): array
+    {
+        return [
+            'create' => [
+                'name'        => ['required', 'string', 'max:255'],
+                'description' => ['string', 'max:1000'],
+                'books'       => ['array'],
+                'tags'        => ['array'],
+                'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
+            ],
+            'update' => [
+                'name'        => ['string', 'min:1', 'max:255'],
+                'description' => ['string', 'max:1000'],
+                'books'       => ['array'],
+                'tags'        => ['array'],
+                'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
+            ],
+        ];
+    }
 }
diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php
index bc403c6d0..b9dd0e799 100644
--- a/app/Http/Controllers/BookController.php
+++ b/app/Http/Controllers/BookController.php
@@ -100,7 +100,6 @@ class BookController extends Controller
         }
 
         $book = $this->bookRepo->create($request->all());
-        $this->bookRepo->updateCoverImage($book, $request->file('image', null));
 
         if ($bookshelf) {
             $bookshelf->appendBook($book);
@@ -158,15 +157,20 @@ class BookController extends Controller
     {
         $book = $this->bookRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-update', $book);
-        $this->validate($request, [
+
+        $validated = $this->validate($request, [
             'name'        => ['required', 'string', 'max:255'],
             'description' => ['string', 'max:1000'],
             'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
         ]);
 
-        $book = $this->bookRepo->update($book, $request->all());
-        $resetCover = $request->has('image_reset');
-        $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
+        if ($request->has('image_reset')) {
+            $validated['image'] = null;
+        } else if (is_null($validated['image'])) {
+            unset($validated['image']);
+        }
+
+        $book = $this->bookRepo->update($book, $validated);
 
         return redirect($book->getUrl());
     }
diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php
index 9a7f78a85..ce2e508c8 100644
--- a/app/Http/Controllers/BookshelfController.php
+++ b/app/Http/Controllers/BookshelfController.php
@@ -83,15 +83,14 @@ class BookshelfController extends Controller
     public function store(Request $request)
     {
         $this->checkPermission('bookshelf-create-all');
-        $this->validate($request, [
+        $validated = $this->validate($request, [
             'name'        => ['required', 'string', 'max:255'],
             'description' => ['string', 'max:1000'],
             'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
         ]);
 
         $bookIds = explode(',', $request->get('books', ''));
-        $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
-        $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
+        $shelf = $this->bookshelfRepo->create($validated, $bookIds);
 
         return redirect($shelf->getUrl());
     }
@@ -160,16 +159,20 @@ class BookshelfController extends Controller
     {
         $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
-        $this->validate($request, [
+        $validated = $this->validate($request, [
             'name'        => ['required', 'string', 'max:255'],
             'description' => ['string', 'max:1000'],
             'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
         ]);
 
+        if ($request->has('image_reset')) {
+            $validated['image'] = null;
+        } else if (is_null($validated['image'])) {
+            unset($validated['image']);
+        }
+
         $bookIds = explode(',', $request->get('books', ''));
-        $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
-        $resetCover = $request->has('image_reset');
-        $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
+        $shelf = $this->bookshelfRepo->update($shelf, $validated, $bookIds);
 
         return redirect($shelf->getUrl());
     }
diff --git a/tests/OpenGraphTest.php b/tests/OpenGraphTest.php
index 17a5aa2c5..dd99b7bef 100644
--- a/tests/OpenGraphTest.php
+++ b/tests/OpenGraphTest.php
@@ -6,6 +6,7 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\BaseRepo;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\BookshelfRepo;
 use Illuminate\Support\Str;
@@ -69,8 +70,8 @@ class OpenGraphTest extends TestCase
         $this->assertArrayNotHasKey('image', $tags);
 
         // Test image set if image has cover image
-        $shelfRepo = app(BookshelfRepo::class);
-        $shelfRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
+        $baseRepo = app(BaseRepo::class);
+        $baseRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
         $resp = $this->asEditor()->get($shelf->getUrl());
         $tags = $this->getOpenGraphTags($resp);