媒体同步功能说明
这个增强版插件新增了以下媒体文件同步功能:
-
自动同步上传的媒体文件:
-
当您在任何站点上传图片或其他媒体文件时,会自动同步到另一个站点
-
包括文件本身和附件的元数据
-
-
文件大小限制:
-
默认最大同步文件大小为10MB(可在配置中修改)
-
超过限制的文件会被跳过并记录错误
-
-
文件类型检查:
-
只同步WordPress允许的文件类型
-
防止上传潜在的危险文件类型
-
-
完整的媒体处理:
-
自动生成缩略图(如果图片)
-
保留原始文件名和元数据
-
正确处理媒体库中的显示
-
安装与配置指南
-
安装插件:
-
将代码保存为
wp-bidirectional-sync.php
-
上传到
/wp-content/plugins/
目录 -
在WordPress后台激活插件
-
-
配置修改:
private $remote_sites = [ 'https://您的公网站点.com/wp-admin/admin-ajax.php', 'https://您的NAS站点.local/wp-admin/admin-ajax.php' ]; private $api_keys = [ 'site1' => '公网站点的API密钥', 'site2' => 'NAS站点的API密钥' ]; // 确保包含 'attachment' private $sync_post_types = ['post', 'page', 'attachment']; // 调整最大文件大小 (字节) private $max_file_size = 20971520; // 20MB
-
服务器要求:
-
PHP必须启用
fileinfo
扩展(用于文件类型检测) -
确保
wp-content/uploads
目录可写 -
增加PHP内存限制(建议至少128MB)
-
-
测试同步:
-
在两个站点都安装并配置好插件
-
在一个站点上传图片,检查另一个站点是否自动出现
-
检查后台同步状态页面是否有错误
-
注意事项
-
性能考虑:
-
大文件同步会消耗较多带宽和服务器资源
-
建议在低峰期进行大量媒体同步
-
-
存储空间:
-
确保两个站点都有足够的存储空间
-
同步的媒体文件会占用双倍空间
-
-
首次同步:
-
对于已有的大量媒体文件,建议先手动批量同步
-
可以使用All-in-One WP Migration等插件先同步基础内容
-
-
替代方案:
-
如果媒体文件特别多,可以考虑使用共享存储(如NFS)
-
或者使用云存储(如AWS S3)作为两个站点的共同媒体库
-
这个插件应该能满足您同步文章内容和媒体文件的需求。如果遇到任何问题,可以检查WordPress的debug.log文件获取详细错误信息。
代码如下:
'公网站点的API密钥', // 与第一个站点URL对应
'site2' => 'NAS站点的API密钥' // 与第二个站点URL对应
];
// 同步内容类型 (post, page, attachment 等)
private $sync_post_types = ['post', 'page', 'attachment'];
// 同步间隔 (seconds)
private $sync_interval = 300; // 5分钟 = 300秒
// 最大文件大小 (字节),默认20MB
private $max_file_size = 20971520;
/******************************
* 以下代码无需修改
******************************/
private $current_site_id;
public function __construct() {
$this->current_site_id = $this->identify_current_site();
// 初始化钩子
$this->init_hooks();
// 激活/停用插件时的操作
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
}
private function identify_current_site() {
$site_url = site_url();
foreach ($this->remote_sites as $key => $url) {
if (strpos($url, $site_url) !== false) {
return 'site' . ($key + 1);
}
}
return 'site1'; // 默认
}
private function init_hooks() {
// 内容变更钩子
add_action('save_post', [$this, 'handle_content_change'], 99, 3);
add_action('delete_post', [$this, 'handle_content_delete'], 99, 2);
// 媒体文件上传处理
add_action('add_attachment', [$this, 'handle_media_upload']);
// 定时同步检查
add_action('init', [$this, 'setup_schedule']);
add_action('wpsync_bidirectional_check', [$this, 'check_for_changes']);
// 接收端处理
add_action('wp_ajax_wpsync_receive_data', [$this, 'receive_data']);
add_action('wp_ajax_nopriv_wpsync_receive_data', [$this, 'receive_data']);
// 添加管理菜单
add_action('admin_menu', [$this, 'add_admin_menu']);
}
public function add_admin_menu() {
add_options_page(
'双向同步设置',
'站点同步',
'manage_options',
'wp-bidirectional-sync',
[$this, 'settings_page']
);
}
public function settings_page() {
?>
WordPress 双向同步设置
当前配置
当前站点ID: current_site_id); ?>
远程站点:
-
remote_sites as $site): ?>
同步内容类型: sync_post_types); ?>
同步间隔: sync_interval; ?> 秒
最大文件大小: max_file_size); ?>
同步状态
最后同步检查:
最后同步错误:
check_for_changes(true);
echo '
手动同步已触发!
';
}
}
public function activate() {
$this->setup_schedule();
// 创建必要的目录
wp_mkdir_p($this->get_sync_temp_dir());
}
public function deactivate() {
wp_clear_scheduled_hook('wpsync_bidirectional_check');
// 清理临时文件
$this->clean_temp_files();
}
private function get_sync_temp_dir() {
$upload_dir = wp_upload_dir();
return trailingslashit($upload_dir['basedir']) . 'wpsync_temp/';
}
private function clean_temp_files() {
$temp_dir = $this->get_sync_temp_dir();
if (is_dir($temp_dir)) {
array_map('unlink', glob($temp_dir . '*'));
@rmdir($temp_dir);
}
}
public function setup_schedule() {
if (!wp_next_scheduled('wpsync_bidirectional_check')) {
wp_schedule_event(time(), 'wpsync_interval', 'wpsync_bidirectional_check');
}
// 添加自定义时间间隔
add_filter('cron_schedules', function($schedules) {
$schedules['wpsync_interval'] = [
'interval' => $this->sync_interval,
'display' => sprintf('每 %d 秒', $this->sync_interval)
];
return $schedules;
});
}
public function handle_content_change($post_id, $post, $update) {
if (!in_array($post->post_type, $this->sync_post_types)) return;
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return;
$change_data = [
'action' => 'content_change',
'post_id' => $post_id,
'post_data' => $post,
'post_meta' => get_post_meta($post_id),
'change_type' => $update ? 'update' : 'create',
'timestamp' => time(),
'source_site' => $this->current_site_id,
'sync_token' => $this->generate_sync_token()
];
$this->broadcast_change($change_data);
}
public function handle_media_upload($attachment_id) {
$post = get_post($attachment_id);
$this->handle_content_change($attachment_id, $post, false);
// 单独处理文件传输
$file_path = get_attached_file($attachment_id);
if (file_exists($file_path)) {
$this->broadcast_file($attachment_id, $file_path);
}
}
private function broadcast_file($attachment_id, $file_path) {
$file_size = filesize($file_path);
if ($file_size > $this->max_file_size) {
error_log('文件太大,跳过同步: ' . $file_path);
return;
}
$file_data = [
'action' => 'file_upload',
'attachment_id' => $attachment_id,
'file_name' => basename($file_path),
'file_type' => get_post_mime_type($attachment_id),
'source_site' => $this->current_site_id,
'sync_token' => $this->generate_sync_token()
];
foreach ($this->remote_sites as $site) {
if (strpos($site, site_url()) === false) { // 只发送给远程站点
$boundary = wp_generate_password(24);
$payload = $this->prepare_file_payload($file_path, $file_data, $boundary);
$response = wp_remote_post($site, [
'body' => $payload,
'timeout' => 60, // 文件传输需要更长时间
'headers' => [
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
'X-WP-Sync-Signature' => $this->generate_signature($file_data)
]
]);
if (is_wp_error($response)) {
error_log('文件同步错误: ' . $response->get_error_message());
update_option('wpsync_last_error', current_time('mysql') . ': 文件同步失败 - ' . $response->get_error_message());
}
}
}
}
private function prepare_file_payload($file_path, $data, $boundary) {
$payload = '';
// 添加元数据
foreach ($data as $name => $value) {
$payload .= "--$boundary\r\n";
$payload .= "Content-Disposition: form-data; name=\"$name\"\r\n\r\n";
$payload .= "$value\r\n";
}
// 添加文件内容
$payload .= "--$boundary\r\n";
$payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename($file_path) . "\"\r\n";
$payload .= "Content-Type: " . mime_content_type($file_path) . "\r\n\r\n";
$payload .= file_get_contents($file_path) . "\r\n";
$payload .= "--$boundary--\r\n";
return $payload;
}
public function handle_content_delete($post_id, $post) {
if (!in_array($post->post_type, $this->sync_post_types)) return;
$change_data = [
'action' => 'content_change',
'post_id' => $post_id,
'post_type' => $post->post_type,
'change_type' => 'delete',
'timestamp' => time(),
'source_site' => $this->current_site_id,
'sync_token' => $this->generate_sync_token()
];
$this->broadcast_change($change_data);
}
private function broadcast_change($data) {
foreach ($this->remote_sites as $site) {
if (strpos($site, site_url()) === false) { // 只发送给远程站点
$response = wp_remote_post($site, [
'body' => $data,
'timeout' => 30,
'headers' => [
'X-WP-Sync-Signature' => $this->generate_signature($data)
]
]);
if (is_wp_error($response)) {
error_log('同步错误: ' . $response->get_error_message());
update_option('wpsync_last_error', current_time('mysql') . ': ' . $response->get_error_message());
}
}
}
}
public function check_for_changes($force = false) {
$last_check = get_option('wpsync_last_check', 0);
if (!$force && (time() - $last_check < $this->sync_interval)) {
return;
}
global $wpdb;
$changed_posts = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_modified_gmt > %s
AND post_type IN ('" . implode("','", $this->sync_post_types) . "')
AND post_status IN ('publish', 'inherit')",
date('Y-m-d H:i:s', $last_check)
));
foreach ($changed_posts as $post) {
if ($post->post_type === 'attachment') {
$file_path = get_attached_file($post->ID);
if (file_exists($file_path)) {
$this->broadcast_file($post->ID, $file_path);
}
} else {
$this->handle_content_change($post->ID, $post, true);
}
}
update_option('wpsync_last_check', time());
}
public function receive_data() {
try {
// 验证签名
if (!$this->validate_request()) {
throw new Exception('无效的请求签名');
}
$data = $_POST;
// 防止同步循环
if ($data['source_site'] === $this->current_site_id) {
throw new Exception('同步循环检测');
}
// 检查是否已处理
if ($this->is_change_processed($data['sync_token'])) {
wp_send_json_success(['message' => '变更已处理']);
}
// 处理文件上传
if (!empty($_FILES['file'])) {
$this->process_file_upload($data);
$this->mark_change_processed($data['sync_token']);
wp_send_json_success(['message' => '文件同步成功']);
}
// 处理内容变更
switch ($data['action']) {
case 'content_change':
$this->process_content_change($data);
break;
default:
throw new Exception('未知的操作类型');
}
// 标记为已处理
$this->mark_change_processed($data['sync_token']);
wp_send_json_success(['message' => '同步成功']);
} catch (Exception $e) {
wp_send_json_error(['message' => $e->getMessage()], 400);
}
}
private function process_file_upload($data) {
if (empty($_FILES['file'])) {
throw new Exception('没有接收到文件');
}
$file = $_FILES['file'];
// 检查文件大小
if ($file['size'] > $this->max_file_size) {
throw new Exception('文件大小超过限制');
}
// 检查文件类型
$allowed_types = get_allowed_mime_types();
if (!in_array($file['type'], $allowed_types)) {
throw new Exception('不允许的文件类型: ' . $file['type']);
}
// 准备上传数据
$upload = [
'name' => $file['name'],
'type' => $file['type'],
'tmp_name' => $file['tmp_name'],
'error' => $file['error'],
'size' => $file['size']
];
// 处理上传
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');
$overrides = ['test_form' => false];
$uploaded_file = wp_handle_upload($upload, $overrides);
if (isset($uploaded_file['error'])) {
throw new Exception('文件上传失败: ' . $uploaded_file['error']);
}
// 创建附件
$attachment = [
'post_mime_type' => $uploaded_file['type'],
'post_title' => preg_replace('/\.[^.]+$/', '', basename($uploaded_file['file'])),
'post_content' => '',
'post_status' => 'inherit',
'guid' => $uploaded_file['url']
];
$attachment_id = wp_insert_attachment($attachment, $uploaded_file['file']);
if (is_wp_error($attachment_id)) {
throw new Exception('创建附件失败: ' . $attachment_id->get_error_message());
}
// 生成元数据
$attachment_data = wp_generate_attachment_metadata($attachment_id, $uploaded_file['file']);
wp_update_attachment_metadata($attachment_id, $attachment_data);
// 记录来源
update_post_meta($attachment_id, '_wpsync_source_id', $data['source_site'] . '|' . $data['attachment_id']);
return $attachment_id;
}
private function process_content_change($data) {
global $wpdb;
switch ($data['change_type']) {
case 'create':
case 'update':
// 检查是否已存在(通过源站点ID+源文章ID)
$existing_id = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key = '_wpsync_source_id'
AND meta_value = %s",
$data['source_site'] . '|' . $data['post_id'])
);
if ($existing_id) {
$data['post_data']['ID'] = $existing_id;
}
// 插入或更新文章
$new_id = wp_insert_post($data['post_data']);
if (is_wp_error($new_id)) {
throw new Exception('文章保存失败: ' . $new_id->get_error_message());
}
// 保存元数据
if (!empty($data['post_meta'])) {
foreach ($data['post_meta'] as $key => $values) {
if ($key === '_wpsync_source_id') continue;
foreach ($values as $value) {
update_post_meta($new_id, $key, maybe_unserialize($value));
}
}
}
// 记录来源
update_post_meta($new_id, '_wpsync_source_id', $data['source_site'] . '|' . $data['post_id']);
break;
case 'delete':
$post_id = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key = '_wpsync_source_id'
AND meta_value = %s",
$data['source_site'] . '|' . $data['post_id'])
);
if ($post_id) {
wp_delete_post($post_id, true);
}
break;
default:
throw new Exception('未知的变更类型');
}
}
private function generate_sync_token() {
return md5(uniqid($this->current_site_id, true) . microtime());
}
private function generate_signature($data) {
return hash_hmac('sha256', json_encode($data), $this->api_keys[$this->current_site_id]);
}
private function validate_request() {
$signature = $_SERVER['HTTP_X_WP_SYNC_SIGNATURE'] ?? '';
$expected = $this->generate_signature($_POST);
return hash_equals($expected, $signature);
}
private function is_change_processed($token) {
global $wpdb;
return (bool) $wpdb->get_var($wpdb->prepare(
"SELECT option_id FROM {$wpdb->options}
WHERE option_name = %s",
'_wpsync_processed_' . $token)
);
}
private function mark_change_processed($token) {
add_option('_wpsync_processed_' . $token, time(), '', 'no');
}
}
new WP_Bidirectional_Sync();