PHP ftp_mdtm() 函数

ftp_mdtm() 函数获取FTP服务器上指定文件的最后修改时间。

注意:不是所有FTP服务器都支持此功能。如果服务器不支持,函数将返回-1。

语法

ftp_mdtm(resource $ftp_stream, string $remote_file): int

参数说明

参数 描述
$ftp_stream 必需。FTP连接的资源标识符,由ftp_connect()ftp_ssl_connect()函数返回。
$remote_file 必需。远程文件的路径。

返回值

  • 成功时返回文件的最后修改时间(Unix时间戳)
  • 失败时返回 -1(如果文件不存在、服务器不支持或发生错误)

示例

示例1:基本使用

获取远程文件的最后修改时间:

<?php
// FTP服务器信息
$ftp_server = "ftp.example.com";
$ftp_user = "username";
$ftp_pass = "password";

// 建立FTP连接
$conn_id = ftp_connect($ftp_server);
if ($conn_id === false) {
    die("无法连接到FTP服务器");
}

// 登录FTP服务器
if (!ftp_login($conn_id, $ftp_user, $ftp_pass)) {
    ftp_close($conn_id);
    die("FTP登录失败");
}

// 开启被动模式
ftp_pasv($conn_id, true);

// 远程文件
$remote_file = "public_html/index.php";

// 获取文件修改时间
$timestamp = ftp_mdtm($conn_id, $remote_file);

if ($timestamp != -1) {
    echo "文件: $remote_file\n";
    echo "最后修改时间 (Unix时间戳): $timestamp\n";
    echo "格式化时间: " . date('Y-m-d H:i:s', $timestamp) . "\n";
    echo "相对时间: " . timeAgo($timestamp) . "\n";
} else {
    echo "无法获取文件修改时间\n";
    echo "可能原因:\n";
    echo "1. 文件不存在\n";
    echo "2. 服务器不支持MDTM命令\n";
    echo "3. 权限不足\n";
}

// 关闭连接
ftp_close($conn_id);

// 辅助函数:将时间戳转换为相对时间
function timeAgo($timestamp) {
    $current_time = time();
    $time_diff = $current_time - $timestamp;

    if ($time_diff < 60) {
        return "刚刚";
    } elseif ($time_diff < 3600) {
        $minutes = floor($time_diff / 60);
        return "{$minutes}分钟前";
    } elseif ($time_diff < 86400) {
        $hours = floor($time_diff / 3600);
        return "{$hours}小时前";
    } elseif ($time_diff < 2592000) {
        $days = floor($time_diff / 86400);
        return "{$days}天前";
    } elseif ($time_diff < 31536000) {
        $months = floor($time_diff / 2592000);
        return "{$months}个月前";
    } else {
        $years = floor($time_diff / 31536000);
        return "{$years}年前";
    }
}
?>

示例2:检查文件是否更新

<?php
/**
 * 检查远程文件是否比本地文件新
 */
class FileSyncChecker {
    private $conn;

    public function __construct($host, $username, $password) {
        $this->conn = ftp_connect($host);
        if (!$this->conn) {
            throw new Exception("无法连接到FTP服务器");
        }

        if (!ftp_login($this->conn, $username, $password)) {
            ftp_close($this->conn);
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->conn, true);
    }

    /**
     * 检查文件是否需要更新
     */
    public function checkFileForUpdate($remote_file, $local_file) {
        // 获取远程文件修改时间
        $remote_mtime = ftp_mdtm($this->conn, $remote_file);
        if ($remote_mtime == -1) {
            throw new Exception("无法获取远程文件修改时间: $remote_file");
        }

        // 检查本地文件是否存在
        if (file_exists($local_file)) {
            $local_mtime = filemtime($local_file);

            echo "文件比较: " . basename($remote_file) . "\n";
            echo "远程修改时间: " . date('Y-m-d H:i:s', $remote_mtime) . "\n";
            echo "本地修改时间: " . date('Y-m-d H:i:s', $local_mtime) . "\n";

            if ($remote_mtime > $local_mtime) {
                $time_diff = $remote_mtime - $local_mtime;
                echo "远程文件较新 (" . formatTimeDiff($time_diff) . ")\n";
                return true;
            } elseif ($remote_mtime < $local_mtime) {
                $time_diff = $local_mtime - $remote_mtime;
                echo "本地文件较新 (" . formatTimeDiff($time_diff) . ")\n";
                return false;
            } else {
                echo "文件时间相同\n";
                return false;
            }
        } else {
            echo "本地文件不存在,需要下载\n";
            return true;
        }
    }

    /**
     * 批量检查多个文件
     */
    public function batchCheck($file_pairs) {
        $results = [
            'need_update' => [],
            'up_to_date' => [],
            'errors' => []
        ];

        foreach ($file_pairs as $pair) {
            $remote_file = $pair['remote'];
            $local_file = $pair['local'];

            try {
                if ($this->checkFileForUpdate($remote_file, $local_file)) {
                    $results['need_update'][] = [
                        'remote' => $remote_file,
                        'local' => $local_file
                    ];
                } else {
                    $results['up_to_date'][] = [
                        'remote' => $remote_file,
                        'local' => $local_file
                    ];
                }
            } catch (Exception $e) {
                $results['errors'][] = [
                    'file' => $remote_file,
                    'error' => $e->getMessage()
                ];
            }
        }

        return $results;
    }

    /**
     * 获取远程目录中所有文件的修改时间
     */
    public function getRemoteFileTimes($directory = '.') {
        $file_times = [];

        $files = ftp_nlist($this->conn, $directory);
        if ($files === false) {
            throw new Exception("无法获取目录列表: $directory");
        }

        foreach ($files as $file) {
            if ($file == '.' || $file == '..') {
                continue;
            }

            $remote_path = $directory . '/' . basename($file);

            // 检查是否为目录
            $is_dir = (ftp_size($this->conn, $remote_path) == -1);

            if (!$is_dir) {
                $mtime = ftp_mdtm($this->conn, $remote_path);
                if ($mtime != -1) {
                    $file_times[] = [
                        'filename' => basename($file),
                        'path' => $remote_path,
                        'mtime' => $mtime,
                        'formatted_time' => date('Y-m-d H:i:s', $mtime),
                        'size' => ftp_size($this->conn, $remote_path)
                    ];
                }
            }
        }

        // 按修改时间排序
        usort($file_times, function($a, $b) {
            return $b['mtime'] - $a['mtime'];
        });

        return $file_times;
    }

    public function close() {
        if (is_resource($this->conn)) {
            ftp_close($this->conn);
        }
    }

    public function __destruct() {
        $this->close();
    }
}

// 辅助函数:格式化时间差
function formatTimeDiff($seconds) {
    if ($seconds < 60) {
        return "{$seconds}秒";
    } elseif ($seconds < 3600) {
        $minutes = floor($seconds / 60);
        return "{$minutes}分钟";
    } elseif ($seconds < 86400) {
        $hours = floor($seconds / 3600);
        return "{$hours}小时";
    } else {
        $days = floor($seconds / 86400);
        return "{$days}天";
    }
}

// 使用示例
try {
    $checker = new FileSyncChecker("ftp.example.com", "username", "password");

    echo "=== 检查单个文件 ===\n";
    $need_update = $checker->checkFileForUpdate(
        "public_html/index.php",
        "./local/index.php"
    );
    echo "需要更新: " . ($need_update ? "是" : "否") . "\n";

    echo "\n=== 获取远程文件列表 ===\n";
    $files = $checker->getRemoteFileTimes("public_html");
    echo "找到 " . count($files) . " 个文件:\n";
    foreach ($files as $file) {
        echo "- {$file['filename']} ({$file['formatted_time']}, " .
             formatBytes($file['size']) . ")\n";
    }

    echo "\n=== 批量检查 ===\n";
    $pairs = [
        ['remote' => 'public_html/style.css', 'local' => './local/style.css'],
        ['remote' => 'public_html/script.js', 'local' => './local/script.js'],
        ['remote' => 'public_html/images/logo.png', 'local' => './local/images/logo.png']
    ];

    $results = $checker->batchCheck($pairs);
    echo "需要更新: " . count($results['need_update']) . " 个文件\n";
    echo "已是最新: " . count($results['up_to_date']) . " 个文件\n";
    echo "错误: " . count($results['errors']) . " 个\n";

    $checker->close();

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}

// 辅助函数:格式化文件大小
function formatBytes($bytes, $precision = 2) {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);
    $bytes /= pow(1024, $pow);
    return round($bytes, $precision) . ' ' . $units[$pow];
}
?>

示例3:Web界面的文件时间查看器

<?php
// Web界面文件时间查看器
session_start();

// FTP配置
$ftp_config = [
    'server' => 'ftp.example.com',
    'username' => 'webuser',
    'password' => 'password',
    'root_dir' => '/public_html/'
];

// 处理目录浏览请求
$current_dir = $_GET['dir'] ?? $ftp_config['root_dir'];
$current_dir = rtrim($current_dir, '/') . '/';

// 连接到FTP服务器
$file_list = [];
$error = null;

$conn = ftp_connect($ftp_config['server']);
if ($conn && ftp_login($conn, $ftp_config['username'], $ftp_config['password'])) {
    ftp_pasv($conn, true);

    // 尝试切换到指定目录
    if (@ftp_chdir($conn, $current_dir)) {
        $files = ftp_nlist($conn, ".");

        if ($files !== false) {
            foreach ($files as $file) {
                $filename = basename($file);
                if ($filename == '.' || $filename == '..') {
                    continue;
                }

                $file_path = $current_dir . $filename;

                // 检查是否为目录
                $is_dir = (ftp_size($conn, $file_path) == -1);

                if ($is_dir) {
                    // 目录
                    $file_list[] = [
                        'name' => $filename,
                        'type' => 'directory',
                        'path' => $file_path,
                        'icon' => 'fa-folder',
                        'color' => '#ffc107'
                    ];
                } else {
                    // 文件 - 获取详细信息
                    $mtime = ftp_mdtm($conn, $file_path);
                    $size = ftp_size($conn, $file_path);

                    $file_list[] = [
                        'name' => $filename,
                        'type' => 'file',
                        'path' => $file_path,
                        'size' => $size,
                        'formatted_size' => formatFileSize($size),
                        'mtime' => $mtime,
                        'formatted_time' => $mtime != -1 ? date('Y-m-d H:i:s', $mtime) : '未知',
                        'relative_time' => $mtime != -1 ? getRelativeTime($mtime) : '未知',
                        'icon' => getFileIcon($filename),
                        'color' => getFileColor($filename)
                    ];
                }
            }
        }
    } else {
        $error = "无法访问目录: $current_dir";
    }

    ftp_close($conn);
} else {
    $error = "FTP连接失败";
}

// 按类型和名称排序
usort($file_list, function($a, $b) {
    if ($a['type'] != $b['type']) {
        return $a['type'] == 'directory' ? -1 : 1;
    }
    return strcasecmp($a['name'], $b['name']);
});

// 辅助函数
function getFileIcon($filename) {
    $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    $icons = [
        'pdf' => 'fa-file-pdf',
        'txt' => 'fa-file-alt',
        'html' => 'fa-file-code',
        'htm' => 'fa-file-code',
        'php' => 'fa-file-code',
        'css' => 'fa-file-code',
        'js' => 'fa-file-code',
        'jpg' => 'fa-file-image',
        'jpeg' => 'fa-file-image',
        'png' => 'fa-file-image',
        'gif' => 'fa-file-image',
        'zip' => 'fa-file-archive',
        'rar' => 'fa-file-archive',
        'doc' => 'fa-file-word',
        'docx' => 'fa-file-word',
        'xls' => 'fa-file-excel',
        'xlsx' => 'fa-file-excel'
    ];

    return isset($icons[$extension]) ? $icons[$extension] : 'fa-file';
}

function getFileColor($filename) {
    $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    $colors = [
        'pdf' => '#e74c3c',
        'txt' => '#95a5a6',
        'html' => '#e67e22',
        'htm' => '#e67e22',
        'php' => '#3498db',
        'css' => '#9b59b6',
        'js' => '#f1c40f',
        'jpg' => '#2ecc71',
        'jpeg' => '#2ecc71',
        'png' => '#2ecc71',
        'gif' => '#2ecc71',
        'zip' => '#f39c12',
        'rar' => '#f39c12',
        'doc' => '#2980b9',
        'docx' => '#2980b9',
        'xls' => '#27ae60',
        'xlsx' => '#27ae60'
    ];

    return isset($colors[$extension]) ? $colors[$extension] : '#7f8c8d';
}

function formatFileSize($bytes) {
    if ($bytes >= 1073741824) {
        return number_format($bytes / 1073741824, 2) . ' GB';
    } elseif ($bytes >= 1048576) {
        return number_format($bytes / 1048576, 2) . ' MB';
    } elseif ($bytes >= 1024) {
        return number_format($bytes / 1024, 2) . ' KB';
    } else {
        return $bytes . ' 字节';
    }
}

function getRelativeTime($timestamp) {
    $current_time = time();
    $time_diff = $current_time - $timestamp;

    if ($time_diff < 60) {
        return '刚刚';
    } elseif ($time_diff < 3600) {
        $minutes = floor($time_diff / 60);
        return $minutes . '分钟前';
    } elseif ($time_diff < 86400) {
        $hours = floor($time_diff / 3600);
        return $hours . '小时前';
    } elseif ($time_diff < 2592000) {
        $days = floor($time_diff / 86400);
        return $days . '天前';
    } else {
        return date('Y-m-d', $timestamp);
    }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>FTP文件时间查看器</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <style>
        .file-item {
            border-radius: 8px;
            transition: all 0.2s;
            border: 1px solid #dee2e6;
        }
        .file-item:hover {
            background-color: #f8f9fa;
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        .file-icon {
            font-size: 1.5em;
            margin-right: 10px;
        }
        .file-name {
            font-weight: 500;
            word-break: break-all;
        }
        .file-meta {
            font-size: 0.85em;
            color: #6c757d;
        }
        .breadcrumb {
            background-color: #e9ecef;
            border-radius: 5px;
            padding: 10px 15px;
        }
        .time-badge {
            font-size: 0.75em;
        }
        .directory-link {
            text-decoration: none;
            color: inherit;
        }
        .directory-link:hover {
            color: #0d6efd;
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <div class="row mb-4">
            <div class="col">
                <h1><i class="fas fa-clock me-2"></i>FTP文件时间查看器</h1>
                <p class="text-muted">查看FTP服务器上文件的修改时间</p>
            </div>
        </div>

        <?php if ($error): ?>
            <div class="alert alert-danger">
                <i class="fas fa-exclamation-triangle me-2"></i><?php echo htmlspecialchars($error); ?>
            </div>
        <?php endif; ?>

        <!-- 面包屑导航 -->
        <div class="row mb-3">
            <div class="col">
                <nav aria-label="breadcrumb">
                    <ol class="breadcrumb">
                        <?php
                        $path_parts = explode('/', trim($current_dir, '/'));
                        $current_path = '';
                        echo '<li class="breadcrumb-item"><a href="?dir=' . urlencode($ftp_config['root_dir']) . '">根目录</a></li>';

                        foreach ($path_parts as $part) {
                            if (!empty($part)) {
                                $current_path .= '/' . $part;
                                echo '<li class="breadcrumb-item"><a href="?dir=' . urlencode($current_path . '/') . '">' .
                                     htmlspecialchars($part) . '</a></li>';
                            }
                        }
                        ?>
                    </ol>
                </nav>
            </div>
        </div>

        <!-- 文件列表 -->
        <div class="row">
            <div class="col">
                <div class="card">
                    <div class="card-header">
                        <h5 class="mb-0">文件列表 - <?php echo htmlspecialchars($current_dir); ?></h5>
                    </div>
                    <div class="card-body">
                        <?php if (empty($file_list)): ?>
                            <div class="text-center py-5">
                                <i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
                                <p class="text-muted">目录为空</p>
                            </div>
                        <?php else: ?>
                            <div class="row row-cols-1 row-cols-md-2 g-4">
                                <?php foreach ($file_list as $file): ?>
                                    <div class="col">
                                        <div class="file-item p-3 h-100">
                                            <div class="d-flex align-items-start">
                                                <i class="fas <?php echo $file['icon']; ?> file-icon"
                                                   style="color: <?php echo $file['color']; ?>"></i>
                                                <div class="flex-grow-1">
                                                    <?php if ($file['type'] == 'directory'): ?>
                                                        <a href="?dir=<?php echo urlencode($file['path']); ?>"
                                                           class="directory-link">
                                                            <div class="file-name">
                                                                <i class="fas fa-folder me-1"></i>
                                                                <?php echo htmlspecialchars($file['name']); ?>
                                                            </div>
                                                        </a>
                                                    <?php else: ?>
                                                        <div class="file-name">
                                                            <?php echo htmlspecialchars($file['name']); ?>
                                                        </div>
                                                        <div class="file-meta mt-2">
                                                            <span class="badge bg-secondary me-2">
                                                                <i class="fas fa-hdd me-1"></i>
                                                                <?php echo $file['formatted_size']; ?>
                                                            </span>
                                                            <span class="badge bg-info time-badge">
                                                                <i class="fas fa-clock me-1"></i>
                                                                <?php echo $file['relative_time']; ?>
                                                            </span>
                                                        </div>
                                                        <div class="file-meta mt-1 small">
                                                            <i class="fas fa-calendar me-1"></i>
                                                            <?php echo $file['formatted_time']; ?>
                                                        </div>
                                                    <?php endif; ?>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                <?php endforeach; ?>
                            </div>
                        <?php endif; ?>
                    </div>
                    <div class="card-footer text-muted">
                        <small>
                            <i class="fas fa-info-circle me-1"></i>
                            共 <?php echo count($file_list); ?> 个项目
                            <?php
                            $file_count = 0;
                            $dir_count = 0;
                            foreach ($file_list as $file) {
                                if ($file['type'] == 'file') {
                                    $file_count++;
                                } else {
                                    $dir_count++;
                                }
                            }
                            echo "($dir_count 个目录, $file_count 个文件)";
                            ?>
                        </small>
                    </div>
                </div>
            </div>
        </div>

        <!-- 统计信息 -->
        <div class="row mt-4">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h6 class="mb-0"><i class="fas fa-chart-bar me-2"></i>文件类型统计</h6>
                    </div>
                    <div class="card-body">
                        <?php
                        $file_types = [];
                        foreach ($file_list as $file) {
                            if ($file['type'] == 'file') {
                                $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
                                $file_types[$ext] = isset($file_types[$ext]) ? $file_types[$ext] + 1 : 1;
                            }
                        }
                        arsort($file_types);
                        ?>
                        <ul class="list-unstyled mb-0">
                            <?php foreach ($file_types as $ext => $count): ?>
                                <li>
                                    <i class="fas fa-file me-2"></i>
                                    .<?php echo htmlspecialchars($ext ?: '无扩展名'); ?>
                                    <span class="badge bg-primary float-end"><?php echo $count; ?></span>
                                </li>
                            <?php endforeach; ?>
                        </ul>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h6 class="mb-0"><i class="fas fa-calendar-alt me-2"></i>最近修改的文件</h6>
                    </div>
                    <div class="card-body">
                        <?php
                        $recent_files = [];
                        foreach ($file_list as $file) {
                            if ($file['type'] == 'file' && $file['mtime'] != -1) {
                                $recent_files[] = $file;
                            }
                        }

                        usort($recent_files, function($a, $b) {
                            return $b['mtime'] - $a['mtime'];
                        });

                        $recent_files = array_slice($recent_files, 0, 5);
                        ?>
                        <ul class="list-unstyled mb-0">
                            <?php foreach ($recent_files as $file): ?>
                                <li class="mb-2">
                                    <div class="d-flex justify-content-between">
                                        <span class="text-truncate" title="<?php echo htmlspecialchars($file['name']); ?>">
                                            <?php echo htmlspecialchars($file['name']); ?>
                                        </span>
                                        <span class="badge bg-secondary">
                                            <?php echo $file['relative_time']; ?>
                                        </span>
                                    </div>
                                </li>
                            <?php endforeach; ?>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 为文件项添加悬停效果
            document.querySelectorAll('.file-item').forEach(function(item) {
                item.addEventListener('mouseenter', function() {
                    this.style.cursor = 'pointer';
                });

                item.addEventListener('click', function() {
                    const link = this.querySelector('a.directory-link');
                    if (link) {
                        link.click();
                    }
                });
            });

            // 更新页面标题显示当前目录
            const currentDir = "<?php echo htmlspecialchars(basename(rtrim($current_dir, '/')) ?: '根目录'); ?>";
            document.title = currentDir + ' - FTP文件时间查看器';
        });
    </script>
</body>
</html>

示例4:基于时间的文件同步

<?php
/**
 * 基于文件修改时间的同步器
 */
class TimeBasedFileSyncer {
    private $conn;
    private $sync_log = [];

    public function __construct($host, $username, $password) {
        $this->conn = ftp_connect($host);
        if (!$this->conn) {
            throw new Exception("无法连接到FTP服务器");
        }

        if (!ftp_login($this->conn, $username, $password)) {
            ftp_close($this->conn);
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->conn, true);
    }

    /**
     * 同步单个文件
     */
    public function syncFile($remote_file, $local_file, $direction = 'both') {
        $result = [
            'remote_file' => $remote_file,
            'local_file' => $local_file,
            'direction' => $direction,
            'action' => 'none',
            'reason' => '',
            'timestamp' => time()
        ];

        // 获取远程文件信息
        $remote_exists = false;
        $remote_mtime = -1;
        $remote_size = -1;

        if (ftp_size($this->conn, $remote_file) != -1) {
            $remote_exists = true;
            $remote_mtime = ftp_mdtm($this->conn, $remote_file);
            $remote_size = ftp_size($this->conn, $remote_file);
        }

        // 获取本地文件信息
        $local_exists = file_exists($local_file);
        $local_mtime = $local_exists ? filemtime($local_file) : -1;
        $local_size = $local_exists ? filesize($local_file) : -1;

        // 同步逻辑
        if (!$remote_exists && !$local_exists) {
            $result['action'] = 'skip';
            $result['reason'] = '远程和本地文件都不存在';
        } elseif (!$remote_exists && $local_exists) {
            if ($direction == 'upload' || $direction == 'both') {
                // 上传本地文件到远程
                $this->uploadFile($local_file, $remote_file);
                $result['action'] = 'upload';
                $result['reason'] = '远程文件不存在,上传本地文件';
            } else {
                $result['action'] = 'skip';
                $result['reason'] = '远程文件不存在,但同步方向不允许上传';
            }
        } elseif ($remote_exists && !$local_exists) {
            if ($direction == 'download' || $direction == 'both') {
                // 下载远程文件到本地
                $this->downloadFile($remote_file, $local_file);
                $result['action'] = 'download';
                $result['reason'] = '本地文件不存在,下载远程文件';
            } else {
                $result['action'] = 'skip';
                $result['reason'] = '本地文件不存在,但同步方向不允许下载';
            }
        } else {
            // 两个文件都存在,比较修改时间
            if ($remote_mtime == -1 || $local_mtime == -1) {
                $result['action'] = 'skip';
                $result['reason'] = '无法获取文件修改时间';
            } elseif ($remote_mtime > $local_mtime) {
                if ($direction == 'download' || $direction == 'both') {
                    // 远程文件较新,下载
                    $this->downloadFile($remote_file, $local_file);
                    $result['action'] = 'download';
                    $result['reason'] = '远程文件较新';
                } else {
                    $result['action'] = 'skip';
                    $result['reason'] = '远程文件较新,但同步方向不允许下载';
                }
            } elseif ($local_mtime > $remote_mtime) {
                if ($direction == 'upload' || $direction == 'both') {
                    // 本地文件较新,上传
                    $this->uploadFile($local_file, $remote_file);
                    $result['action'] = 'upload';
                    $result['reason'] = '本地文件较新';
                } else {
                    $result['action'] = 'skip';
                    $result['reason'] = '本地文件较新,但同步方向不允许上传';
                }
            } else {
                $result['action'] = 'skip';
                $result['reason'] = '文件时间相同';
            }
        }

        // 记录同步结果
        $this->sync_log[] = $result;
        return $result;
    }

    /**
     * 同步整个目录
     */
    public function syncDirectory($remote_dir, $local_dir, $direction = 'both', $recursive = false) {
        $results = [];

        // 确保本地目录存在
        if (!is_dir($local_dir)) {
            mkdir($local_dir, 0755, true);
        }

        // 获取远程文件列表
        $remote_items = ftp_nlist($this->conn, $remote_dir);
        if ($remote_items === false) {
            throw new Exception("无法获取远程目录列表: $remote_dir");
        }

        foreach ($remote_items as $item) {
            $item_name = basename($item);
            if ($item_name == '.' || $item_name == '..') {
                continue;
            }

            $remote_path = $remote_dir . '/' . $item_name;
            $local_path = $local_dir . '/' . $item_name;

            // 检查是否为目录
            $is_dir = (ftp_size($this->conn, $remote_path) == -1);

            if ($is_dir && $recursive) {
                // 递归同步子目录
                $sub_results = $this->syncDirectory($remote_path, $local_path, $direction, true);
                $results = array_merge($results, $sub_results);
            } elseif (!$is_dir) {
                // 同步文件
                $result = $this->syncFile($remote_path, $local_path, $direction);
                $results[] = $result;
            }
        }

        return $results;
    }

    /**
     * 下载文件
     */
    private function downloadFile($remote_file, $local_file) {
        $mode = $this->getTransferMode($remote_file);
        return ftp_get($this->conn, $local_file, $remote_file, $mode);
    }

    /**
     * 上传文件
     */
    private function uploadFile($local_file, $remote_file) {
        $mode = $this->getTransferMode($local_file);
        return ftp_put($this->conn, $remote_file, $local_file, $mode);
    }

    /**
     * 获取传输模式
     */
    private function getTransferMode($filename) {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        $text_extensions = ['txt', 'html', 'htm', 'php', 'css', 'js', 'json', 'xml', 'csv'];
        return in_array($extension, $text_extensions) ? FTP_ASCII : FTP_BINARY;
    }

    /**
     * 获取同步日志
     */
    public function getSyncLog() {
        return $this->sync_log;
    }

    /**
     * 生成同步报告
     */
    public function generateSyncReport() {
        $total = count($this->sync_log);
        $actions = [
            'download' => 0,
            'upload' => 0,
            'skip' => 0,
            'error' => 0
        ];

        foreach ($this->sync_log as $log) {
            if (isset($actions[$log['action']])) {
                $actions[$log['action']]++;
            }
        }

        return [
            'total_files' => $total,
            'actions' => $actions,
            'log' => $this->sync_log
        ];
    }

    public function close() {
        if (is_resource($this->conn)) {
            ftp_close($this->conn);
        }
    }
}

// 使用示例
try {
    $syncer = new TimeBasedFileSyncer("ftp.example.com", "username", "password");

    echo "=== 同步单个文件 ===\n";
    $result = $syncer->syncFile(
        "public_html/index.php",
        "./sync/index.php",
        "both"
    );
    echo "操作: {$result['action']}\n";
    echo "原因: {$result['reason']}\n";

    echo "\n=== 同步整个目录 ===\n";
    $results = $syncer->syncDirectory(
        "public_html",
        "./sync/public_html",
        "both",
        true
    );

    $report = $syncer->generateSyncReport();
    echo "同步完成\n";
    echo "总文件数: {$report['total_files']}\n";
    echo "下载: {$report['actions']['download']}\n";
    echo "上传: {$report['actions']['upload']}\n";
    echo "跳过: {$report['actions']['skip']}\n";

    // 显示详细日志
    echo "\n详细日志:\n";
    foreach ($report['log'] as $log) {
        echo "- {$log['remote_file']}: {$log['action']} ({$log['reason']})\n";
    }

    $syncer->close();

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

示例5:监控文件变化的守护进程

<?php
/**
 * FTP文件变化监控器
 */
class FTPFileWatcher {
    private $conn;
    private $config;
    private $running = false;
    private $file_cache = [];

    public function __construct($config) {
        $this->config = array_merge([
            'host' => '',
            'username' => '',
            'password' => '',
            'watch_dir' => '/',
            'check_interval' => 60, // 秒
            'log_file' => null,
            'callback' => null
        ], $config);
    }

    /**
     * 启动监控
     */
    public function start() {
        echo "启动FTP文件监控器...\n";
        echo "监控目录: {$this->config['watch_dir']}\n";
        echo "检查间隔: {$this->config['check_interval']}秒\n\n";

        $this->running = true;
        $this->connect();

        // 首次扫描
        $this->scanFiles();

        // 主循环
        while ($this->running) {
            $this->checkForChanges();
            sleep($this->config['check_interval']);
        }
    }

    /**
     * 停止监控
     */
    public function stop() {
        echo "\n停止监控...\n";
        $this->running = false;
        $this->disconnect();
    }

    /**
     * 连接FTP服务器
     */
    private function connect() {
        $this->conn = ftp_connect($this->config['host']);
        if (!$this->conn) {
            throw new Exception("无法连接到FTP服务器");
        }

        if (!ftp_login($this->conn, $this->config['username'], $this->config['password'])) {
            ftp_close($this->conn);
            throw new Exception("FTP登录失败");
        }

        ftp_pasv($this->conn, true);
    }

    /**
     * 断开连接
     */
    private function disconnect() {
        if (is_resource($this->conn)) {
            ftp_close($this->conn);
        }
    }

    /**
     * 扫描文件并建立缓存
     */
    private function scanFiles() {
        $this->file_cache = $this->getFileList($this->config['watch_dir']);
        echo "初始扫描完成,发现 " . count($this->file_cache) . " 个文件\n";
    }

    /**
     * 获取文件列表
     */
    private function getFileList($directory, &$all_files = []) {
        $files = ftp_nlist($this->conn, $directory);
        if ($files === false) {
            return [];
        }

        foreach ($files as $file) {
            $filename = basename($file);
            if ($filename == '.' || $filename == '..') {
                continue;
            }

            $full_path = $directory . '/' . $filename;

            // 检查是否为目录
            $is_dir = (ftp_size($this->conn, $full_path) == -1);

            if ($is_dir) {
                // 递归扫描子目录
                $this->getFileList($full_path, $all_files);
            } else {
                // 获取文件信息
                $mtime = ftp_mdtm($this->conn, $full_path);
                $size = ftp_size($this->conn, $full_path);

                if ($mtime != -1) {
                    $all_files[$full_path] = [
                        'mtime' => $mtime,
                        'size' => $size,
                        'last_check' => time()
                    ];
                }
            }
        }

        return $all_files;
    }

    /**
     * 检查文件变化
     */
    private function checkForChanges() {
        echo "\n[" . date('Y-m-d H:i:s') . "] 检查文件变化...\n";

        $current_files = $this->getFileList($this->config['watch_dir']);
        $changes = [];

        // 检查文件修改
        foreach ($current_files as $path => $current_info) {
            if (isset($this->file_cache[$path])) {
                $cached_info = $this->file_cache[$path];

                if ($current_info['mtime'] != $cached_info['mtime']) {
                    $changes[] = [
                        'type' => 'modified',
                        'file' => $path,
                        'old_mtime' => $cached_info['mtime'],
                        'new_mtime' => $current_info['mtime'],
                        'old_size' => $cached_info['size'],
                        'new_size' => $current_info['size']
                    ];
                }
            } else {
                // 新文件
                $changes[] = [
                    'type' => 'created',
                    'file' => $path,
                    'mtime' => $current_info['mtime'],
                    'size' => $current_info['size']
                ];
            }
        }

        // 检查文件删除
        foreach ($this->file_cache as $path => $cached_info) {
            if (!isset($current_files[$path])) {
                $changes[] = [
                    'type' => 'deleted',
                    'file' => $path,
                    'mtime' => $cached_info['mtime'],
                    'size' => $cached_info['size']
                ];
            }
        }

        // 处理变化
        if (!empty($changes)) {
            echo "发现 " . count($changes) . " 个变化:\n";

            foreach ($changes as $change) {
                $this->handleChange($change);
            }

            // 更新缓存
            $this->file_cache = $current_files;
        } else {
            echo "没有发现变化\n";
        }
    }

    /**
     * 处理文件变化
     */
    private function handleChange($change) {
        $message = '';

        switch ($change['type']) {
            case 'created':
                $message = "新文件: {$change['file']} (" .
                          formatBytes($change['size']) . ", " .
                          date('Y-m-d H:i:s', $change['mtime']) . ")";
                break;

            case 'modified':
                $time_diff = $change['new_mtime'] - $change['old_mtime'];
                $size_diff = $change['new_size'] - $change['old_size'];
                $message = "文件修改: {$change['file']} (" .
                          "时间: " . date('Y-m-d H:i:s', $change['old_mtime']) . " → " .
                          date('Y-m-d H:i:s', $change['new_mtime']) . ", " .
                          "大小: " . formatBytes($change['old_size']) . " → " .
                          formatBytes($change['new_size']) . ")";
                break;

            case 'deleted':
                $message = "文件删除: {$change['file']}";
                break;
        }

        echo "  {$message}\n";

        // 记录到日志文件
        if ($this->config['log_file']) {
            $log_entry = "[" . date('Y-m-d H:i:s') . "] {$message}\n";
            file_put_contents($this->config['log_file'], $log_entry, FILE_APPEND);
        }

        // 调用回调函数
        if (is_callable($this->config['callback'])) {
            call_user_func($this->config['callback'], $change);
        }
    }
}

// 辅助函数
function formatBytes($bytes) {
    if ($bytes >= 1073741824) {
        return number_format($bytes / 1073741824, 2) . ' GB';
    } elseif ($bytes >= 1048576) {
        return number_format($bytes / 1048576, 2) . ' MB';
    } elseif ($bytes >= 1024) {
        return number_format($bytes / 1024, 2) . ' KB';
    } else {
        return $bytes . ' 字节';
    }
}

// 使用示例
try {
    // 配置监控器
    $watcher = new FTPFileWatcher([
        'host' => 'ftp.example.com',
        'username' => 'username',
        'password' => 'password',
        'watch_dir' => '/public_html',
        'check_interval' => 30, // 每30秒检查一次
        'log_file' => './ftp_changes.log',
        'callback' => function($change) {
            // 自定义处理逻辑
            // 例如:发送邮件通知、触发同步等
        }
    ]);

    // 设置信号处理器(用于优雅退出)
    pcntl_signal(SIGINT, function() use ($watcher) {
        echo "\n接收到中断信号\n";
        $watcher->stop();
        exit(0);
    });

    pcntl_signal(SIGTERM, function() use ($watcher) {
        echo "\n接收到终止信号\n";
        $watcher->stop();
        exit(0);
    });

    // 启动监控(在实际使用中可能需要在后台运行)
    echo "按 Ctrl+C 停止监控\n";
    $watcher->start();

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
    exit(1);
}
?>

常见问题

  1. 文件不存在:指定的远程文件路径不正确
  2. 服务器不支持:FTP服务器不支持MDTM命令
  3. 权限不足:用户没有访问该文件的权限
  4. 连接问题:FTP连接已断开或出现错误
  5. 目录而非文件:指定的是目录路径而不是文件
  6. 超时:服务器响应超时

<?php
function checkMDTMSupport($ftp_server, $ftp_user, $ftp_pass) {
    $conn = ftp_connect($ftp_server);
    if (!$conn) {
        return false;
    }

    if (!ftp_login($conn, $ftp_user, $ftp_pass)) {
        ftp_close($conn);
        return false;
    }

    ftp_pasv($conn, true);

    // 尝试获取已知文件的修改时间
    $test_files = ['index.html', 'index.php', 'readme.txt', '.htaccess'];
    $supported = false;

    foreach ($test_files as $test_file) {
        $mtime = ftp_mdtm($conn, $test_file);
        if ($mtime != -1) {
            $supported = true;
            echo "服务器支持MDTM命令 (测试文件: $test_file)\n";
            break;
        }
    }

    if (!$supported) {
        // 尝试获取当前目录的修改时间(通常目录不支持)
        $current_dir = ftp_pwd($conn);
        $mtime = ftp_mdtm($conn, $current_dir);
        if ($mtime != -1) {
            echo "警告: 服务器可能不支持标准的MDTM命令\n";
        } else {
            echo "服务器不支持MDTM命令\n";
        }
    }

    ftp_close($conn);
    return $supported;
}

// 使用示例
checkMDTMSupport("ftp.example.com", "username", "password");
?>

<?php
function getFileTimeWithTimezone($ftp_conn, $remote_file, $timezone = 'UTC') {
    $timestamp = ftp_mdtm($ftp_conn, $remote_file);

    if ($timestamp == -1) {
        return false;
    }

    // 创建DateTime对象
    $date = new DateTime("@$timestamp");
    $date->setTimezone(new DateTimeZone($timezone));

    return [
        'timestamp' => $timestamp,
        'formatted' => $date->format('Y-m-d H:i:s'),
        'timezone' => $timezone,
        'iso8601' => $date->format('c'),
        'rfc2822' => $date->format('r')
    ];
}

// 使用示例
$conn = ftp_connect("ftp.example.com");
if ($conn && ftp_login($conn, "user", "pass")) {
    $times = getFileTimeWithTimezone($conn, "public_html/index.php", "Asia/Shanghai");

    if ($times) {
        echo "原始时间戳: {$times['timestamp']}\n";
        echo "上海时区: {$times['formatted']}\n";
        echo "时区: {$times['timezone']}\n";
        echo "ISO8601格式: {$times['iso8601']}\n";
        echo "RFC2822格式: {$times['rfc2822']}\n";
    }

    ftp_close($conn);
}
?>

相关函数

函数 描述
filemtime() 获取本地文件的修改时间
ftp_size() 获取远程文件的大小
ftp_nlist() 获取远程目录中的文件列表
ftp_rawlist() 获取远程目录的详细列表
time() 获取当前Unix时间戳
date() 格式化日期/时间
最佳实践:
  • 在使用ftp_mdtm()前,先用ftp_size()检查文件是否存在
  • 处理返回的-1值,提供有意义的错误信息
  • 考虑服务器时区,必要时进行时区转换
  • 对于关键应用,实现回退机制(如使用ftp_rawlist()解析时间)
  • 缓存文件时间信息以减少服务器请求
  • 记录无法获取时间的文件,用于后续分析