Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
HybridLogFile
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 19
1190
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 modified
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 open
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 seek
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 tell
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 eof
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 gets
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 close
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 optimize
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 copyToGz
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 copyToPlain
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 deletePlain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 purge
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 serialize
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 deserialize
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace Olz\Apps\Logs\Utils;
4
5use Olz\Utils\WithUtilsTrait;
6
7class HybridLogFile implements LogFileInterface {
8    use WithUtilsTrait;
9
10    protected ?LogFileInterface $plainLogFile;
11
12    public function __construct(
13        public string $gzPath,
14        public string $plainPath,
15        public string $indexPath,
16    ) {
17    }
18
19    public function getPath(): string {
20        return $this->gzPath;
21    }
22
23    public function getIndexPath(): string {
24        return $this->indexPath;
25    }
26
27    public function exists(): bool {
28        return is_file($this->gzPath) || is_file($this->plainPath);
29    }
30
31    public function modified(): int {
32        $result = match (true) {
33            is_file($this->gzPath) => filemtime($this->gzPath),
34            is_file($this->plainPath) => filemtime($this->plainPath),
35            default => false,
36        };
37        $this->generalUtils()->checkNotBool($result, "filemtime({$this->gzPath}) failed");
38        return $result;
39    }
40
41    /** @return resource */
42    public function open(string $mode): mixed {
43        if (!is_file($this->plainPath)) {
44            $this->copyToPlain();
45        }
46        $this->plainLogFile = new PlainLogFile($this->plainPath, $this->indexPath);
47        $this->generalUtils()->checkNotNull($this->plainLogFile, "No plain log file");
48        return $this->plainLogFile->open($mode);
49    }
50
51    /** @param resource $fp */
52    public function seek(mixed $fp, int $offset, int $whence = SEEK_SET): int {
53        $this->generalUtils()->checkNotNull($this->plainLogFile, "No plain log file");
54        return $this->plainLogFile->seek($fp, $offset, $whence);
55    }
56
57    /** @param resource $fp */
58    public function tell(mixed $fp): int {
59        $this->generalUtils()->checkNotNull($this->plainLogFile, "No plain log file");
60        return $this->plainLogFile->tell($fp);
61    }
62
63    /** @param resource $fp */
64    public function eof(mixed $fp): bool {
65        $this->generalUtils()->checkNotNull($this->plainLogFile, "No plain log file");
66        return $this->plainLogFile->eof($fp);
67    }
68
69    /** @param resource $fp */
70    public function gets(mixed $fp): ?string {
71        $this->generalUtils()->checkNotNull($this->plainLogFile, "No plain log file");
72        return $this->plainLogFile->gets($fp);
73    }
74
75    /** @param resource $fp */
76    public function close(mixed $fp): bool {
77        $this->generalUtils()->checkNotNull($this->plainLogFile, "No plain log file");
78        $result = $this->plainLogFile->close($fp);
79        $this->plainLogFile = null;
80        if (is_file($this->gzPath)) {
81            $this->deletePlain();
82        }
83        return $result;
84    }
85
86    public function optimize(): void {
87        $has_gz = is_file($this->gzPath);
88        $has_plain = is_file($this->plainPath);
89        $pretty_has_gz = $has_gz ? '✅' : '🚫';
90        $pretty_has_plain = $has_plain ? '✅' : '🚫';
91        $this->log()->debug("Optimizing hybrid log file {$this->plainPath} {$pretty_has_plain} / {$this->gzPath} {$pretty_has_gz}...");
92        if (!$has_gz && $has_plain) {
93            $this->copyToGz();
94        }
95        if ($has_plain) {
96            $this->deletePlain();
97        }
98    }
99
100    protected function copyToGz(): void {
101        $this->log()->debug("Optimize hybrid log file {$this->plainPath} -> {$this->gzPath}");
102        $fp = fopen($this->plainPath, 'r');
103        $gzp = gzopen($this->gzPath, 'wb');
104        $this->generalUtils()->checkNotBool($fp, 'fopen failed');
105        $this->generalUtils()->checkNotBool($gzp, 'gzopen failed');
106        while ($buf = fread($fp, 1024)) {
107            gzwrite($gzp, $buf, 1024);
108        }
109        fclose($fp);
110        gzclose($gzp);
111    }
112
113    protected function copyToPlain(): void {
114        $this->log()->debug("Cache hybrid log file {$this->gzPath} -> {$this->plainPath}");
115        $gzp = gzopen($this->gzPath, 'rb');
116        $fp = fopen($this->plainPath, 'w');
117        $this->generalUtils()->checkNotBool($gzp, 'gzopen failed');
118        $this->generalUtils()->checkNotBool($fp, 'fopen failed');
119        while ($buf = gzread($gzp, 1024)) {
120            fwrite($fp, $buf, 1024);
121        }
122        gzclose($gzp);
123        fclose($fp);
124    }
125
126    protected function deletePlain(): void {
127        $this->log()->debug("Remove redundant hybrid log file {$this->plainPath}");
128        unlink($this->plainPath);
129    }
130
131    public function purge(): void {
132        if (is_file($this->gzPath)) {
133            unlink($this->gzPath);
134            $this->log()->info("Removed old gz log file {$this->gzPath}");
135        }
136        if (is_file($this->plainPath)) {
137            unlink($this->plainPath);
138            $this->log()->info("Removed old plain log file {$this->plainPath}");
139        }
140        if (is_file($this->indexPath)) {
141            unlink($this->indexPath);
142            $this->log()->info("Removed old log index file {$this->indexPath}");
143        }
144    }
145
146    public function __toString(): string {
147        return "HybridLogFile({$this->gzPath}{$this->plainPath}{$this->indexPath})";
148    }
149
150    public function serialize(): string {
151        return json_encode([
152            'class' => self::class,
153            'plainPath' => $this->plainPath,
154            'gzPath' => $this->gzPath,
155            'indexPath' => $this->indexPath,
156        ]) ?: '{}';
157    }
158
159    public static function deserialize(string $serialized): ?LogFileInterface {
160        $deserialized = json_decode($serialized, true);
161        if ($deserialized['class'] !== self::class) {
162            return null;
163        }
164        return new self(
165            $deserialized['plainPath'],
166            $deserialized['gzPath'],
167            $deserialized['indexPath'],
168        );
169    }
170}