Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 80
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 / 80
0.00% covered (danger)
0.00%
0 / 19
1056
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 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 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        if (!$has_gz && $has_plain) {
90            $this->copyToGz();
91        }
92        if ($has_plain) {
93            $this->deletePlain();
94        }
95    }
96
97    protected function copyToGz(): void {
98        $this->log()->debug("Optimize hybrid log file {$this->plainPath} -> {$this->gzPath}");
99        $fp = fopen($this->plainPath, 'r');
100        $gzp = gzopen($this->gzPath, 'wb');
101        $this->generalUtils()->checkNotBool($fp, 'fopen failed');
102        $this->generalUtils()->checkNotBool($gzp, 'gzopen failed');
103        while ($buf = fread($fp, 1024)) {
104            gzwrite($gzp, $buf, 1024);
105        }
106        fclose($fp);
107        gzclose($gzp);
108    }
109
110    protected function copyToPlain(): void {
111        $this->log()->debug("Cache hybrid log file {$this->gzPath} -> {$this->plainPath}");
112        $gzp = gzopen($this->gzPath, 'rb');
113        $fp = fopen($this->plainPath, 'w');
114        $this->generalUtils()->checkNotBool($gzp, 'gzopen failed');
115        $this->generalUtils()->checkNotBool($fp, 'fopen failed');
116        while ($buf = gzread($gzp, 1024)) {
117            fwrite($fp, $buf, 1024);
118        }
119        gzclose($gzp);
120        fclose($fp);
121    }
122
123    protected function deletePlain(): void {
124        $this->log()->debug("Remove redundant hybrid log file {$this->plainPath}");
125        unlink($this->plainPath);
126    }
127
128    public function purge(): void {
129        if (is_file($this->gzPath)) {
130            unlink($this->gzPath);
131            $this->log()->info("Removed old gz log file {$this->gzPath}");
132        }
133        if (is_file($this->plainPath)) {
134            unlink($this->plainPath);
135            $this->log()->info("Removed old plain log file {$this->plainPath}");
136        }
137        if (is_file($this->indexPath)) {
138            unlink($this->indexPath);
139            $this->log()->info("Removed old log index file {$this->indexPath}");
140        }
141    }
142
143    public function __toString(): string {
144        return "HybridLogFile({$this->gzPath}{$this->plainPath}{$this->indexPath})";
145    }
146
147    public function serialize(): string {
148        return json_encode([
149            'class' => self::class,
150            'plainPath' => $this->plainPath,
151            'gzPath' => $this->gzPath,
152            'indexPath' => $this->indexPath,
153        ]) ?: '{}';
154    }
155
156    public static function deserialize(string $serialized): ?LogFileInterface {
157        $deserialized = json_decode($serialized, true);
158        if ($deserialized['class'] !== self::class) {
159            return null;
160        }
161        return new self(
162            $deserialized['plainPath'],
163            $deserialized['gzPath'],
164            $deserialized['indexPath'],
165        );
166    }
167}