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
MySQL TIMESTAMP vs. DATETIME: The Ultimate Showdown on Time Zones, UTC, and Storage
Duration: 00:00 | DP | 2025-12-02 08:31:40The Ultimate 'Connection Refused' Guide: A PHP PDO & Docker Debugging Saga of a Forgotten Port
Duration: 00:00 | DP | 2025-12-03 09:03:20Solving the MySQL Docker "Permission Denied" Error on Synology NAS: A Step-by-Step Guide
Duration: 00:00 | DP | 2025-12-03 21:19:10NVM/Node Command Not Found in New macOS Terminals? A Two-Step Permanent Fix!
Duration: 00:00 | DP | 2025-12-04 09:35:00One-Command Website Stability Check: The Ultimate Curl Latency Test Script for Zsh
Duration: 00:00 | DP | 2025-12-07 23:25:50How Can a Docker Container Access the Mac Host? The Ultimate Guide to Connecting to Nginx
Duration: 00:00 | DP | 2025-12-08 23:57:30Recommended
IPv6 Demystified: Can You Still Use Ports with DDNS Like in IPv4?
00:00 | 8New to IPv6 and wondering if it supports ports for...
“Claude Code requires Git Bash” Error on Windows? Here's the Easy Fix
00:00 | 355Encountering the "Claude Code on Windows requires ...
Multilingual SEO Showdown: URL Parameters vs. Subdomains vs. Subdirectories—Which is Best?
00:00 | 21Choosing a URL structure for your multilingual web...
Bootstrap Border Magic: Instantly Add Top or Bottom Borders to Elements
00:00 | 8Tired of writing custom CSS for simple 1px borders...