79 lines
5.2 KiB
PHP
79 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
/** Lossless archival of fully acknowledged OLD outbox days. No device/API calls. */
|
|
final class NetzfahrplanV4Datenarchiv
|
|
{
|
|
public static function run(string $directory, array $cursor, int $now): array
|
|
{
|
|
$day=$cursor['day']??'';
|
|
if ($day==='') return ['archived'=>0];
|
|
if (!is_string($day)||!preg_match('/^raw-[0-9]{8}\.jsonl$/D',$day)
|
|
||!is_int($cursor['offset']??null)||$cursor['offset']<0
|
|
||is_link($directory)||realpath($directory)!==$directory) {
|
|
throw new \RuntimeException('Invalid archive cursor/directory');
|
|
}
|
|
$cutoff='raw-'.gmdate('Ymd',$now-7*86400).'.jsonl';
|
|
$archive=$directory.'/archive';
|
|
if (is_link($archive)) throw new \RuntimeException('Archive symlink refused');
|
|
if (!is_dir($archive)&&!mkdir($archive,0700)) throw new \RuntimeException('Archive directory unavailable');
|
|
if ((fileperms($archive)&0007)!==0) throw new \RuntimeException('Archive must remain private');
|
|
$lockPath=$directory.'/.writer.lock';
|
|
if (is_link($lockPath)) throw new \RuntimeException('Writer lock symlink refused');
|
|
$lock=fopen($lockPath,'c+b');
|
|
if ($lock===false) throw new \RuntimeException('Archive lock unavailable');
|
|
$done=[];
|
|
try {
|
|
if (!flock($lock,LOCK_EX|LOCK_NB)) return ['archived'=>0,'status'=>'writer_busy'];
|
|
foreach (glob($directory.'/raw-*.jsonl')?:[] as $file) {
|
|
$name=basename($file);
|
|
if (!preg_match('/^raw-[0-9]{8}\.jsonl$/D',$name)||$name>=$day||$name>=$cutoff) continue;
|
|
if (is_link($file)||realpath($file)!==$file||!is_file($file)) throw new \RuntimeException('Unexpected journal');
|
|
$before=stat($file);
|
|
if (!$before||$before['size']>67108864) throw new \RuntimeException('Archive input bounds');
|
|
$digest=hash_file('sha256',$file);
|
|
if (!is_string($digest)) throw new \RuntimeException('Cannot hash journal');
|
|
$target=$archive.'/'.$name.'.'.$digest.'.gz';
|
|
if (is_link($target)) throw new \RuntimeException('Archive target symlink');
|
|
if (!is_file($target)) {
|
|
$tmp=tempnam($archive,'.pending-');
|
|
if ($tmp===false) throw new \RuntimeException('Cannot create archive');
|
|
try {
|
|
chmod($tmp,0600);$in=fopen($file,'rb');$gz=gzopen($tmp,'wb6');
|
|
if ($in===false||$gz===false) {if(is_resource($in))fclose($in);if(is_resource($gz))gzclose($gz);throw new \RuntimeException('Cannot open archive streams');}
|
|
try {
|
|
while (!feof($in)) {
|
|
$block=fread($in,65536);
|
|
if ($block===false) throw new \RuntimeException('Archive read failed');
|
|
$offset=0;
|
|
while($offset<strlen($block)) {$n=gzwrite($gz,substr($block,$offset));if($n===false||$n===0)throw new \RuntimeException('Archive write failed');$offset+=$n;}
|
|
}
|
|
} finally {fclose($in);gzclose($gz);}
|
|
$sync=fopen($tmp,'r+b');
|
|
if ($sync===false) throw new \RuntimeException('Cannot sync archive');
|
|
try {if(!fsync($sync))throw new \RuntimeException('Archive sync failed');} finally {fclose($sync);}
|
|
if (!rename($tmp,$target)) throw new \RuntimeException('Archive rename failed');
|
|
} finally {if(is_file($tmp))unlink($tmp);}
|
|
}
|
|
$gz=gzopen($target,'rb');if($gz===false)throw new \RuntimeException('Cannot verify archive');
|
|
$hash=hash_init('sha256');$bytes=0;
|
|
try {while(!gzeof($gz)){$block=gzread($gz,65536);if($block===false)throw new \RuntimeException('Archive verification read failed');$bytes+=strlen($block);if($bytes>$before['size'])throw new \RuntimeException('Oversized decompressed archive');hash_update($hash,$block);}}
|
|
finally {gzclose($gz);}
|
|
clearstatcache(true,$file);$after=stat($file);
|
|
if ($bytes!==$before['size']||hash_final($hash)!==$digest||!$after
|
|
||$after['size']!==$before['size']||$after['ino']!==$before['ino']||$after['mtime']!==$before['mtime']
|
|
||hash_file('sha256',$file)!==$digest) throw new \RuntimeException('Archive mismatch; original retained');
|
|
$directoryHandle=fopen($archive,'r');
|
|
if ($directoryHandle===false) throw new \RuntimeException('Cannot sync archive directory');
|
|
try {if(!fsync($directoryHandle))throw new \RuntimeException('Archive directory sync failed');} finally {fclose($directoryHandle);}
|
|
if (!unlink($file)) throw new \RuntimeException('Verified original could not be retired');
|
|
$done[]=$name;
|
|
if (count($done)>=2) break;
|
|
}
|
|
} finally {flock($lock,LOCK_UN);fclose($lock);}
|
|
return ['archived'=>count($done),'compressedOriginalsRetained'=>true,'unacknowledgedDaysTouched'=>false];
|
|
}
|
|
}
|