Skip to content

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 return true for paths within /wp-content/uploads/.
  • scandir(), list_files(), and opendir()  will not work as expected, and will instead return either an empty array or false 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 path https://example.com/uploads/2022/12/image.jpg, the directory structure on the local machine should be uploads/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:

/themes/example-theme/functions.php
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

Relevant to

  • WordPress