├── README.md ├── config.php └── fsmon.php /README.md: -------------------------------------------------------------------------------- 1 | ### What's this 2 | 3 | Script allow monitor changes to files in specified directories. 4 | When changes detected, it'll notify administrator by email or/and write logs 5 | this changes. 6 | 7 | ``` 8 | [ modified] /home/user/www/fs_monitor/fsmon.php 5.1 kb 28.09.2009 23:37 9 | [ new] /home/user/www/fs_monitor/1/sc.phps 1 kb 28.09.2009 23:09 10 | [ deleted] /home/user/www/fs_monitor/1/op.phps 2 kb 01.01.1970 03:00 11 | ``` 12 | 13 | ### Setup 14 | 15 | Make sure `.cache` file php-writable 16 | ``` 17 | touch .cache 18 | chmod g+w ./.cache 19 | ``` 20 | 21 | 22 | ### Basic Configure 23 | 24 | See options in `config.php` 25 | 26 | 27 | ### Run task with cron 28 | 29 | ``` 30 | crontab -e 31 | 0 3 * * * /usr/bin/env php -f /home/user/fsmon.php > /dev/null 2>&1 32 | ``` 33 | 34 | [Описание на русском](http://www.skillz.ru/dev/php/article-Skript_monitoringa_izmenenii_faylov.html) 35 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | '/root/to/scan', 9 | 10 | // allowed multi root 11 | // 'root' => ['/first', '/second', ...] 12 | 13 | //'root' => array( 14 | // __DIR__ . '\\1', 15 | // __DIR__ . '\\2' 16 | //), 17 | 18 | // skip this dirs 19 | //'ignore_dirs' => [ 20 | // 'C:\\path\\fs_monitor\\2\\test1' 21 | //], 22 | 23 | // ServerTag for text reports, default _SERVER[SERVER_NAME] 24 | // 'server' => 'server_name', 25 | 26 | // files pattern 27 | 'files' => '(\.php.?|\.htaccess|\.txt)$', 28 | 29 | // write logs to ./logs/Ym/d-m-y.log 30 | 'log' => true, 31 | 32 | // notify administrator email 33 | 'mail' => array( 34 | 'from' => 'info@skillz.ru', 35 | 'to' => 'rustyj4ck@gmail.com', 36 | 37 | // disabled by default 38 | 'enable' => false 39 | ) 40 | 41 | ); -------------------------------------------------------------------------------- /fsmon.php: -------------------------------------------------------------------------------- 1 | 7 | * @link https://github.com/rustyJ4ck/FSMon 8 | */ 9 | 10 | set_time_limit(0); 11 | error_reporting(E_ALL); 12 | ini_set('display_errors','on'); 13 | 14 | $root_dir = $this_dir = dirname(__FILE__) . DIRECTORY_SEPARATOR; 15 | 16 | // read config 17 | 18 | $config = include($this_dir . 'config.php'); 19 | 20 | if (isset($config['root'])) { 21 | $root_dir = $config['root']; 22 | } 23 | 24 | $files_preg = @$config['files']; 25 | 26 | // server name 27 | 28 | $SERVER_NAME = @$config['server'] ? $config['server'] : @$_SERVER['SERVER_NAME']; 29 | $SERVER_NAME = $SERVER_NAME ? $SERVER_NAME : 'localhost'; 30 | 31 | $precache = $cache = array(); 32 | 33 | console::start(); 34 | 35 | $first_run = false; 36 | 37 | // read cache 38 | 39 | $cache_file = $this_dir . '.cache'; 40 | 41 | if (file_exists($cache_file)) { 42 | $precache = $cache = unserialize(file_get_contents($cache_file)); 43 | } else { 44 | $first_run = true; 45 | } 46 | 47 | // scan 48 | 49 | $result = array(); 50 | 51 | $checked_ids = array(); 52 | 53 | $tree = fsTree::tree($root_dir, $config['ignore_dirs'], $files_preg); 54 | 55 | console::log("[1] list files"); 56 | 57 | foreach ($tree->getFilesIterator() as $f) { 58 | 59 | console::log("...%s", $f); 60 | 61 | $id = fsTree::fileId($f); 62 | 63 | $checked_ids [] = $id; 64 | $csumm = fsTree::crcFile($f); 65 | 66 | if (isset($cache[$id])) { 67 | // existed 68 | if ($cache[$id]['crc'] != $csumm) { 69 | // modded 70 | $cache[$id]['crc'] = $csumm; 71 | $cache[$id]['file'] = $f; 72 | $result[] = array('file' => $f, 'result' => 'modified'); 73 | } else { 74 | // old one 75 | } 76 | } else { 77 | // new one 78 | $cache[$id]['crc'] = $csumm; 79 | $cache[$id]['file'] = $f; 80 | $result[] = array('file' => $f, 'result' => 'new'); 81 | } 82 | 83 | } 84 | 85 | unset($tree); 86 | 87 | console::log("[2] check for deleted files"); 88 | 89 | $deleted = !empty($precache) ? my_array_diff(array_keys($precache), $checked_ids) : false; 90 | 91 | if (!empty($deleted)) { 92 | foreach ($deleted as $id) { 93 | $result[] = array('file' => $precache[$id]['file'], 'result' => 'deleted'); 94 | unset($cache[$id]); 95 | } 96 | } 97 | 98 | console::log("[3] result checks"); 99 | 100 | if (!empty($result)) { 101 | $buffer = ''; 102 | 103 | console::log('Reporting...'); 104 | 105 | foreach ($result as $r) { 106 | 107 | $line = sprintf("[%10s]\t%s\t%s kb\t%s" 108 | , $r['result'] 109 | , $r['file'] 110 | , @round(filesize($r['file']) / 1024, 1) 111 | , @date('d.m.Y H:i', filemtime($r['file'])) 112 | ); 113 | 114 | console::log($line); 115 | 116 | $buffer .= $line; 117 | $buffer .= PHP_EOL; 118 | } 119 | 120 | if ($first_run) { 121 | $buffer = "[First Run]\n\n" . $buffer; 122 | } 123 | 124 | // log 125 | 126 | if (@$config['log']) { 127 | 128 | $logs_dir = dirname(__FILE__) . '/logs/' . date('Ym'); 129 | @mkdir($logs_dir, 0770, 1); 130 | 131 | file_put_contents($logs_dir . '/' . date('d-H-i') . '.log', $buffer); 132 | } 133 | 134 | // mail 135 | 136 | if (@$config['mail']['enable'] && !$first_run) { 137 | 138 | $from = @$config['mail']['from'] ? $config['mail']['from'] : 'root@localhost'; 139 | $to = @$config['mail']['to'] ? $config['mail']['to'] : 'root@localhost'; 140 | 141 | if ($to === 'root@localhost') { 142 | echo "Empty mail@to"; 143 | } else { 144 | 145 | $subject = "FSMon report for " . $SERVER_NAME; 146 | $buffer .= "\n\nGenerated by FSMon | " . date('d.m.Y H:i') . '.'; 147 | 148 | console::log('Message to %s', $to); 149 | 150 | mailer::send( 151 | $from, $to, $subject, $buffer 152 | ); 153 | } 154 | } 155 | } else { 156 | console::log('All clear'); 157 | } 158 | 159 | // 160 | // save result 161 | // 162 | 163 | file_put_contents( 164 | $cache_file 165 | , serialize($cache) 166 | ); 167 | 168 | console::log('Done'); 169 | console::log('Memory [All/Curr] %.2f %.2f', memory_get_peak_usage(), memory_get_usage()); 170 | 171 | // 172 | // Done 173 | // 174 | 175 | function my_array_diff(&$a, &$b) { 176 | $map = $out = array(); 177 | foreach($a as $val) $map[$val] = 1; 178 | foreach($b as $val) if(isset($map[$val])) $map[$val] = 0; 179 | foreach($map as $val => $ok) if($ok) $out[] = $val; 180 | return $out; 181 | } 182 | 183 | class console { 184 | 185 | private static $time; 186 | 187 | static function start() { 188 | self::$time = microtime(1); 189 | } 190 | 191 | static function log() { 192 | $args = func_get_args(); 193 | $format = array_shift($args); 194 | $format = '%.5f| ' . $format; 195 | array_unshift($args, self::time()); 196 | echo vsprintf($format, $args); 197 | echo PHP_EOL; 198 | } 199 | 200 | private static function time() { 201 | return microtime(1) - self::$time; 202 | } 203 | } 204 | 205 | /** 206 | * Mail helper 207 | */ 208 | class mailer { 209 | 210 | static function send($from, $to, $subject, $message) { 211 | 212 | $headers = 'From: ' . $from . "\r\n" . 213 | 'Reply-To: ' . $from . "\r\n" . 214 | "Content-Type: text/plain; charset=\"utf-8\"\r\n" . 215 | 'X-Mailer: PHP/fsmon'; 216 | 217 | return mail($to, $subject, $message, $headers); 218 | } 219 | 220 | } 221 | 222 | /** 223 | * FileSystem helpers 224 | */ 225 | class fsTree { 226 | 227 | const DS = DIRECTORY_SEPARATOR; 228 | const IGNORE_DOT_DIRS = true; 229 | 230 | /** 231 | * Find files 232 | */ 233 | static function lsFiles($o_dir, $files_preg = '') { 234 | $ret = array(); 235 | $dir = @opendir($o_dir); 236 | 237 | if (!$dir) { 238 | return false; 239 | } 240 | 241 | while (false !== ($file = readdir($dir))) { 242 | $path = $o_dir . /*DIRECTORY_SEPARATOR .*/ 243 | $file; 244 | if ($file !== '..' && $file !== '.' && !is_dir($path) 245 | && (empty($files_preg) || (!empty($files_preg) && preg_match("#{$files_preg}#", $file))) 246 | ) { 247 | $ret []= $path; 248 | } 249 | } 250 | 251 | closedir($dir); 252 | 253 | return $ret; 254 | } 255 | 256 | /** 257 | * Scan dirs. One level 258 | */ 259 | static function lsDirs($o_dir) { 260 | 261 | $ret = array(); 262 | $dir = @opendir($o_dir); 263 | 264 | if (!$dir) { 265 | return false; 266 | } 267 | 268 | while (false !== ($file = readdir($dir))) { 269 | $path = $o_dir /*. DIRECTORY_SEPARATOR*/ . $file; 270 | if ($file !== '..' && $file !== '.' && is_dir($path)) { 271 | $ret [] = $path; 272 | } 273 | } 274 | 275 | closedir($dir); 276 | 277 | return $ret; 278 | } 279 | 280 | private $_files = array(); 281 | private $_dirs = array(); 282 | 283 | function getFilesIterator() { 284 | return new ArrayIterator($this->_files); 285 | } 286 | 287 | function getDirsIterator() { 288 | return new ArrayIterator($this->_dirs); 289 | } 290 | 291 | /** 292 | * Build tree 293 | * 294 | * @desc build tree 295 | * @param string|array root 296 | * @param array &buffer 297 | * @param array dir filters 298 | * @param string file regex filter 299 | * @return fsTree 300 | */ 301 | 302 | public static function tree($root_path, $dirs_filter = array(), $files_preg = '.*') 303 | { 304 | $self = new self; 305 | $self->buildTree($root_path, $dirs_filter, $files_preg); 306 | return $self; 307 | } 308 | 309 | public function buildTree($root_path, $dirs_filter = array(), $files_preg = '.*') 310 | { 311 | if (empty($root_path)) { 312 | return; 313 | } 314 | 315 | if (!is_array($root_path)) { 316 | $root_path = array($root_path); 317 | } 318 | 319 | foreach ($root_path as $path) { 320 | 321 | $_path = $path; //no-slash 322 | 323 | if (substr($path, -1, 1) != self::DS) $path .= self::DS; 324 | 325 | console::log("ls %s ", $_path); 326 | 327 | $skipper = false; 328 | 329 | if (self::IGNORE_DOT_DIRS) { 330 | $exPath = explode(self::DS, $_path); 331 | $dirname = array_pop($exPath); 332 | $skipper = (substr($dirname, 0, 1) === '.'); 333 | } 334 | 335 | if (!$skipper && (empty($dirs_filter) || !in_array($_path, $dirs_filter))) { 336 | 337 | $dirs = self::lsDirs($path); 338 | 339 | if ($dirs === false) { 340 | //opendir(/var/www/html/...): failed to open dir: Permission denied 341 | console::log('..opendir failed!'); 342 | } else { 343 | 344 | $files = self::lsFiles($path, $files_preg); 345 | 346 | $this->_dirs []= $path; 347 | $this->_files = array_merge($this->_files, $files); 348 | 349 | $this->buildTree($dirs, $dirs_filter, $files_preg); 350 | 351 | } 352 | 353 | } else { 354 | console::log("...skipped %s", $_path); 355 | } 356 | } 357 | 358 | 359 | 360 | 361 | } 362 | 363 | /** 364 | * unique file name 365 | */ 366 | public static function fileId($path) { 367 | return md5($path); 368 | } 369 | 370 | /** 371 | * Checksum 372 | */ 373 | public static function crcFile($path) { 374 | return sprintf("%u", crc32(file_get_contents($path))); 375 | } 376 | } 377 | 378 | --------------------------------------------------------------------------------