使用php的inotify扩展来监听文件变更
偶尔下载一些视频媒体的时候会想要对它进行压缩来缩小占用空间,
但是如果每次等下载完再去用软件压缩会比较麻烦
这里就找到了inotify扩展,可以监听文件变更
这里其实想吹一下小米的mimo2.5模型,因为要装php扩展,
我的是php7.4,网上搜了下有基于pecl,有的是基于源码
我就直接丢给配了小米模型的hermes agent来安装,直接就给我装好了
顺带再帮我写了个artisan命令1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
namespace App\Console\Commands;
use Illuminate\Console\Command;
class InotifyWatchCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'file:watch
{path : Directory to watch}
{--recursive : Watch subdirectories recursively}
{--event=* : Events to watch (create, modify, delete, move). Default: create,modify}
{--ext=* : Only watch files with these extensions (e.g., --ext=mp4 --ext=mkv)}
{--output= : Output file path for logging (optional)}
{--format=text : Output format: text or json}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Watch directory for file write events using inotify';
/**
* Event constants mapping.
*/
protected $eventMap = [
'create' => IN_CREATE,
'modify' => IN_MODIFY,
'delete' => IN_DELETE,
'move' => IN_MOVED_TO,
'close_write' => IN_CLOSE_WRITE,
'open' => IN_OPEN,
'access' => IN_ACCESS,
'attrib' => IN_ATTRIB,
];
/**
* @var resource
*/
protected $inotify;
/**
* @var array
*/
protected $watchDescriptors = [];
/**
* @var resource|null
*/
protected $outputHandle;
/**
* @var bool
*/
protected $running = true;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$path = trim($this->argument('path'));
$recursive = $this->option('recursive');
$events = $this->option('event') ?: ['create', 'modify'];
$extensions = $this->option('ext');
$outputFile = $this->option('output');
$format = $this->option('format');
// Validate path
if (!is_dir($path)) {
$this->error("Directory does not exist: {$path}");
return 1;
}
// Setup output file if specified
if ($outputFile) {
$this->outputHandle = fopen($outputFile, 'a');
if (!$this->outputHandle) {
$this->error("Cannot open output file: {$outputFile}");
return 1;
}
}
// Calculate event mask
$eventMask = 0;
foreach ($events as $event) {
if (!isset($this->eventMap[$event])) {
$this->error("Unknown event: {$event}. Available: " . implode(', ', array_keys($this->eventMap)));
return 1;
}
$eventMask |= $this->eventMap[$event];
}
// Always add IN_CLOSE_WRITE for reliable write detection
$eventMask |= IN_CLOSE_WRITE;
// Initialize inotify
$this->inotify = inotify_init();
if (!$this->inotify) {
$this->error("Failed to initialize inotify");
return 1;
}
// Set non-blocking mode for graceful shutdown
stream_set_blocking($this->inotify, false);
// Setup signal handling for graceful shutdown
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
pcntl_signal(SIGINT, [$this, 'handleSignal']);
}
// Add watches
$this->addWatch($path, $recursive);
$this->info("Watching: {$path}");
$this->info("Events: " . implode(', ', $events));
$this->info("Recursive: " . ($recursive ? 'yes' : 'no'));
if ($extensions) {
$this->info("Extensions: " . implode(', ', $extensions));
}
$this->info("Press Ctrl+C to stop");
$this->line('');
// Main loop
$checkInterval = 500000; // 500ms in microseconds
while ($this->running) {
// Check for signals
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
$events = inotify_read($this->inotify);
if ($events !== false && !empty($events)) {
foreach ($events as $event) {
$this->handleEvent($event, $extensions, $format);
}
}
usleep($checkInterval);
}
// Cleanup
$this->cleanup();
return 0;
}
/**
* Add inotify watch for a directory.
*/
protected function addWatch(string $path, bool $recursive): void
{
$mask = IN_CREATE | IN_MODIFY | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE | IN_OPEN | IN_ATTRIB;
$wd = inotify_add_watch($this->inotify, $path, $mask);
$this->watchDescriptors[$wd] = $path;
if ($recursive) {
$items = scandir($path);
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$this->addWatch($fullPath, true);
}
}
}
}
/**
* Handle inotify event.
*/
protected function handleEvent(array $event, array $extensions, string $format): void
{
$wd = $event['wd'];
$mask = $event['mask'];
$name = $event['name'] ?? '';
$cookie = $event['cookie'] ?? 0;
// Get directory path from watch descriptor
$dir = $this->watchDescriptors[$wd] ?? 'unknown';
// Skip if no filename
if (empty($name)) {
return;
}
// Filter by extension if specified
if (!empty($extensions)) {
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (!in_array($ext, $extensions)) {
return;
}
}
// Determine event type
$eventType = $this->getEventType($mask);
// Build file path
$filePath = $dir . DIRECTORY_SEPARATOR . $name;
// Build event data
$data = [
'timestamp' => date('Y-m-d H:i:s'),
'event' => $eventType,
'file' => $name,
'path' => $filePath,
'dir' => $dir,
'cookie' => $cookie,
];
// Output based on format
if ($format === 'json') {
$output = json_encode($data, JSON_UNESCAPED_UNICODE) . "\n";
} else {
$output = $this->formatTextOutput($data);
}
// Write to console
$this->writeToConsole($eventType, $output);
// Write to file if handle exists
if ($this->outputHandle) {
fwrite($this->outputHandle, $output);
}
}
/**
* Get human-readable event type from mask.
*/
protected function getEventType(int $mask): string
{
$types = [];
if ($mask & IN_CREATE) {
$types[] = 'CREATE';
}
if ($mask & IN_MODIFY) {
$types[] = 'MODIFY';
}
if ($mask & IN_DELETE) {
$types[] = 'DELETE';
}
if ($mask & IN_MOVED_TO) {
$types[] = 'MOVED_TO';
}
if ($mask & IN_CLOSE_WRITE) {
$types[] = 'CLOSE_WRITE';
}
if ($mask & IN_OPEN) {
$types[] = 'OPEN';
}
if ($mask & IN_ACCESS) {
$types[] = 'ACCESS';
}
if ($mask & IN_ATTRIB) {
$types[] = 'ATTRIB';
}
return implode('|', $types);
}
/**
* Format text output.
*/
protected function formatTextOutput(array $data): string
{
return sprintf(
"[%s] %-12s %s (%s)\n",
$data['timestamp'],
$data['event'],
$data['file'],
$data['dir']
);
}
/**
* Write output to console with color coding.
*/
protected function writeToConsole(string $eventType, string $output): void
{
if (strpos($eventType, 'CREATE') !== false) {
$this->info(rtrim($output));
} elseif (strpos($eventType, 'DELETE') !== false) {
$this->error(rtrim($output));
} elseif (strpos($eventType, 'MODIFY') !== false || strpos($eventType, 'CLOSE_WRITE') !== false) {
$this->comment(rtrim($output));
} elseif (strpos($eventType, 'MOVED_TO') !== false) {
$this->warn(rtrim($output));
} else {
$this->line(rtrim($output));
}
}
/**
* Handle signal for graceful shutdown.
*/
public function handleSignal(int $signal): void
{
$this->running = false;
$this->line('');
$this->info("Received signal {$signal}, shutting down...");
}
/**
* Cleanup resources.
*/
protected function cleanup(): void
{
if ($this->outputHandle) {
fclose($this->outputHandle);
}
// Remove all watches
foreach ($this->watchDescriptors as $wd => $path) {
inotify_rm_watch($this->inotify, $wd);
}
if (is_resource($this->inotify)) {
fclose($this->inotify);
}
$this->info("Watch stopped.");
}
}
只需要执行命令php artisan file:watch /home/nick/listen --recursive
然后我新建一个文本文件1
touch 1.txt
就能看到文件的变更
这个流程下来我都没有任何调整,都是一步到位了
这个因为没有准确的条件对比,可能是hermes也起了一定作用,还是说本身模型就很强
主要是监听了文件的创建,修改,关闭写入等,
首先是inotify_init函数 — 初始化 inotify 实例,类似于客户端
然后是inotify_add_watch 函数 — 添加监听到已初始化的 inotify 实例1
2
3
4
5
6
7
8inotify_add_watch(resource $inotify_instance, string $pathname, int $mask): int|false
参数 ¶
inotify_instance
inotify_init()返回的资源
pathname
要监听的文件或目录
mask
监听事件。详情见 预定义常量。
具体的预定义常量有1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66IN_ACCESS (int)
文件被访问(读)(*)
IN_MODIFY (int)
文件被修改(*)
IN_ATTRIB (int)
元数据变更(例如:权限、修改时间等)(*)
IN_CLOSE_WRITE (int)
打开写入后关闭的文件(*)
IN_CLOSE_NOWRITE (int)
未打开写入的文件被关闭(*)
IN_OPEN (int)
文件被打开(*)
IN_MOVED_TO (int)
文件被移动到监听目录(*)
IN_MOVED_FROM (int)
文件移出监听目录(*)
IN_CREATE (int)
在监听目录中创建文件或文件夹(*)
IN_DELETE (int)
监听目录中删除文件或文件夹(*)
IN_DELETE_SELF (int)
监听的文件或目录被删除
IN_MOVE_SELF (int)
IN_CLOSE (int)
与 IN_CLOSE_WRITE | IN_CLOSE_NOWRITE 相等
IN_MOVE (int)
与 IN_MOVED_FROM | IN_MOVED_TO 相等
IN_ALL_EVENTS (int)
以上所有常量的位掩码
IN_UNMOUNT (int)
卸载包含监听对象的文件系统
IN_Q_OVERFLOW (int)
事件队列溢出(此事件中监听描述符是 -1)
IN_IGNORED (int)
监听被移除(通过 inotify_rm_watch() 显示移除,或者是文件被移动,或者文件系统被卸载)
IN_ISDIR (int)
事件发生的主体是目录
IN_ONLYDIR (int)
如果是目录,仅监听路径名(从 Linux 2.6.15 开始)
IN_DONT_FOLLOW (int)
如果是符号链接,不引用路径名(从 Linux 2.6.15 开始)
IN_MASK_ADD (int)
如果路径名存在,追加监听此路径的事件掩码(代替更换掩码)
IN_ONESHOT (int)
路径触发一个监听事件后,从监听列表中移除。
接下去是1
inotify_read(resource $inotify_instance): array
会返回以下内容1
2
3
4wd 是由 inotify_add_watch() 返回的监听描述符
mask 是 events 的位掩码
cookie 是连接相关事件(例如:IN_MOVE_FROM 和 IN_MOVE_TO)的唯一 id
name 是文件名(例如:监听目录中被修改的文件)
注意这个返回值1
返回 inotify 事件数组。在没有待处理事件或 inotify_instance 非阻塞时返回 false。事件发生时都会返回包含以下键的数组:
如果要设置非阻塞的话1
fd, 0);
$fd就是init返回的inotify对象
如果是非阻塞就是自己循环去read有没有事件,
如果是阻塞的话就是read会一直等到有事件了才返回
还可以通过1
inotify_queue_len(resource $inotify_instance): int
如果有数量代表read不是阻塞的,有待处理的事件
最后还有通过inotify_rm_watch来清除监听器1
inotify_rm_watch(resource $inotify_instance, int $watch_descriptor): bool
这样针对文件系统有变更的话就会比较方便去做下一步处理,
比如压缩,移动归档等
之前最开始的处理方式是每次都遍历,感觉还是挺不科学的,
已有的文件会一遍遍被重复扫描
这个方法还有个局限性就是得在Linux系统里,
其他系统例如mac需要fs_usage这些工具才行
Windows里需要使用 ReadDirectoryChangesW 等api
总结下来这个方式对于Linux系统下的ext4系统是比较方便的,
其他的系统可能需要再研究下