9 • 16 • 22 natesymer
If there's one pain point in using WordPress, it's the lack of separation between content and configuration/code. While its usually plugins and themes outside of wordpress core that are guilty, WordPress core itself is also guilty in one crucial area: updates. Let me explain:
Whenever a user with Administrator privileges updates core or installs/updates plugins/themes, code is downloaded to a temporary location in the form of an archive, extracted to a temporary location, and then copied into the appropriate place inside ABSPATH
. This wouldn't be an issue, except this is WordPress and there's always some vestige of a legacy version that adds complexity/difficulty. In this instance, it's the wp-content/upgrade/
directory inside ABSPATH
that WordPress extracts archives (that it already downloaded to /tmp
!) to. In these days where /tmp
is often in RAM and filesystems can be shared/distributed and/or remote, this makes no sense at all (in fact, I developed the solution you'll see further down the page for a hosting environment I designed and implemented).
So, what to do about this problem, eh? I couldn't find any configuration constant that would change the location, so I searched the whole source tree of WordPress core for the string upgrade/
and found it in wp-admin/includes/class-wp-upgrader.php
, in the method WP_Updater::unpack_package
.
To try to get a documented, non-hacky solution, I opened a ticket with WordPress, citing my use case (WordPress on NFS to enable scaling out w/ containers that have /tmp
in RAM). It's been a while and I've lost track of it, but I offered a solution where there was a PHP constant, WP_UPGRADE_DIR
that would change where WordPress extracted packages. This idea was met with indignation, and admonishment for not knowing that "that's how we ensure the wp-content directory gets created!". I countered that WP_Upgrader::unpack_package
could simply ensure that wp-content/
exists without needing to use the upgrade
subdirectory. Again, I was met with hostility, and this time, my ticket was marked as wontfix and closed with no explanation. I'm glad WordPress Core is maintained by such openminded gentlemen!
Now, I wasn't going to take any garbage, so I went back to the source tree and followed the code through to the call to WP_Upgrader::unpack_package
to see if there were any opportunities to make a call to runkit7_method_redefine
after WP_Upgrader
was defined but also before the call to WP_Upgrader::unpack_package
was called. And I was in luck, there is a filter called upgrader_package_options
that gets called in WP_Upgrader::run
before that method calls WP_Upgrader::unpack_package
. It's a bit hacky because it's a filter and not an action - callbacks for filters should be pure predicates, not impure ones that cause side effects like redefining a method! But here is the solution using runkit7
nonetheless (use at your own peril!):
add_filter('upgrader_package_options', function($options) {
runkit7_method_redefine("WP_Upgrader", "unpack_package", function($package, $delete_package = true) {
// START: copied from original method
global $wp_filesystem;
$this->skin->feedback( 'unpack_package' );
// EDITED: This line is modified.
$upgrade_folder = "/tmp/wp_upgrade/"; // $wp_filesystem->wp_content_dir() . 'upgrade/';
// Clean up contents of upgrade directory beforehand.
$upgrade_files = $wp_filesystem->dirlist( $upgrade_folder );
if ( ! empty( $upgrade_files ) ) {
foreach ( $upgrade_files as $file ) {
$wp_filesystem->delete( $upgrade_folder . $file['name'], true );
}
}
// We need a working directory - strip off any .tmp or .zip suffixes.
$working_dir = $upgrade_folder . basename( basename( $package, '.tmp' ), '.zip' );
// Clean up working directory.
if ( $wp_filesystem->is_dir( $working_dir ) ) {
$wp_filesystem->delete( $working_dir, true );
}
// Unzip package to working directory.
$result = unzip_file( $package, $working_dir );
// Once extracted, delete the package if required.
if ( $delete_package ) {
unlink( $package );
}
if ( is_wp_error( $result ) ) {
$wp_filesystem->delete( $working_dir, true );
if ( 'incompatible_archive' === $result->get_error_code() ) {
return new WP_Error( 'incompatible_archive', $this->strings['incompatible_archive'], $result->get_error_data() );
}
return $result;
}
return $working_dir;
// END: copied from original method
}, RUNKIT_ACC_PUBLIC);
return $options;
});
Here are a couple caveats:
- This is wholly undocumented. While it's unlikely to change (see maintainers' unwillingness to change this code), there is no guarantee. You'll need to check to make sure WP Core didn't change significantly every time you update it.
- If your WordPress install is on NFS or another networked filesystem, be aware that updates will still need to be copied from your
/tmp
folder to your wordpress install. See below:
With patch (4 RAM ops, 1 NFS op): download (RAM write) > extract (RAM read, RAM write) > copy (RAM read, NFS write)
Without patch (2 RAM ops, 1.5-3* NFS ops): download (RAM write) > extract (RAM read, NFS write) > copy (NFS copy)
* Notice the extra NFS copy? At the time of writing (9.16.22), major NFS providers like AWS EFS don't use the version of NFS that supports server side copy (v4.2), they use an older version. Server Side Copy (SSC) allows an NFS client to tell the NFS share to do the copy entirely on its end, without sending data over the wire. Without SSC, that NFS copy becomes an NFS read/write pair. What's more, NFS isn't the only distributed filesystem solution out there, and there are others that suffer from the same problem SSC solves.