| @@ -6,8 +6,9 @@ | ||
| 6 | 6 | class Logger { |
| 7 | 7 | private static $instance = null; |
| 8 | 8 | private $log_dir; |
| 9 | 9 | private $log_file; |
| 10 | + private $handle = null; | |
| 10 | 11 | |
| 11 | 12 | private function __construct() { |
| 12 | 13 | $upload_dir = wp_upload_dir(); |
| 13 | 14 | $this->log_dir = trailingslashit($upload_dir['basedir']) . Schema::getConstant('UPLOADS'); |
| @@ -18,8 +19,14 @@ | ||
| 18 | 19 | |
| 19 | 20 | $this->log_file = $this->log_dir . '/sync-log.json'; |
| 20 | 21 | } |
| 21 | 22 | |
| 23 | + public function __destruct() { | |
| 24 | + if (is_resource($this->handle)) { | |
| 25 | + fclose($this->handle); | |
| 26 | + } | |
| 27 | + } | |
| 28 | + | |
| 22 | 29 | /** |
| 23 | 30 | * Singleton instance |
| 24 | 31 | */ |
| 25 | 32 | public static function instance() { |
| @@ -29,8 +36,71 @@ | ||
| 29 | 36 | return self::$instance; |
| 30 | 37 | } |
| 31 | 38 | |
| 32 | 39 | /** |
| 40 | + * Reuse a single open file handle for the process instead of | |
| 41 | + * reopening/closing on every call (heavy when many logs happen at once) | |
| 42 | + */ | |
| 43 | + private function get_handle($create = true) { | |
| 44 | + if (is_resource($this->handle)) { | |
| 45 | + return $this->handle; | |
| 46 | + } | |
| 47 | + | |
| 48 | + if (!$create && !file_exists($this->log_file)) { | |
| 49 | + return null; | |
| 50 | + } | |
| 51 | + | |
| 52 | + $this->handle = fopen($this->log_file, 'c+'); | |
| 53 | + return $this->handle ?: null; | |
| 54 | + } | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * Read logs under a shared lock | |
| 58 | + */ | |
| 59 | + private function read_logs() { | |
| 60 | + $handle = $this->get_handle(false); | |
| 61 | + if (!$handle) return []; | |
| 62 | + | |
| 63 | + $logs = []; | |
| 64 | + try { | |
| 65 | + if (flock($handle, LOCK_SH)) { | |
| 66 | + rewind($handle); | |
| 67 | + $content = stream_get_contents($handle); | |
| 68 | + $logs = json_decode($content, true) ?: []; | |
| 69 | + } | |
| 70 | + } finally { | |
| 71 | + flock($handle, LOCK_UN); | |
| 72 | + } | |
| 73 | + | |
| 74 | + return $logs; | |
| 75 | + } | |
| 76 | + | |
| 77 | + /** | |
| 78 | + * Read-modify-write logs under an exclusive lock held for the whole cycle | |
| 79 | + */ | |
| 80 | + private function update_logs(callable $mutator) { | |
| 81 | + $handle = $this->get_handle(true); | |
| 82 | + if (!$handle) return; | |
| 83 | + | |
| 84 | + try { | |
| 85 | + if (flock($handle, LOCK_EX)) { | |
| 86 | + rewind($handle); | |
| 87 | + $content = stream_get_contents($handle); | |
| 88 | + $logs = json_decode($content, true) ?: []; | |
| 89 | + | |
| 90 | + $logs = $mutator($logs); | |
| 91 | + | |
| 92 | + rewind($handle); | |
| 93 | + ftruncate($handle, 0); | |
| 94 | + fwrite($handle, wp_json_encode($logs)); | |
| 95 | + fflush($handle); | |
| 96 | + } | |
| 97 | + } finally { | |
| 98 | + flock($handle, LOCK_UN); | |
| 99 | + } | |
| 100 | + } | |
| 101 | + | |
| 102 | + /** | |
| 33 | 103 | * Add or update a log entry |
| 34 | 104 | */ |
| 35 | 105 | public function add_log($type, $media_id, $source_type, $log = []) { |
| 36 | 106 | $entry = [ |
| @@ -46,30 +116,25 @@ | ||
| 46 | 116 | /** |
| 47 | 117 | * Append log using file-based indexing |
| 48 | 118 | */ |
| 49 | 119 | private function append_log($entry, $log=[]) { |
| 50 | - $logs = []; | |
| 120 | + $this->update_logs(function($logs) use ($entry, $log) { | |
| 121 | + // Unique key per media/type/source | |
| 122 | + $key = $entry['media_id'] . '|' . $entry['type'] . '|' . $entry['source_type']; | |
| 51 | 123 | |
| 52 | - if (file_exists($this->log_file)) { | |
| 53 | - $content = file_get_contents($this->log_file); | |
| 54 | - $logs = json_decode($content, true) ?: []; | |
| 55 | - } | |
| 56 | - | |
| 57 | - // Unique key per media/type/source | |
| 58 | - $key = $entry['media_id'] . '|' . $entry['type'] . '|' . $entry['source_type']; | |
| 59 | - | |
| 60 | - if (isset($logs[$key])) { | |
| 61 | - if (!isset($logs[$key]['logs']) || !is_array($logs[$key]['logs'])) { | |
| 62 | - $logs[$key]['logs'] = []; | |
| 124 | + if (isset($logs[$key])) { | |
| 125 | + if (!isset($logs[$key]['logs']) || !is_array($logs[$key]['logs'])) { | |
| 126 | + $logs[$key]['logs'] = []; | |
| 127 | + } | |
| 128 | + $logs[$key]['logs'][] = $log; | |
| 129 | + $logs[$key]['time'] = $entry['time'] ?? current_time('mysql'); | |
| 130 | + } else { | |
| 131 | + $logs[$key] = $entry; | |
| 132 | + $logs[$key]['logs'][] = $log; | |
| 63 | 133 | } |
| 64 | - $logs[$key]['logs'][] = $log; | |
| 65 | - $logs[$key]['time'] = $entry['time'] ?? current_time('mysql'); | |
| 66 | - } else { | |
| 67 | - $logs[$key] = $entry; | |
| 68 | - $logs[$key]['logs'][] = $log; | |
| 69 | - } | |
| 70 | 134 | |
| 71 | - file_put_contents($this->log_file, wp_json_encode($logs)); | |
| 135 | + return $logs; | |
| 136 | + }); | |
| 72 | 137 | } |
| 73 | 138 | |
| 74 | 139 | /** |
| 75 | 140 | * Get log by media ID, type, and source type |
| @@ -74,12 +139,10 @@ | ||
| 74 | 139 | /** |
| 75 | 140 | * Get log by media ID, type, and source type |
| 76 | 141 | */ |
| 77 | 142 | public function get_log($type, $media_id, $source_type) { |
| 78 | - if (!file_exists($this->log_file)) return []; | |
| 79 | - | |
| 80 | 143 | $key = $media_id . '|' . $type . '|' . $source_type; |
| 81 | - return json_decode(file_get_contents($this->log_file), true)[$key] ?? []; | |
| 144 | + return $this->read_logs()[$key] ?? []; | |
| 82 | 145 | } |
| 83 | 146 | |
| 84 | 147 | /** |
| 85 | 148 | * Get all logs (numeric array, supports pagination) |
| @@ -94,9 +157,9 @@ | ||
| 94 | 157 | 'per_page' => $per_page ?:1, |
| 95 | 158 | ]; |
| 96 | 159 | } |
| 97 | 160 | |
| 98 | - $logs = array_values(json_decode(file_get_contents($this->log_file), true) ?: []); | |
| 161 | + $logs = array_values($this->read_logs()); | |
| 99 | 162 | |
| 100 | 163 | $total = count($logs); |
| 101 | 164 | |
| 102 | 165 | if ($page === null || $per_page === null) { |
| @@ -135,9 +198,9 @@ | ||
| 135 | 198 | 'per_page' => $per_page ?:1, |
| 136 | 199 | ]; |
| 137 | 200 | }; |
| 138 | 201 | |
| 139 | - $all_logs = json_decode(file_get_contents($this->log_file) ?: '[]', true) ?: []; | |
| 202 | + $all_logs = $this->read_logs(); | |
| 140 | 203 | |
| 141 | 204 | $logs = array_values(array_filter($all_logs, function($log) use ($type) { |
| 142 | 205 | return isset($log['type']) && $log['type'] === $type; |
| 143 | 206 | })); |
| @@ -174,17 +237,17 @@ | ||
| 174 | 237 | */ |
| 175 | 238 | public function remove_logs_by_type($type) { |
| 176 | 239 | if (!file_exists($this->log_file)) return; |
| 177 | 240 | |
| 178 | - $logs = json_decode(file_get_contents($this->log_file), true) ?: []; | |
| 179 | - | |
| 180 | - foreach ($logs as $key => $log) { | |
| 181 | - if (isset($log['type']) && $log['type'] === $type) { | |
| 182 | - unset($logs[$key]); | |
| 241 | + $this->update_logs(function($logs) use ($type) { | |
| 242 | + foreach ($logs as $key => $log) { | |
| 243 | + if (isset($log['type']) && $log['type'] === $type) { | |
| 244 | + unset($logs[$key]); | |
| 245 | + } | |
| 183 | 246 | } |
| 184 | - } | |
| 185 | 247 | |
| 186 | - file_put_contents($this->log_file, wp_json_encode($logs)); | |
| 248 | + return $logs; | |
| 249 | + }); | |
| 187 | 250 | } |
| 188 | 251 | |
| 189 | 252 | /** |
| 190 | 253 | * Remove logs by media ID, type, and source type |
| @@ -191,17 +254,17 @@ | ||
| 191 | 254 | */ |
| 192 | 255 | public function remove_log($type, $media_id, $source_type) { |
| 193 | 256 | if (!file_exists($this->log_file)) return; |
| 194 | 257 | |
| 195 | - $logs = json_decode(file_get_contents($this->log_file), true) ?: []; | |
| 258 | + $this->update_logs(function($logs) use ($type, $media_id, $source_type) { | |
| 259 | + $key = $media_id . '|' . $type . '|' . $source_type; | |
| 196 | 260 | |
| 197 | - $key = $media_id . '|' . $type . '|' . $source_type; | |
| 261 | + if (isset($logs[$key])) { | |
| 262 | + unset($logs[$key]); | |
| 263 | + } | |
| 198 | 264 | |
| 199 | - if (isset($logs[$key])) { | |
| 200 | - unset($logs[$key]); | |
| 201 | - } | |
| 202 | - | |
| 203 | - file_put_contents($this->log_file, wp_json_encode($logs)); | |
| 265 | + return $logs; | |
| 266 | + }); | |
| 204 | 267 | } |
| 205 | 268 | |
| 206 | 269 | /** |
| 207 | 270 | * Remove Log by media ID & source type |
| @@ -208,15 +271,13 @@ | ||
| 208 | 271 | */ |
| 209 | 272 | public function remove_log_by_media_id($media_id, $source_type) { |
| 210 | 273 | if (!file_exists($this->log_file)) return; |
| 211 | 274 | |
| 212 | - $logs = json_decode(file_get_contents($this->log_file), true) ?: []; | |
| 213 | - | |
| 214 | - $new_log = array_filter($logs, function($log) use ($media_id, $source_type) { | |
| 215 | - return $log['media_id'] !== $media_id || $log['source_type'] !== $source_type; | |
| 275 | + $this->update_logs(function($logs) use ($media_id, $source_type) { | |
| 276 | + return array_filter($logs, function($log) use ($media_id, $source_type) { | |
| 277 | + return $log['media_id'] !== $media_id || $log['source_type'] !== $source_type; | |
| 278 | + }); | |
| 216 | 279 | }); |
| 217 | - | |
| 218 | - file_put_contents($this->log_file, wp_json_encode($new_log)); | |
| 219 | 280 | } |
| 220 | 281 | |
| 221 | 282 | /** |
| 222 | 283 | * Clear all logs |
| @@ -221,9 +282,14 @@ | ||
| 221 | 282 | /** |
| 222 | 283 | * Clear all logs |
| 223 | 284 | */ |
| 224 | 285 | public function clear_logs() { |
| 286 | + if (is_resource($this->handle)) { | |
| 287 | + fclose($this->handle); | |
| 288 | + $this->handle = null; | |
| 289 | + } | |
| 290 | + | |
| 225 | 291 | if (file_exists($this->log_file)) { |
| 226 | 292 | unlink($this->log_file); |
| 227 | 293 | } |
| 228 | 294 | } |
| 229 | 295 | } |