The private tree mirrors the public uploads layout. The main site’s private files sit at the root of the private directory (private-uploads/2026/10/file.pdf); every other site’s sit under sites/ID/ (private-uploads/sites/60/2026/10/file.pdf), exactly as its public uploads sit under uploads/sites/ID/. Proxy URLs carry the same path below /protected-files/.
Releases before 1.5.0 put every site’s private files at the root, alongside the main site’s. On a subsite, a file still there is a legacy flat file (LH_Private_Uploads_Paths::is_legacy_flat_path()). It stays readable through the proxy only while that site’s own postmeta holds its exact path, can still be moved back to public, and once moved into its site’s directory, a request for its old flat URL is redirected (301) to the new one.
private_site_dir() is the current site’s own directory; public_to_private() and private_to_public() swap only a leading directory; path_belongs_to_current_site() checks a path is in the site’s own directory (and, on the main site, not in sites/). Also the /protected-files/ rewrite rule and the wp_get_attachment_url filter that returns proxy URLs._wp_attached_file to the absolute path and the guid. A move never overwrites an existing file: it fails with target_exists./protected-files/ request, refuses anything outside the private tree (404) or another site’s directory (403), looks the attachment up by exact absolute path in this site’s postmeta, runs the password or capability check and streams the file.lh_private_uploads_protected_statuses: post statuses whose files are kept private. Default private.lh_private_uploads_user_can_access: whether the current user may open a protected file. Starts false.lh_private_uploads_attachment_is_protected: per-attachment override of that decision.lh_private_uploads_password_form: the password form HTML.lh_private_uploads_metabox_statuses and lh_private_uploads_unregister_attachment_statuses: the statuses offered in the File Access control.LH_PRIVATE_UPLOADS_DIR: the private directory. Default private-uploads one level above the WordPress root.LH_PRIVATE_UPLOADS_XSENDFILE: true to stream files with X-Sendfile (mod_xsendfile) instead of PHP.