diff --git a/composer.json b/composer.json
index c02d7c16..0b443c02 100644
--- a/composer.json
+++ b/composer.json
@@ -14,22 +14,22 @@
     }
   ],
   "require": {
-    "drupal/context": "^4.0@beta",
-    "drupal/search_api": "~1.8",
-    "islandora/jsonld": "^2",
-    "stomp-php/stomp-php": "4.* || ^5",
-    "drupal/jwt": "^1.0.0-beta5",
-    "drupal/filehash": "^1.1 || ^2",
-    "drupal/prepopulate" : "^2.2",
-    "drupal/eva" : "^2.0",
+    "drupal/context": "^4",
+    "drupal/ctools": "^3.8 || ^4",
+    "drupal/eva" : "^3.0",
     "drupal/features" : "^3.7",
-    "drupal/migrate_plus" : "^5.1",
+    "drupal/file_replace": "^1.1",
+    "drupal/filehash": "^2",
+    "drupal/flysystem" : "^2.0@alpha",
+    "drupal/jwt": "^1.0",
+    "drupal/migrate_plus" : "^5.1 || ^6",
     "drupal/migrate_source_csv" : "^3.4",
+    "drupal/prepopulate" : "^2.2",
+    "drupal/search_api": "^1.8",
     "drupal/token" : "^1.3",
-    "drupal/flysystem" : "^2.0@alpha",
     "islandora/crayfish-commons": "^2",
-    "drupal/file_replace": "^1.1",
-    "drupal/ctools": "^3.8 || ^4"
+    "islandora/jsonld": "^2",
+    "stomp-php/stomp-php": "4.* || ^5"
   },
   "require-dev": {
     "phpunit/phpunit": "^6",
diff --git a/config/install/islandora.settings.yml b/config/install/islandora.settings.yml
index 8fb25fb8..179d4213 100644
--- a/config/install/islandora.settings.yml
+++ b/config/install/islandora.settings.yml
@@ -1,4 +1,5 @@
 broker_url: 'tcp://localhost:61613'
 jwt_expiry: '+2 hour'
 gemini_url: ''
+delete_media_and_files: TRUE
 gemini_pseudo_bundles: []
diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml
index e85d5739..86b65fd0 100644
--- a/config/schema/islandora.schema.yml
+++ b/config/schema/islandora.schema.yml
@@ -14,6 +14,9 @@ islandora.settings:
     jwt_expiry:
       type: string
       label: 'How long JWTs should last before expiring.'
+    delete_media_and_files:
+      type: boolean
+      label: 'Node Delete with Media and Files'
     upload_form_location:
       type: string
       label: 'Upload Form Location'
@@ -166,7 +169,7 @@ condition.plugin.node_had_namespace:
     pid_field:
       type: ignore
       label: 'PID field'
-      
+
 field.formatter.settings.islandora_image:
   type: field.formatter.settings.image
   label: 'Islandora image field display format settings'
diff --git a/css/islandora.css b/css/islandora.css
new file mode 100644
index 00000000..696587ac
--- /dev/null
+++ b/css/islandora.css
@@ -0,0 +1,3 @@
+.container .islandora-media-items {
+  margin: 0;
+}
diff --git a/islandora.libraries.yml b/islandora.libraries.yml
new file mode 100644
index 00000000..840dc294
--- /dev/null
+++ b/islandora.libraries.yml
@@ -0,0 +1,5 @@
+islandora:
+  version: VERSION
+  css:
+    theme:
+      css/islandora.css: {}
diff --git a/islandora.module b/islandora.module
index 5b5446d1..69176714 100644
--- a/islandora.module
+++ b/islandora.module
@@ -27,6 +27,8 @@ use Drupal\file\FileInterface;
 use Drupal\taxonomy\TermInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
+use Drupal\Core\Entity\EntityForm;
+use Drupal\file\Entity\File;
 
 /**
  * Implements hook_help().
@@ -181,7 +183,8 @@ function islandora_file_insert(FileInterface $file) {
  */
 function islandora_file_update(FileInterface $file) {
   // Exit early if unchanged.
-  if ($file->filehash != NULL && $file->original->filehash != NULL && $file->filehash['sha1'] == $file->original->filehash['sha1']) {
+  if ($file->hasField('sha1') && $file->original->hasField('sha1')
+    && $file->sha1->getString() == $file->original->sha1->getString()) {
     return;
   }
 
@@ -331,6 +334,197 @@ function islandora_form_alter(&$form, FormStateInterface $form_state, $form_id)
       }
     }
   }
+
+  $form_object = $form_state->getFormObject();
+  $utils = \Drupal::service('islandora.utils');
+  $config = \Drupal::config('islandora.settings')->get('delete_media_and_files');
+
+  if ($config == 1 && $form_object instanceof EntityForm) {
+    $entity = $form_object->getEntity();
+
+    if ($entity->getEntityTypeId() == "node" && $utils->isIslandoraType($entity->getEntityTypeId(), $entity->bundle()) && strpos($form['#form_id'], 'delete_form') !== FALSE) {
+      $medias = $utils->getMedia($form_state->getFormObject()->getEntity());
+      if (count($medias) != 0) {
+        $form['delete_associated_content'] = [
+          '#type' => 'checkbox',
+          '#title' => t('Delete all associated medias and nodes'),
+        ];
+
+        $media_list = [];
+
+        foreach ($medias as $media) {
+          $media_list[] = $media->getName();
+        }
+
+        $form['container'] = [
+          '#type' => 'container',
+          '#states' => [
+            'visible' => [
+              ':input[name="delete_associated_content"]' => ['checked' => TRUE],
+            ],
+          ],
+        ];
+
+        $form['container']['media_items'] = [
+          '#theme' => 'item_list',
+          '#type' => 'ul',
+          '#items' => $media_list,
+          '#attributes' => ['class' => ['islandora-media-items']],
+          '#wrapper_attributes' => ['class' => ['container']],
+          '#attached' => [
+            'library' => [
+              'islandora/islandora',
+            ],
+          ],
+        ];
+
+        $form['actions']['submit']['#submit'][] = 'islandora_object_delete_form_submit';
+        return $form;
+      }
+    }
+  }
+
+  return $form;
+}
+
+/**
+ * Implements a submit handler for the delete form.
+ */
+function islandora_object_delete_form_submit($form, FormStateInterface $form_state) {
+
+  $result = $form_state->getValues('delete_associated_content');
+  $utils = \Drupal::service('islandora.utils');
+
+  if ($result['delete_associated_content'] == 1) {
+
+    $node = $form_state->getFormObject()->getEntity();
+    $medias = $utils->getMedia($node);
+    $media_list = [];
+
+    $entity_field_manager = \Drupal::service('entity_field.manager');
+    $current_user = \Drupal::currentUser();
+    $logger = \Drupal::logger('logger.channel.islandora');
+    $messenger = \Drupal::messenger();
+
+    $delete_media = [];
+    $media_translations = [];
+    $media_files = [];
+    $entity_protected_medias = [];
+    $inaccessible_entities = [];
+
+    foreach ($medias as $id => $media) {
+      $lang = $media->language()->getId();
+      $selected_langcodes[$lang] = $lang;
+
+      if (!$media->access('delete', $current_user)) {
+        $inaccessible_entities[] = $media;
+        continue;
+      }
+      // Check for files.
+      $fields = $entity_field_manager->getFieldDefinitions('media', $media->bundle());
+      foreach ($fields as $field) {
+        $type = $field->getType();
+        if ($type == 'file' || $type == 'image') {
+          $target_id = $media->get($field->getName())->target_id;
+          $file = File::load($target_id);
+          if ($file) {
+            if (!$file->access('delete', $current_user)) {
+              $inaccessible_entities[] = $file;
+              continue;
+            }
+            $media_files[$id][$file->id()] = $file;
+          }
+        }
+      }
+
+      foreach ($selected_langcodes as $langcode) {
+        // We're only working with media, which are translatable.
+        $entity = $media->getTranslation($langcode);
+        if ($entity->isDefaultTranslation()) {
+          $delete_media[$id] = $entity;
+          unset($media_translations[$id]);
+        }
+        elseif (!isset($delete_media[$id])) {
+          $media_translations[$id][] = $entity;
+        }
+      }
+    }
+
+    if ($delete_media) {
+      foreach ($delete_media as $id => $media) {
+        try {
+          $media->delete();
+          $media_list[] = $id;
+          $logger->notice('The media %label has been deleted.', [
+            '%label' => $media->label(),
+          ]);
+        }
+        catch (Exception $e) {
+          $entity_protected_medias[] = $id;
+        }
+      }
+    }
+
+    $delete_files = array_filter($media_files, function ($media) use ($entity_protected_medias) {
+      return !in_array($media, $entity_protected_medias);
+    }, ARRAY_FILTER_USE_KEY);
+
+    if ($delete_files) {
+      foreach ($delete_files as $files_array) {
+        foreach ($files_array as $file) {
+          $file->delete();
+          $logger->notice('The file %label has been deleted.', [
+            '%label' => $file->label(),
+          ]);
+        }
+      }
+    }
+
+    $delete_media_translations = array_filter($media_translations, function ($media) use ($entity_protected_medias) {
+      return !in_array($media, $entity_protected_medias);
+    }, ARRAY_FILTER_USE_KEY);
+
+    if ($delete_media_translations) {
+      foreach ($delete_media_translations as $id => $translations) {
+        $media = $medias[$id];
+        foreach ($translations as $translation) {
+          $media->removeTranslation($translation->language()->getId());
+        }
+        $media->save();
+        foreach ($translations as $translation) {
+          $logger->notice('The media %label @language translation has been deleted', [
+            '%label' => $media->label(),
+            '@language' => $translation->language()->getName(),
+          ]);
+        }
+      }
+    }
+
+    if ($inaccessible_entities) {
+      $messenger->addWarning("@count items have not been deleted because you do not have the necessary permissions.", [
+        '@count' => count($inaccessible_entities),
+      ]);
+    }
+
+    $build = [
+      'heading' => [
+        '#type' => 'html_tag',
+        '#tag' => 'div',
+        '#value' => t("The repository item @node and @media", [
+          '@node' => $node->getTitle(),
+          '@media' => \Drupal::translation()->formatPlural(
+            count($media_list), 'the media with the id @media has been deleted.',
+            'the medias with the ids @media have been deleted.',
+            ['@media' => implode(", ", $media_list)],
+          ),
+        ]),
+      ],
+    ];
+
+    $message = \Drupal::service('renderer')->renderPlain($build);
+    $messenger->deleteByType('status');
+    $messenger->addStatus($message);
+  }
 }
 
 /**
@@ -515,7 +709,7 @@ function islandora_preprocess_views_view_table(&$variables) {
 
   // Check for a weight selector field.
   foreach ($variables['view']->field as $field_key => $field) {
-    if ($field->options['plugin_id'] == 'integer_weight_selector') {
+    if ($field->getPluginId() == 'integer_weight_selector') {
 
       // Check if the weight selector is on the first column.
       $is_first_column = array_search($field_key, array_keys($variables['view']->field)) > 0 ? FALSE : TRUE;
diff --git a/islandora.post_update.php b/islandora.post_update.php
new file mode 100644
index 00000000..0a29e56e
--- /dev/null
+++ b/islandora.post_update.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @file
+ * Post updates.
+ */
+
+/**
+ * Set default value for delete_media_and_files field in settings.
+ */
+function islandora_post_update_delete_media_and_files() {
+  $config_factory = \Drupal::configFactory();
+  $config = $config_factory->getEditable('islandora.settings');
+  $config->set('delete_media_and_files', TRUE);
+  $config->save(TRUE);
+}
diff --git a/modules/islandora_core_feature/config/install/filehash.settings.yml b/modules/islandora_core_feature/config/install/filehash.settings.yml
index 7deecc69..3dcae297 100644
--- a/modules/islandora_core_feature/config/install/filehash.settings.yml
+++ b/modules/islandora_core_feature/config/install/filehash.settings.yml
@@ -1,5 +1,24 @@
 algos:
-  sha1: sha1
+  blake2b_128: '0'
+  blake2b_160: '0'
+  blake2b_224: '0'
+  blake2b_256: '0'
+  blake2b_384: '0'
+  blake2b_512: '0'
   md5: '0'
+  sha1: sha1
+  sha224: '0'
   sha256: '0'
+  sha384: '0'
+  sha512_224: '0'
+  sha512_256: '0'
+  sha512: '0'
+  sha3_224: '0'
+  sha3_256: '0'
+  sha3_384: '0'
+  sha3_512: '0'
 dedupe: 0
+rehash: true
+original: true
+dedupe_original: false
+mime_types: {  }
diff --git a/modules/islandora_core_feature/config/install/views.view.file_checksum.yml b/modules/islandora_core_feature/config/install/views.view.file_checksum.yml
index b498ba66..2c819101 100644
--- a/modules/islandora_core_feature/config/install/views.view.file_checksum.yml
+++ b/modules/islandora_core_feature/config/install/views.view.file_checksum.yml
@@ -1,14 +1,15 @@
 langcode: en
 status: true
 dependencies:
-  enforced:
-    module:
-      - islandora_core_feature
   module:
     - file
     - filehash
     - rest
     - serialization
+    - user
+  enforced:
+    module:
+      - islandora_core_feature
 id: file_checksum
 label: 'File Checksum'
 module: views
@@ -16,71 +17,24 @@ description: 'Exposes a REST endpoint for getting the checksum of a File'
 tag: ''
 base_table: file_managed
 base_field: fid
-core: 8.x
 display:
   default:
-    display_plugin: default
     id: default
     display_title: Master
+    display_plugin: default
     position: 0
     display_options:
-      access:
-        type: none
-        options: {  }
-      cache:
-        type: tag
-        options: {  }
-      query:
-        type: views_query
-        options:
-          disable_sql_rewrite: false
-          distinct: false
-          replica: false
-          query_comment: ''
-          query_tags: {  }
-      exposed_form:
-        type: basic
-        options:
-          submit_button: Apply
-          reset_button: false
-          reset_button_label: Reset
-          exposed_sorts_label: 'Sort by'
-          expose_sort_order: true
-          sort_asc_label: Asc
-          sort_desc_label: Desc
-      pager:
-        type: mini
-        options:
-          items_per_page: 10
-          offset: 0
-          id: 0
-          total_pages: null
-          expose:
-            items_per_page: false
-            items_per_page_label: 'Items per page'
-            items_per_page_options: '5, 10, 25, 50'
-            items_per_page_options_all: false
-            items_per_page_options_all_label: '- All -'
-            offset: false
-            offset_label: Offset
-          tags:
-            previous: ‹‹
-            next: ››
-      style:
-        type: serializer
-      row:
-        type: 'entity:file'
-        options:
-          relationship: none
-          view_mode: default
       fields:
         sha1:
           id: sha1
-          table: filehash
+          table: file_managed
           field: sha1
           relationship: none
           group_type: group
           admin_label: ''
+          entity_type: file
+          entity_field: sha1
+          plugin_id: field
           label: ''
           exclude: false
           alter:
@@ -92,7 +46,7 @@ display:
             external: false
             replace_spaces: false
             path_case: none
-            trim_whitespace: false
+            trim_whitespace: true
             alt: ''
             rel: ''
             link_class: ''
@@ -106,7 +60,7 @@ display:
             more_link: false
             more_link_text: ''
             more_link_path: ''
-            strip_tags: false
+            strip_tags: true
             trim: false
             preserve_tags: ''
             html: false
@@ -122,13 +76,120 @@ display:
           hide_empty: false
           empty_zero: false
           hide_alter_empty: true
-          plugin_id: standard
-      filters: {  }
-      sorts: {  }
-      header: {  }
-      footer: {  }
+          click_sort_column: value
+          type: filehash
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        original_sha1:
+          id: original_sha1
+          table: file_managed
+          field: original_sha1
+          relationship: none
+          group_type: group
+          admin_label: ''
+          entity_type: file
+          entity_field: original_sha1
+          plugin_id: field
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: true
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: true
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: false
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: filehash
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      pager:
+        type: mini
+        options:
+          offset: 0
+          items_per_page: 10
+          total_pages: null
+          id: 0
+          tags:
+            next: ››
+            previous: ‹‹
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      access:
+        type: perm
+        options:
+          perm: 'view checksums'
+      cache:
+        type: tag
+        options: {  }
       empty: {  }
-      relationships: {  }
+      sorts: {  }
       arguments:
         fid:
           id: fid
@@ -137,6 +198,9 @@ display:
           relationship: none
           group_type: group
           admin_label: ''
+          entity_type: file
+          entity_field: fid
+          plugin_id: file_fid
           default_action: 'not found'
           exception:
             value: all
@@ -151,8 +215,8 @@ display:
           summary_options:
             base_path: ''
             count: true
-            items_per_page: 25
             override: false
+            items_per_page: 25
           summary:
             sort_order: asc
             number_of_records: 0
@@ -164,31 +228,47 @@ display:
           validate_options: {  }
           break_phrase: false
           not: false
-          entity_type: file
-          entity_field: fid
-          plugin_id: file_fid
+      filters: {  }
+      style:
+        type: serializer
+      row:
+        type: 'entity:file'
+        options:
+          relationship: none
+          view_mode: default
+      query:
+        type: views_query
+        options:
+          query_comment: ''
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_tags: {  }
+      relationships: {  }
+      header: {  }
+      footer: {  }
       display_extenders: {  }
     cache_metadata:
       max-age: -1
       contexts:
+        - 'languages:language_content'
         - 'languages:language_interface'
         - request_format
         - url
         - url.query_args
+        - user.permissions
       tags: {  }
   rest_export_1:
-    display_plugin: rest_export
     id: rest_export_1
     display_title: 'REST export'
+    display_plugin: rest_export
     position: 1
     display_options:
-      display_extenders: {  }
-      path: checksum/%file
       pager:
         type: some
         options:
-          items_per_page: 10
           offset: 0
+          items_per_page: 10
       style:
         type: serializer
         options:
@@ -198,6 +278,19 @@ display:
         type: data_field
         options:
           field_options: {  }
+      display_extenders:
+        matomo:
+          enabled: false
+          keyword_gets: ''
+          keyword_behavior: first
+          keyword_concat_separator: ' '
+          category_behavior: none
+          category_gets: ''
+          category_concat_separator: ' '
+          category_fallback: ''
+          category_facets: {  }
+          category_facets_concat_separator: ', '
+      path: checksum/%file
       auth:
         - basic_auth
         - jwt_auth
@@ -205,7 +298,10 @@ display:
     cache_metadata:
       max-age: -1
       contexts:
+        - 'languages:language_content'
         - 'languages:language_interface'
         - request_format
         - url
+        - user.permissions
       tags: {  }
+
diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
index cc4b5e94..c2a2fbc3 100644
--- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
+++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
@@ -205,11 +205,13 @@ class IIIFManifest extends StylePluginBase {
             continue;
           }
 
-          $ocrs = $entity->{$ocrField->definition['field_name']};
+          if (!is_null($ocrField)) {
+            $ocrs = $entity->{$ocrField->definition['field_name']};
+            $ocr = isset($ocrs[$i]) ? $ocrs[$i] : FALSE;
+          }
 
           // Create the IIIF URL for this file
           // Visiting $iiif_url will resolve to the info.json for the image.
-          $ocr = isset($ocrs[$i]) ? $ocrs[$i] : FALSE;
           $file_url = $image->entity->createFileUrl(FALSE);
           $mime_type = $image->entity->getMimeType();
           $iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url);
diff --git a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php
index d6f99b58..a6e06774 100644
--- a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php
+++ b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php
@@ -6,7 +6,10 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\islandora\Plugin\Action\AbstractGenerateDerivativeMediaFile;
 
 /**
- * Emits a Node for generating derivatives event.
+ * Emits a Media for generating derivatives event.
+ *
+ * Attaches the result as a file in an image field on the emitting
+ * Media ("multi-file media").
  *
  * @Action(
  *   id = "generate_image_derivative_file",
@@ -24,7 +27,6 @@ class GenerateImageDerivativeFile extends AbstractGenerateDerivativeMediaFile {
     $config['path'] = '[date:custom:Y]-[date:custom:m]/[media:mid]-ImageService.jpg';
     $config['mimetype'] = 'application/xml';
     $config['queue'] = 'islandora-connector-houdini';
-    $config['destination_media_type'] = 'file';
     $config['scheme'] = $this->config->get('default_scheme');
     return $config;
   }
@@ -34,9 +36,30 @@ class GenerateImageDerivativeFile extends AbstractGenerateDerivativeMediaFile {
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
     $form = parent::buildConfigurationForm($form, $form_state);
-    $form['mimetype']['#description'] = $this->t('Mimetype to convert to (e.g. application/xml, etc...)');
+    $map = $this->entityFieldManager->getFieldMapByFieldType('image');
+    $file_fields = $map['media'];
+    $file_options = array_combine(array_keys($file_fields), array_keys($file_fields));
+    $file_options = array_merge(['' => ''], $file_options);
+    // @todo figure out how to write to thumbnail, which is not a real field.
+    //   see https://github.com/Islandora/islandora/issues/891.
+    unset($file_options['thumbnail']);
+
+    $form['destination_field_name'] = [
+      '#required' => TRUE,
+      '#type' => 'select',
+      '#options' => $file_options,
+      '#title' => $this->t('Destination Image field'),
+      '#default_value' => $this->configuration['destination_field_name'],
+      '#description' => $this->t('This Action stores the derivative in an
+       Image field. If you are creating a TIFF or JP2, instead use
+       "Generate a Derivative File for Media Attachment". Selected target field
+       must be an additional field, not the media\'s main storage field.
+       Selected target field must be present on the media.'),
+    ];
+
     $form['mimetype']['#value'] = 'image/jpeg';
-    $form['mimetype']['#type'] = 'hidden';
+    $form['mimetype']['#description'] = 'Mimetype to convert to. Must be
+    compatible with the destination image field.';
     return $form;
   }
 
diff --git a/src/Controller/ManageMediaController.php b/src/Controller/ManageMediaController.php
index bd670561..025d1d9d 100644
--- a/src/Controller/ManageMediaController.php
+++ b/src/Controller/ManageMediaController.php
@@ -6,6 +6,7 @@ use Drupal\islandora\IslandoraUtils;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\node\Entity\Node;
+use Drupal\Core\Url;
 use Drupal\node\NodeInterface;
 
 /**
@@ -25,7 +26,7 @@ class ManageMediaController extends ManageMembersController {
   public function addToNodePage(NodeInterface $node) {
     $field = IslandoraUtils::MEDIA_OF_FIELD;
 
-    return $this->generateTypeList(
+    $add_media_list = $this->generateTypeList(
       'media',
       'media_type',
       'entity.media.add_form',
@@ -33,6 +34,21 @@ class ManageMediaController extends ManageMembersController {
       $field,
       ['query' => ["edit[$field][widget][0][target_id]" => $node->id()]]
     );
+
+    $manage_link = Url::fromRoute('entity.media_type.collection')->toRenderArray();
+    $manage_link['#title'] = $this->t('Manage media types');
+    $manage_link['#type'] = 'link';
+    $manage_link['#prefix'] = ' ';
+    $manage_link['#suffix'] = '.';
+
+    return [
+      '#type' => 'markup',
+      '#markup' => $this->t("The following media types can be added because they have the <code>@field</code> field.", [
+        '@field' => $field,
+      ]),
+      'manage_link' => $manage_link,
+      'add_media' => $add_media_list,
+    ];
   }
 
   /**
diff --git a/src/Controller/ManageMembersController.php b/src/Controller/ManageMembersController.php
index 7f480fb3..9827ff35 100644
--- a/src/Controller/ManageMembersController.php
+++ b/src/Controller/ManageMembersController.php
@@ -7,6 +7,7 @@ use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Entity\Controller\EntityController;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Link;
+use Drupal\Core\Url;
 use Drupal\islandora\IslandoraUtils;
 use Drupal\node\NodeInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -88,7 +89,8 @@ class ManageMembersController extends EntityController {
    */
   public function addToNodePage(NodeInterface $node) {
     $field = IslandoraUtils::MEMBER_OF_FIELD;
-    return $this->generateTypeList(
+
+    $add_node_list = $this->generateTypeList(
       'node',
       'node_type',
       'node.add',
@@ -96,6 +98,21 @@ class ManageMembersController extends EntityController {
       $field,
       ['query' => ["edit[$field][widget][0][target_id]" => $node->id()]]
     );
+
+    $manage_link = Url::fromRoute('entity.node_type.collection')->toRenderArray();
+    $manage_link['#title'] = $this->t('Manage content types');
+    $manage_link['#type'] = 'link';
+    $manage_link['#prefix'] = ' ';
+    $manage_link['#suffix'] = '.';
+
+    return [
+      '#type' => 'markup',
+      '#markup' => $this->t("The following content types can be added because they have the <code>@field</code> field.", [
+        '@field' => $field,
+      ]),
+      'manage_link' => $manage_link,
+      'add_node' => $add_node_list,
+    ];
   }
 
   /**
diff --git a/src/Controller/MediaSourceController.php b/src/Controller/MediaSourceController.php
index bab43fcf..3d0e7909 100644
--- a/src/Controller/MediaSourceController.php
+++ b/src/Controller/MediaSourceController.php
@@ -280,8 +280,7 @@ class MediaSourceController extends ControllerBase {
    */
   public function attachToMediaAccess(AccountInterface $account, RouteMatch $route_match) {
     $media = $route_match->getParameter('media');
-    $node = $this->utils->getParentNode($media);
-    return AccessResult::allowedIf($node->access('update', $account) && $account->hasPermission('create media'));
+    return AccessResult::allowedIf($media->access('update', $account));
   }
 
 }
diff --git a/src/EventGenerator/EmitEvent.php b/src/EventGenerator/EmitEvent.php
index 683f3e8b..fd33fd99 100644
--- a/src/EventGenerator/EmitEvent.php
+++ b/src/EventGenerator/EmitEvent.php
@@ -14,6 +14,7 @@ use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\islandora\Event\StompHeaderEvent;
 use Drupal\islandora\Event\StompHeaderEventException;
+use Drupal\islandora\Exception\IslandoraDerivativeException;
 use Stomp\Exception\StompException;
 use Stomp\StatefulStomp;
 use Stomp\Transport\Message;
@@ -168,6 +169,10 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact
         $event->getHeaders()->all()
       );
     }
+    catch (IslandoraDerivativeException $e) {
+      $this->logger->info($e->getMessage());
+      return;
+    }
     catch (StompHeaderEventException $e) {
       $this->logger->error($e->getMessage());
       $this->messenger->addError($e->getMessage());
diff --git a/src/Exception/IslandoraDerivativeException.php b/src/Exception/IslandoraDerivativeException.php
new file mode 100644
index 00000000..7efe4773
--- /dev/null
+++ b/src/Exception/IslandoraDerivativeException.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\islandora\Exception;
+
+/**
+ * Islandora exceptions.
+ *
+ * @package islandora
+ */
+class IslandoraDerivativeException extends \RuntimeException {
+}
diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php
index 5049ef16..90e0b420 100644
--- a/src/Form/IslandoraSettingsForm.php
+++ b/src/Form/IslandoraSettingsForm.php
@@ -42,6 +42,7 @@ class IslandoraSettingsForm extends ConfigFormBase {
     'year',
   ];
   const GEMINI_PSEUDO_FIELD = 'field_gemini_uri';
+  const NODE_DELETE_MEDIA_AND_FILES = 'delete_media_and_files';
 
   /**
    * To list the available bundle types.
@@ -201,6 +202,14 @@ class IslandoraSettingsForm extends ConfigFormBase {
       $fedora_url = NULL;
     }
 
+    $form[self::NODE_DELETE_MEDIA_AND_FILES] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Node Delete with Media and Files'),
+      '#description' => $this->t('Adds a checkbox in the "Delete" tab of islandora objects to delete media and files associated with the object.'
+      ),
+      '#default_value' => (bool) $config->get(self::NODE_DELETE_MEDIA_AND_FILES),
+    ];
+
     $form[self::FEDORA_URL] = [
       '#type' => 'textfield',
       '#title' => $this->t('Fedora URL'),
@@ -351,6 +360,7 @@ class IslandoraSettingsForm extends ConfigFormBase {
       ->set(self::UPLOAD_FORM_LOCATION, $form_state->getValue(self::UPLOAD_FORM_LOCATION))
       ->set(self::UPLOAD_FORM_ALLOWED_MIMETYPES, $form_state->getValue(self::UPLOAD_FORM_ALLOWED_MIMETYPES))
       ->set(self::GEMINI_PSEUDO, $new_pseudo_types)
+      ->set(self::NODE_DELETE_MEDIA_AND_FILES, $form_state->getValue(self::NODE_DELETE_MEDIA_AND_FILES))
       ->save();
 
     parent::submitForm($form, $form_state);
diff --git a/src/MediaSource/MediaSourceService.php b/src/MediaSource/MediaSourceService.php
index 4a10dafa..53378985 100644
--- a/src/MediaSource/MediaSourceService.php
+++ b/src/MediaSource/MediaSourceService.php
@@ -276,8 +276,8 @@ class MediaSourceService {
         'uri' => $content_location,
         'filename' => $this->fileSystem->basename($content_location),
         'filemime' => $mimetype,
-        'status' => FILE_STATUS_PERMANENT,
       ]);
+      $file->setPermanent();
 
       // Validate file extension.
       $source_field_config = $this->entityTypeManager->getStorage('field_config')->load("media.$bundle.$source_field");
@@ -357,8 +357,8 @@ class MediaSourceService {
         'uri' => $content_location,
         'filename' => $this->fileSystem->basename($content_location),
         'filemime' => $mimetype,
-        'status' => FileInterface::STATUS_PERMANENT,
       ]);
+      $file->setPermanent();
 
       // Validate file extension.
       $bundle = $media->bundle();
diff --git a/src/Plugin/Action/AbstractGenerateDerivative.php b/src/Plugin/Action/AbstractGenerateDerivative.php
index b22201e1..b44db477 100644
--- a/src/Plugin/Action/AbstractGenerateDerivative.php
+++ b/src/Plugin/Action/AbstractGenerateDerivative.php
@@ -5,6 +5,7 @@ namespace Drupal\islandora\Plugin\Action;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\islandora\Exception\IslandoraDerivativeException;
 
 /**
  * Emits a Node event.
@@ -60,6 +61,13 @@ class AbstractGenerateDerivative extends AbstractGenerateDerivativeBase {
       throw new \RuntimeException("Could not locate taxonomy term with uri: " . $this->configuration['derivative_term_uri'], 500);
     }
 
+    // See if there is a destination media already set, and abort if it's the
+    // same as the source media. Dont cause an error, just don't continue.
+    $derivative_media = $this->utils->getMediaWithTerm($entity, $derivative_term);
+    if (!is_null($derivative_media) && $derivative_media->id() == $source_media->id()) {
+      throw new IslandoraDerivativeException("Halting derivative, as source and target media are the same. Derivative term: [" . $this->configuration['derivative_term_uri'] . "] Source term: [" . $this->configuration['source_term_uri'] . "] Node id: [" . $entity->id() . "].", 500);
+    }
+
     $route_params = [
       'node' => $entity->id(),
       'media_type' => $this->configuration['destination_media_type'],
diff --git a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php
index be9eca80..f0974b0c 100644
--- a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php
+++ b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php
@@ -7,7 +7,10 @@ use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
 
 /**
- * Emits a Node for generating derivatives event.
+ * Emits a Media for generating derivatives event.
+ *
+ * Attaches the result as a file in a file field on the emitting
+ * Media ("multi-file media").
  *
  * @Action(
  *   id = "generate_derivative_file",
@@ -98,15 +101,23 @@ class AbstractGenerateDerivativeMediaFile extends AbstractGenerateDerivativeBase
     $image_options = array_combine(array_keys($image_fields), array_keys($image_fields));
 
     $file_options = array_merge(['' => ''], $file_options, $image_options);
+
+    // @todo figure out how to write to thumbnail, which is not a real field.
+    //   see https://github.com/Islandora/islandora/issues/891.
+    unset($file_options['thumbnail']);
+
     $form['event']['#disabled'] = 'disabled';
 
     $form['destination_field_name'] = [
       '#required' => TRUE,
       '#type' => 'select',
       '#options' => $file_options,
-      '#title' => $this->t('Destination File field Name'),
+      '#title' => $this->t('Destination File field'),
       '#default_value' => $this->configuration['destination_field_name'],
-      '#description' => $this->t('File field on Media Type to hold derivative.  Cannot be the same as source'),
+      '#description' => $this->t('This Action stores a derivative file
+       in a File or Image field on a media. The destination field
+       must be an additional field, not the media\'s main storage field.
+       Selected destination field must be present on the media.'),
     ];
 
     $form['args'] = [
diff --git a/src/Plugin/views/filter/NodeHasMediaUse.php b/src/Plugin/views/filter/NodeHasMediaUse.php
index 0209505d..3c69bddd 100644
--- a/src/Plugin/views/filter/NodeHasMediaUse.php
+++ b/src/Plugin/views/filter/NodeHasMediaUse.php
@@ -18,10 +18,10 @@ class NodeHasMediaUse extends FilterPluginBase {
    * {@inheritdoc}
    */
   protected function defineOptions() {
-    return [
-      'use_uri' => ['default' => NULL],
-      'negated' => ['default' => FALSE],
-    ];
+    $options = parent::defineOptions();
+    $options['use_uri'] = ['default' => NULL];
+    $options['negated'] = ['default' => FALSE];
+    return $options;
   }
 
   /**
diff --git a/tests/src/Functional/DeleteNodeWithMediaAndFile.php b/tests/src/Functional/DeleteNodeWithMediaAndFile.php
new file mode 100644
index 00000000..40e469c5
--- /dev/null
+++ b/tests/src/Functional/DeleteNodeWithMediaAndFile.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Drupal\Tests\islandora\Functional;
+
+/**
+ * Tests the Delete Node with Media.
+ *
+ * @group islandora
+ */
+class DeleteNodeWithMediaAndFile extends IslandoraFunctionalTestBase {
+
+  /**
+   * Tests delete Node and its assoicated media.
+   */
+  public function testDeleteNodeWithMediaAndFile() {
+    $account = $this->drupalCreateUser([
+      'delete any media',
+      'create media',
+      'view media',
+      'bypass node access',
+      'access files overview',
+      'administer site configuration',
+    ]);
+    $this->drupalLogin($account);
+
+    $assert_session = $this->assertSession();
+
+    $testImageMediaType = $this->createMediaType('image', ['id' => 'test_image_media_type']);
+    $testImageMediaType->save();
+
+    $this->createEntityReferenceField('media', $testImageMediaType->id(), 'field_media_of', 'Media Of', 'node', 'default', [], 2);
+
+    $node = $this->container->get('entity_type.manager')->getStorage('node')->create([
+      'type' => 'test_type',
+      'title' => 'node',
+    ]);
+    $node->save();
+
+    // Make an image for the Media.
+    $file = $this->container->get('entity_type.manager')->getStorage('file')->create([
+      'uid' => $account->id(),
+      'uri' => "public://test.jpeg",
+      'filename' => "test.jpeg",
+      'filemime' => "image/jpeg",
+      'status' => FILE_STATUS_PERMANENT,
+    ]);
+    $file->save();
+
+    $this->drupalGet("node/1/delete");
+    $assert_session->pageTextNotContains('Delete all associated medias and nodes');
+
+    // Make the media, and associate it with the image and node.
+    $media1 = $this->container->get('entity_type.manager')->getStorage('media')->create([
+      'bundle' => $testImageMediaType->id(),
+      'name' => 'Media1',
+      'field_media_image' =>
+        [
+          'target_id' => $file->id(),
+          'alt' => 'Some Alt',
+          'title' => 'Some Title',
+        ],
+      'field_media_of' => ['target_id' => $node->id()],
+    ]);
+    $media1->save();
+
+    $media2 = $this->container->get('entity_type.manager')->getStorage('media')->create([
+      'bundle' => $testImageMediaType->id(),
+      'name' => 'Media2',
+      'field_media_image' =>
+        [
+          'target_id' => $file->id(),
+          'alt' => 'Some Alt',
+          'title' => 'Some Title',
+        ],
+      'field_media_of' => ['target_id' => $node->id()],
+    ]);
+    $media2->save();
+
+    $this->drupalGet("admin/config/islandora/core");
+    $assert_session->pageTextContains('Node Delete with Media and Files');
+    \Drupal::configFactory()->getEditable('islandora.settings')->set('delete_media_and_files', TRUE)->save();
+
+    $delete = ['delete_associated_content' => TRUE];
+
+    $this->drupalGet("node/1/delete");
+    $assert_session->pageTextContains('Media1');
+    $assert_session->pageTextContains('Media2');
+    $this->submitForm($delete, 'Delete');
+
+    $assert_session->pageTextContains($media1->id());
+    $assert_session->pageTextContains($media2->id());
+
+    $this->drupalGet("media/1/delete");
+    $assert_session->pageTextContains('Page not found');
+
+    $this->drupalGet("media/2/delete");
+    $assert_session->pageTextContains('Page not found');
+
+    $this->drupalGet("/admin/content/files");
+    $assert_session->pageTextNotContains('test.jpeg');
+
+  }
+
+}