Working with uploaded files
On most platforms, media files uploaded to a WordPress site are stored in a filesystem local to the site’s web server. On the VIP Platform, media files uploaded or imported to a WordPress environment are stored on the VIP File System, which is an external object store. A WordPress site’s /wp-content/uploads/
directory is mapped to this external object store. This design provides better security and allows VIP’s CDN to more easily scale access to a site’s media.
The VIP File System is read-only but several different methods exist to programmatically access and interact with media files stored on the VIP File System.
Limitations
- Communication with the VIP File System occurs over HTTP. Because of this, a high number of filesystem-related function calls (e.g., lots of
file_exists()
checks) can result in a performance hit and should be used with caution. - Media can only be uploaded to a path within
/uploads/
. Only paths to files that begin with/wp-content/uploads/
are possible; other paths will not work. - Permissions-related operations (e.g.,
chmod()
,chown()
) will not work as expected and should not be used. - The system
/tmp
directory should be used to perform operations on temporary files and other local file operations. - Some themes and plugins expect to have write permissions, or that media files are stored locally, and may not work as expected with the VIP File System.
- All file names are treated as case-insensitive.
PHP Stream Wrapper
A custom PHP Stream Wrapper has been integrated into the VIP File System to allow for more complex interactions. This allows most filesystem functions (e.g. fopen()
, fwrite()
, file_put_contents()
) to automatically work with media uploads within the /wp-content/uploads/
path. Most open, read, and write file-related operations are supported by the PHP Stream Wrapper, including copy()
, rename()
, and delete()
.
Because the VIP File System uses an object store, it lacks a true directory structure. As a result:
- Most operations on directories will not work as expected.
- Directory traversal operations will not work because there are technically no true directories to traverse.
- Some directory-related operations like
mkdir()
will return true because directories are automatically created by the Filesystem API. - Because directories are created automatically, it is not necessary to add logic in code to check if a directory exists. If
file_exists()
is used in code to check if a directory exists, it will always returntrue
for paths within/wp-content/uploads/
. scandir()
,list_files()
, andopendir()
will not work as expected, and will instead return either an empty array orfalse
and trigger a PHP Warning.
WordPress filesystem functions
To generate correct, writeable paths use the WordPress function wp_get_upload_dir()
or wp_upload_dir()
. Hard-coding a path like /wp-content/uploads/...
will not work.
For simple file uploads use media_handle_sideload()
, media_sideload_image()
, or wp_upload_bits()
.
WP_Filesystem API
To use the WP_Filesystem API, the following code must be added to a site’s codebase usually within the /themes
directory, /plugins
directory, or /client-mu-plugins
directory:
global $wp_filesystem;
if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ) {
$creds = request_filesystem_credentials( site_url() );
wp_filesystem( $creds );
}
Overwriting and versioning files
Attempting to overwrite a file by uploading a new version of the file with the same name to a WordPress Media Library will not work as expected. By default, WordPress will not overwrite an existing file (e.g., image.jpg
) and will instead upload the newer file as a separate file with a version number appended to it (e.g., image-1.jpg
).
Files on the VIP File System, however, can be overwritten with new content. The updated version of the file will be available at the same path and file name.
- Files must be overwritten directly by name, at their exact location.
- A limit of 2000 modifications of any particular file path are permitted. Modifications beyond this limit will result in an error being thrown with a
405
HTTP response status code. Because of this, not all use cases for overwriting and versioning are ideal or supported. Plugins or features that use identical filenames (e.g., files with high volumes of updates such as logging) are likely to hit this limit. - Because files in the File System are typically not frequently modified, they have a very long cache-control header (up to 1 year). To ensure that requests to a recently modified file obtain the latest version, the file may need to be explicitly purged from the edge cache after replacement.
Overwriting with VIP-CLI
VIP-CLI command: vip import media [options] [command]
Overwrite a file by importing a newer version of the file with VIP-CLI.
- On the user’s local machine, create a directory structure that mimics the remote file’s path beginning with
uploads/
as the top-most directory, and the newer version of the file nested within it. For example, to overwrite a file located at the remote file pathhttps://example.com/uploads/2022/12/image.jpg
, the directory structure on the local machine should beuploads/2022/12/image.jpg
. - Archive the local
uploads/
directory and upload it to a publicly accessible remote URL. - Include the
--overwriteExistingFiles
option (or-o
) when importing the archived file to force the silent overwriting of the existing file. For example:vip @mytestsite.production import media "https://www.dropbox.com/s/uploads.tar.gz" -o
- If the newer version of the file does not immediately load as expected, the older version of the file may need to be purged from the cache with
vip cache purge-url [options]
.
Modifying the directory structure for media uploads
The directory structure for media uploads defaults to /uploads/YYYY/MM/
. This default can be overridden with the pre_option_uploads_use_yearmonth_folders
filter. This code example will omit the year (/YYYY/
) and month (/MM/
) path segments from the directory structure when added to a theme’s functions.php
file:
add_filter( 'pre_option_uploads_use_yearmonth_folders', function() { return '0'; }, 9999 );
- After deploying the above code example to an environment, new files uploaded to the site’s Media Library will have a URL structure similar to:
wp-content/uploads/{Filename}
for WordPress single sites and for the main site (ID 1) of a WordPress multisite.wp-content/uploads/sites/{Site_ID}/{Filename}
for a network site on a WordPress multisite.
- The directory structure of media files that were uploaded prior to adding the filter will remain unchanged, and will continue to be available at the
/uploads/YYYY/MM/
structure.
To apply other custom directory structures, combine the pre_option_uploads_use_yearmonth_folders
filter with the upload_dir
hook.
Code examples
In this code example, a file is uploaded to /uploads/csv/
:
$csv_content = '1,hello,admin';
$upload_dir = wp_get_upload_dir()['basedir'];
$file_path = $upload_dir . '/csv/updated.csv';
file_put_contents( $file_path, $csv_content );
// The file will now be accessible at https://example-com.go-vip.net/wp-content/uploads/csv/updated.csv
In this code example, a file is uploaded to /uploads/csv/
using the WP_Filesystem API:
$csv_content = '1,hello,admin';
$upload_dir = wp_get_upload_dir()['basedir'];
$file_path = $upload_dir . '/csv/updated.csv';
global $wp_filesystem;
if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ) {
$creds = request_filesystem_credentials( site_url() );
wp_filesystem( $creds );
}
$wp_filesystem->put_contents(
$file_path,
$csv_content
);
In this code example, a CSV file that was previously uploaded to WordPress is retrieved, parsed, and stored in a variable:
$csv_attachment = get_attached_file( 1234 );
$csv_file = file( $csv_attachment );
$csv_content = array_map( 'str_getcsv', $csv_file );
Last updated: May 30, 2023