diff --git a/app/Exceptions/ZipImportException.php b/app/Exceptions/ZipImportException.php
index 2403c5144..452365c6e 100644
--- a/app/Exceptions/ZipImportException.php
+++ b/app/Exceptions/ZipImportException.php
@@ -7,6 +7,7 @@ class ZipImportException extends \Exception
     public function __construct(
         public array $errors
     ) {
-        parent::__construct();
+        $message = "Import failed with errors:" . implode("\n", $this->errors);
+        parent::__construct($message);
     }
 }
diff --git a/app/Exports/Controllers/ImportController.php b/app/Exports/Controllers/ImportController.php
index ec5ac8080..4d2c83090 100644
--- a/app/Exports/Controllers/ImportController.php
+++ b/app/Exports/Controllers/ImportController.php
@@ -79,18 +79,21 @@ class ImportController extends Controller
         $import = $this->imports->findVisible($id);
         $parent = null;
 
-        if ($import->getType() === 'page' || $import->getType() === 'chapter') {
+        if ($import->type === 'page' || $import->type === 'chapter') {
             $data = $this->validate($request, [
                 'parent' => ['required', 'string']
             ]);
             $parent = $data['parent'];
         }
 
-        // TODO  - Run import
-           // TODO - Validate again before
-           // TODO - Check permissions before (create for main item, create for children, create for related items [image, attachments])
+        $entity = $this->imports->runImport($import, $parent);
+        if ($entity) {
+            $this->logActivity(ActivityType::IMPORT_RUN, $import);
+            return redirect($entity->getUrl());
+        }
         // TODO - Redirect to result
         // TODO - Or redirect back with errors
+        return 'failed';
     }
 
     /**
diff --git a/app/Exports/ImportRepo.php b/app/Exports/ImportRepo.php
index b94563545..d169d4845 100644
--- a/app/Exports/ImportRepo.php
+++ b/app/Exports/ImportRepo.php
@@ -2,9 +2,11 @@
 
 namespace BookStack\Exports;
 
+use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Queries\EntityQueries;
 use BookStack\Exceptions\FileUploadException;
 use BookStack\Exceptions\ZipExportException;
+use BookStack\Exceptions\ZipImportException;
 use BookStack\Exceptions\ZipValidationException;
 use BookStack\Exports\ZipExports\Models\ZipExportBook;
 use BookStack\Exports\ZipExports\Models\ZipExportChapter;
@@ -95,9 +97,9 @@ class ImportRepo
     }
 
     /**
-     * @throws ZipValidationException
+     * @throws ZipValidationException|ZipImportException
      */
-    public function runImport(Import $import, ?string $parent = null)
+    public function runImport(Import $import, ?string $parent = null): ?Entity
     {
         $parentModel = null;
         if ($import->type === 'page' || $import->type === 'chapter') {
diff --git a/app/Exports/ZipExports/ZipImportReferences.php b/app/Exports/ZipExports/ZipImportReferences.php
index 8062886e5..3bce16bbb 100644
--- a/app/Exports/ZipExports/ZipImportReferences.php
+++ b/app/Exports/ZipExports/ZipImportReferences.php
@@ -110,7 +110,7 @@ class ZipImportReferences
     {
         foreach ($this->books as $book) {
             $exportBook = $this->zipExportBookMap[$book->id];
-            $content = $exportBook->description_html || '';
+            $content = $exportBook->description_html ?? '';
             $parsed = $this->parser->parseReferences($content, $this->handleReference(...));
 
             $this->baseRepo->update($book, [
@@ -120,7 +120,7 @@ class ZipImportReferences
 
         foreach ($this->chapters as $chapter) {
             $exportChapter = $this->zipExportChapterMap[$chapter->id];
-            $content = $exportChapter->description_html || '';
+            $content = $exportChapter->description_html ?? '';
             $parsed = $this->parser->parseReferences($content, $this->handleReference(...));
 
             $this->baseRepo->update($chapter, [
diff --git a/app/Exports/ZipExports/ZipImportRunner.php b/app/Exports/ZipExports/ZipImportRunner.php
index 345c22be1..9f19f03e2 100644
--- a/app/Exports/ZipExports/ZipImportRunner.php
+++ b/app/Exports/ZipExports/ZipImportRunner.php
@@ -12,17 +12,22 @@ use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\ZipExportException;
 use BookStack\Exceptions\ZipImportException;
 use BookStack\Exports\Import;
+use BookStack\Exports\ZipExports\Models\ZipExportAttachment;
 use BookStack\Exports\ZipExports\Models\ZipExportBook;
 use BookStack\Exports\ZipExports\Models\ZipExportChapter;
+use BookStack\Exports\ZipExports\Models\ZipExportImage;
 use BookStack\Exports\ZipExports\Models\ZipExportPage;
 use BookStack\Exports\ZipExports\Models\ZipExportTag;
+use BookStack\Uploads\Attachment;
+use BookStack\Uploads\AttachmentService;
 use BookStack\Uploads\FileStorage;
+use BookStack\Uploads\Image;
 use BookStack\Uploads\ImageService;
 use Illuminate\Http\UploadedFile;
 
 class ZipImportRunner
 {
-    protected array $tempFilesToCleanup = []; // TODO
+    protected array $tempFilesToCleanup = [];
 
     public function __construct(
         protected FileStorage $storage,
@@ -30,14 +35,19 @@ class ZipImportRunner
         protected ChapterRepo $chapterRepo,
         protected BookRepo $bookRepo,
         protected ImageService $imageService,
+        protected AttachmentService $attachmentService,
         protected ZipImportReferences $references,
     ) {
     }
 
     /**
+     * Run the import.
+     * Performs re-validation on zip, validation on parent provided, and permissions for importing
+     * the planned content, before running the import process.
+     * Returns the top-level entity item which was imported.
      * @throws ZipImportException
      */
-    public function run(Import $import, ?Entity $parent = null): void
+    public function run(Import $import, ?Entity $parent = null): ?Entity
     {
         $zipPath = $this->getZipPath($import);
         $reader = new ZipExportReader($zipPath);
@@ -63,8 +73,16 @@ class ZipImportRunner
         }
 
         $this->ensurePermissionsPermitImport($exportModel);
+        $entity = null;
+
+        if ($exportModel instanceof ZipExportBook) {
+            $entity = $this->importBook($exportModel, $reader);
+        } else if ($exportModel instanceof ZipExportChapter) {
+            $entity = $this->importChapter($exportModel, $parent, $reader);
+        } else if ($exportModel instanceof ZipExportPage) {
+            $entity = $this->importPage($exportModel, $parent, $reader);
+        }
 
-        // TODO - Run import
           // TODO - In transaction?
             // TODO - Revert uploaded files if goes wrong
               // TODO - Attachments
@@ -72,6 +90,23 @@ class ZipImportRunner
               // (Both listed/stored in references)
 
         $this->references->replaceReferences();
+
+        $reader->close();
+        $this->cleanup();
+
+        dd('stop');
+
+        // TODO - Delete import/zip after import?
+          // Do this in parent repo?
+
+        return $entity;
+    }
+
+    protected function cleanup()
+    {
+        foreach ($this->tempFilesToCleanup as $file) {
+            unlink($file);
+        }
     }
 
     protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
@@ -83,17 +118,26 @@ class ZipImportRunner
             'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),
         ]);
 
-        // TODO - Parse/format description_html references
-
         if ($book->cover) {
             $this->references->addImage($book->cover, null);
         }
 
-        // TODO - Pages
-        foreach ($exportBook->chapters as $exportChapter) {
-            $this->importChapter($exportChapter, $book, $reader);
+        $children = [
+            ...$exportBook->chapters,
+            ...$exportBook->pages,
+        ];
+
+        usort($children, function (ZipExportPage|ZipExportChapter $a, ZipExportPage|ZipExportChapter $b) {
+            return ($a->priority ?? 0) - ($b->priority ?? 0);
+        });
+
+        foreach ($children as $child) {
+            if ($child instanceof ZipExportChapter) {
+                $this->importChapter($child, $book, $reader);
+            } else if ($child instanceof ZipExportPage) {
+                $this->importPage($child, $book, $reader);
+            }
         }
-        // TODO - Sort chapters/pages by order
 
         $this->references->addBook($book, $exportBook);
 
@@ -108,17 +152,14 @@ class ZipImportRunner
             'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
         ], $parent);
 
-        // TODO - Parse/format description_html references
-
         $exportPages = $exportChapter->pages;
         usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {
             return ($a->priority ?? 0) - ($b->priority ?? 0);
         });
 
         foreach ($exportPages as $exportPage) {
-            //
+            $this->importPage($exportPage, $chapter, $reader);
         }
-        // TODO - Pages
 
         $this->references->addChapter($chapter, $exportChapter);
 
@@ -129,11 +170,13 @@ class ZipImportRunner
     {
         $page = $this->pageRepo->getNewDraftPage($parent);
 
-        // TODO - Import attachments
-          // TODO - Add attachment references
-        // TODO - Import images
-          // TODO - Add image references
-        // TODO - Parse/format HTML
+        foreach ($exportPage->attachments as $exportAttachment) {
+            $this->importAttachment($exportAttachment, $page, $reader);
+        }
+
+        foreach ($exportPage->images as $exportImage) {
+            $this->importImage($exportImage, $page, $reader);
+        }
 
         $this->pageRepo->publishDraft($page, [
             'name' => $exportPage->name,
@@ -147,6 +190,40 @@ class ZipImportRunner
         return $page;
     }
 
+    protected function importAttachment(ZipExportAttachment $exportAttachment, Page $page, ZipExportReader $reader): Attachment
+    {
+        if ($exportAttachment->file) {
+            $file = $this->zipFileToUploadedFile($exportAttachment->file, $reader);
+            $attachment = $this->attachmentService->saveNewUpload($file, $page->id);
+            $attachment->name = $exportAttachment->name;
+            $attachment->save();
+        } else {
+            $attachment = $this->attachmentService->saveNewFromLink(
+                $exportAttachment->name,
+                $exportAttachment->link ?? '',
+                $page->id,
+            );
+        }
+
+        $this->references->addAttachment($attachment, $exportAttachment->id);
+
+        return $attachment;
+    }
+
+    protected function importImage(ZipExportImage $exportImage, Page $page, ZipExportReader $reader): Image
+    {
+        $file = $this->zipFileToUploadedFile($exportImage->file, $reader);
+        $image = $this->imageService->saveNewFromUpload(
+            $file,
+            $exportImage->type,
+            $page->id,
+        );
+
+        $this->references->addImage($image, $exportImage->id);
+
+        return $image;
+    }
+
     protected function exportTagsToInputArray(array $exportTags): array
     {
         $tags = [];
@@ -235,7 +312,7 @@ class ZipImportRunner
         }
 
         if (count($attachments) > 0) {
-            if (userCan('attachment-create-all')) {
+            if (!userCan('attachment-create-all')) {
                 $errors[] = 'You are lacking the required permissions to create attachments.';
             }
         }
@@ -257,6 +334,8 @@ class ZipImportRunner
         stream_copy_to_stream($stream, $tempFile);
         fclose($tempFile);
 
+        $this->tempFilesToCleanup[] = $tempFilePath;
+
         return $tempFilePath;
     }
 }