I was able to resolve this without using contrib. @leymannx's answer using FileField Paths is great, but I didn't want to install a contrib module for a one-time script. If you're familiar with batch processing items in hook_post_update_NAME(), it's a piece of cake.
Step 1: Update your field to use private files. (See step 1 of @leymannx's answer)
Step 2: Make sure the new private files directory (or directories) exists (the subdirectory where you want to move your files)
Here's my example:
/**
* Make sure the private files directory exists.
*/
function mymodule_update_9001(&$sandbox) {
$directory = 'private://MY-SUBDIRECTORY';
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$success = $file_system
->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
if (!$success) {
throw new UpdateException('The required directory does not exist (or exists and is not writable) or could not be created.');
}
}
Step 3: Move existing files to the new directory.
The code to do this is straightforward for a single file:
use Drupal\file\FileInterface;
// Move file to the private files directory.
$file = loadMyFileEntitySomehow();
if ($file instanceof FileInterface) {
// Note: $destination must be the full file uri, including file name.
// Additionally, the directory must exist and be writable.
$destination = 'private://MY-SUBDIRECTORY/' . $file->getFilename();
\Drupal::service('file.repository')
->move($file, $destination);
}
Example post_update to update all files in a specific field on a specific node type:
use Drupal\file\FileInterface;
/**
* Move files to the private files directory.
*/
function mymodule_post_update_make_files_private(&$sandbox) {
$batch_size = 25;
$entity_type = 'node';
$bundle = 'MY_BUNDLE';
if (!isset($sandbox['num_processed'])) {
$sandbox['num_processed'] = 0;
}
if (empty($sandbox['entity_ids'])) {
$sandbox['entity_ids'] = \Drupal::entityTypeManager()
->getStorage($entity_type)
->getQuery()
->condition('type', $bundle)
->exists('field_MY_FILE_FIELD')
->execute();
if (is_array($sandbox['entity_ids'])) {
$sandbox['total'] = count($sandbox['entity_ids']);
}
}
if (!empty($sandbox['entity_ids'])) {
$current_batch_ids = array_slice($sandbox['entity_ids'], $sandbox['num_processed'], $batch_size);
$current_batch_entities = \Drupal::entityTypeManager()
->getStorage($entity_type)
->loadMultiple($current_batch_ids);
foreach ($current_batch_entities as $entity) {
// Move files to the private files directory.
$files = $entity->field_MY_FILE_FIELD->referencedEntities();
foreach ($files as $file) {
if ($file instanceof FileInterface) {
\Drupal::service('file.repository')
->move($file, 'private://MY-SUBDIRECTORY/' . $file->getFilename());
}
}
$sandbox['num_processed']++;
}
}
if (!empty($sandbox['total'])) {
$sandbox['#finished'] = $sandbox['num_processed'] / $sandbox['total'];
}
else {
$sandbox['#finished'] = 1;
}
}