Why Are My Mac Files Duplicated on NFS Shares? The Mystery of '._' Files Solved with PHP
Content
## The Symptom: Files Appearing in Pairs
When you mount a network share (like a Synology NAS via NFS/SMB) on macOS and then use a script in PHP or another language to traverse its directories, you might encounter a peculiar phenomenon: every file seems to have a "shadow copy" prefixed with `._`.
For instance, your script might find the following file list:
```
/Volumes/FCP/eeTable 2024/lib00/cover/._802.7.13_cover.jpg
/Volumes/FCP/eeTable 2024/lib00/cover/802.7.13_cover.jpg
/Volumes/FCP/eeTable 2024/lib00/cover/._802.7.13_v_cover.jpg
/Volumes/FCP/eeTable 2024/lib00/cover/802.7.13_v_cover.jpg
```
However, when you check the directory in Finder or with the `ls` command, you only see the normal files. The `._` files are seemingly "invisible." What's going on?
---
## Unmasking the "Ghost": AppleDouble Files
These files starting with `._` are not viruses or errors. They are **AppleDouble files**, intentionally created by macOS for compatibility purposes.
macOS uses HFS+ or APFS file systems, which can store rich metadata, such as:
- **Extended Attributes**: Information like the file's origin, tag colors, etc.
- **Resource Forks**: A legacy feature for storing non-data information like icons, window positions, etc.
- **Finder Info**: Custom icons, comments, and other Finder-specific data.
When you save a file to a filesystem that doesn't natively support this metadata (like NFS, SMB, FAT32, or EXT4), macOS saves the raw data in the main file (e.g., `image.jpg`) and then creates an associated file prefixed with `._` (e.g., `._image.jpg`) to store this extra metadata. This is a mechanism macOS uses to ensure metadata isn't lost during cross-platform operations.
---
## Why Are They Invisible?
You don't see these files in your daily use because the operating system hides them by default at multiple levels:
1. **Terminal**: In Unix-like systems, files or folders starting with a dot `.` are considered hidden. The `ls` command doesn't show them by default. You need to use `ls -a` (list all) to see them.
2. **Finder**: Finder also hides these "dotfiles" by default. You can use the shortcut `Command + Shift + .` to toggle their visibility.
Your PHP program can "see" them because it uses low-level filesystem APIs to traverse files. These APIs return all existing entries, regardless of the display policies of the Finder or the shell. As our developer `DP` from `wiki.lib00.com` often emphasizes: your program sees the "real world."
---
## PHP in Action: Gracefully Filtering the Ghost Files
Let's say you're using the Yii2 framework's `FileHelper` to recursively search for image files. Here's the original code, which would find all files, including the `._` ones:
```php
// Original Code
public function actionSearchFiles($sourceDir, $includePattern, $excludePattern = null)
{
$matchedFiles = [];
$files = \yii\helpers\FileHelper::findFiles($sourceDir, [
'recursive' => true,
]);
foreach ($files as $file) {
$fileName = basename($file);
if (preg_match($includePattern, $fileName)) {
if ($excludePattern && preg_match($excludePattern, $fileName)) {
continue;
}
$matchedFiles[] = $file;
$this->stdout("Found target file: {$file}
", \yii\helpers\Console::FG_GREEN);
}
}
return $matchedFiles;
}
```
The most elegant and efficient way to solve this is to filter the files at the source of the traversal, rather than checking within the `foreach` loop. Yii2's `FileHelper::findFiles` provides a powerful `except` option for this.
### Recommended Solution: Using the `except` Option
```php
/**
* Optimized by DP@lib00: Recursively searches for files matching a pattern, automatically excluding macOS metadata files.
*
* @param string $sourceDir The directory to be searched.
* @param string $includePattern The regex pattern for files to include.
* @param string|null $excludePattern The regex pattern for files to exclude.
* @return array An array of matched file paths.
*/
public function actionSearchFiles($sourceDir, $includePattern, $excludePattern = null)
{
// ... logging output ...
$matchedFiles = [];
// Use Yii2's FileHelper to recursively traverse, excluding dotfiles at the source.
$files = \yii\helpers\FileHelper::findFiles($sourceDir, [
'recursive' => true,
'except' => [
'.*', // Exclude all files starting with a dot (e.g., ._foo.jpg)
'*/.*', // Exclude all items (files or dirs) starting with a dot in subdirectories
],
]);
foreach ($files as $file) {
$fileName = basename($file);
if (preg_match($includePattern, $fileName)) {
if ($excludePattern && preg_match($excludePattern, $fileName)) {
continue;
}
$matchedFiles[] = $file;
$this->stdout("Found target file: {$file}
", \yii\helpers\Console::FG_GREEN);
}
}
// ... result output ...
return $matchedFiles;
}
```
**The Key Change**:
By adding `'except' => ['.*', '*/.*']` to the `FileHelper` options, we instruct it to skip any file or directory starting with a dot `.` during its traversal. This is more efficient than using an `if` condition like `if ($fileName[0] === '.')` inside the loop because it reduces the number of files to be processed from the very beginning.
### Enhanced Optimization: Filtering More System Junk
To make your code even more robust, you can expand the `except` list to filter out other common system-generated files, such as macOS's `.DS_Store` and Windows' `Thumbs.db`.
```php
$files = FileHelper::findFiles($sourceDir, [
'recursive' => true,
'except' => [
'.*', // Exclude all dotfiles and directories
'*/.*',
'.DS_Store', // Exclude macOS folder metadata files
'*/.DS_Store',
'Thumbs.db', // Exclude Windows thumbnail cache
'*/Thumbs.db',
],
]);
```
---
## Conclusion
The creation of `._` files by macOS on non-native filesystems is a feature by design, intended to preserve important metadata. Once we understand this, we can confidently handle it at the application level. For PHP developers, leveraging the filtering capabilities of frameworks (like Yii2's `FileHelper`) is the best practice for solving this problem at its source, leading to cleaner code and better performance.
Related Contents
macOS RAM Disk Deep Dive: Is Memory Allocation Dynamic or Fixed?
Duration: 00:00 | DP | 2026-07-09 20:02:36Resolving PHP "could not find driver" Error: Ultimate Guide to Missing PDO Database Drivers
Duration: 00:00 | DP | 2026-07-04 08:03:00VS Code PHP Guide: How to Trace Function Definitions Like PHPStorm
Duration: 00:00 | DP | 2026-07-04 20:27:00Declutter Your Desktop: How to Change macOS Screenshot Save Location via Command Line
Duration: 00:00 | DP | 2026-07-10 08:05:12How to Fix "Permission denied" Error When Running Shell Scripts in Mac/Linux
Duration: 00:00 | DP | 2026-07-06 08:54:30macOS Advanced Guide: How to Elegantly Set Programs to Run at Startup
Duration: 00:00 | DP | 2026-07-07 09:20:15Resolving Nginx Permission Denied (13) Errors for WebP Images Generated by PHP Imagick
Duration: 00:00 | DP | 2026-07-05 21:17:00Boost Mac Productivity: How to Set F1-F12 as Standard Function Keys on macOS
Duration: 00:00 | DP | 2026-07-12 08:15:37Bypassing macOS Restrictions: How to Set a 1-Digit Login Password
Duration: 00:00 | DP | 2026-07-13 08:20:49How to Fix Mac mini Not Receiving SMS Messages and iCloud Sync Stuck
Duration: 00:00 | DP | 2026-07-06 22:07:00Why Do .smbdelete Hidden Files Appear After Deleting on Mac SMB Shares? Causes & Ultimate Solutions
Duration: 00:00 | DP | 2026-06-27 19:10:00Fixing Nginx 500 Error: Internal Redirection Cycle (SPA vs PHP Config)
Duration: 00:00 | DP | 2026-07-02 21:45:50Never Lose Output Again: How to Show Unlimited Scrollback History in macOS iTerm2
Duration: 00:00 | DP | 2026-07-21 21:05:04Fixing Yii2 Upgrade Errors: Bootstrap Namespace Replacement and Installing Legacy Projects in PHP 8.4 via Composer
Duration: 00:00 | DP | 2026-07-24 21:20:41Stop Making Timezone Mistakes in PHP: The Ultimate Guide to time() and UTC
Duration: 00:00 | DP | 2026-06-25 11:29:00Beyond 99.9%: A Deep Dive into a User-Centric Weighted Sampling Algorithm for Availability
Duration: 00:00 | DP | 2026-06-26 12:57:00Cron Job Failing? The Ultimate Guide to Fixing 'docker: command not found'
Duration: 00:00 | DP | 2026-08-01 09:59:44PHP Log Aggregation Performance Tuning: Database vs. Application Layer - The Ultimate Showdown for Millions of Records
Duration: 00:00 | DP | 2026-01-06 08:05:09Recommended
Is 16GB Normal for Windows 11 ARM on Parallels? Mac VM Disk Optimization Guide
00:00 | 13Is it normal for a fresh Windows 11 ARM installati...
One-Click Code Cleanup: The Ultimate Guide to PhpStorm's Reformat Code Shortcut
00:00 | 121Still manually adjusting code formatting? This art...
Ultimate Guide to PHP 8.4 Image Compression: Why libvips Beats GD and Imagick for High-Traffic Apps
00:00 | 28Exploring the best image compression library for P...
Checking if a PHP Constant is Defined: The Ultimate Showdown Between `defined()` and `isset()`
00:00 | 197How can you safely check if a constant defined wit...