Completed
Push — master ( 31b5db...524ecf )
by Michael
03:26
created

CommonFileHandlingTrait::deleteWithRetry()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 27
ccs 0
cts 25
cp 0
rs 8.439
cc 5
eloc 20
nc 5
nop 2
crap 30
1
<?php
2
declare(strict_types = 1);
3
/**
4
 * Contains CommonFileHandlingTrait Trait.
5
 *
6
 * PHP version 7.0+
7
 *
8
 * LICENSE:
9
 * This file is part of Yet Another Php Eve Api Library also know as Yapeal
10
 * which can be used to access the Eve Online API data and place it into a
11
 * database.
12
 * Copyright (C) 2015-2016 Michael Cummings
13
 *
14
 * This program is free software: you can redistribute it and/or modify it
15
 * under the terms of the GNU Lesser General Public License as published by the
16
 * Free Software Foundation, either version 3 of the License, or (at your
17
 * option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful, but WITHOUT
20
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
21
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
22
 * for more details.
23
 *
24
 * You should have received a copy of the GNU Lesser General Public License
25
 * along with this program. If not, see
26
 * <http://spdx.org/licenses/LGPL-3.0.html>.
27
 *
28
 * You should be able to find a copy of this license in the COPYING-LESSER.md
29
 * file. A copy of the GNU GPL should also be available in the COPYING.md file. 
30
 *
31
 * @copyright 2015-2016 Michael Cummings
32
 * @license   http://www.gnu.org/copyleft/lesser.html GNU LGPL
33
 * @author    Michael Cummings <[email protected]>
34
 */
35
namespace Yapeal\FileSystem;
36
37
use FilePathNormalizer\FilePathNormalizerTrait;
38
use Yapeal\Event\MediatorInterface;
39
use Yapeal\Log\Logger;
40
41
/**
42
 * Trait CommonFileHandlingTrait
43
 */
44
trait CommonFileHandlingTrait
45
{
46
    use FilePathNormalizerTrait;
47
    /**
48
     * @param string            $fileName
49
     *
50
     * @param MediatorInterface $yem
51
     *
52
     * @return false|string
53
     * @throws \DomainException
54
     * @throws \InvalidArgumentException
55
     * @throws \LogicException
56
     */
57
    protected function safeFileRead(string $fileName, MediatorInterface $yem)
58
    {
59
        $fileName = $this->getFpn()
60
            ->normalizeFile($fileName);
61
        if (!is_readable($fileName) || !is_file($fileName)) {
62
            $mess = 'Could NOT find accessible file, was given ' . $fileName;
63
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::INFO, $mess);
64
            return false;
65
        }
66
        return $this->safeDataRead($fileName, $yem);
67
    }
68
    /**
69
     * Safely write file using lock and temp file.
70
     *
71
     * @param string            $data
72
     * @param string            $pathFile
73
     * @param MediatorInterface $yem
74
     *
75
     * @return bool
76
     * @throws \DomainException
77
     * @throws \InvalidArgumentException
78
     * @throws \LogicException
79
     */
80
    protected function safeFileWrite(string $data, string $pathFile, MediatorInterface $yem): bool
81
    {
82
        $pathFile = $this->getFpn()
83
            ->normalizeFile($pathFile);
84
        $path = dirname($pathFile);
85
        $suffix = substr(strrchr($pathFile, '.'), 1);
86
        $baseFile = basename($pathFile, $suffix);
87
        if (false === $this->isWritablePath($path, $yem)) {
88
            return false;
89
        }
90
        if (false === $this->deleteWithRetry($pathFile, $yem)) {
91
            return false;
92
        }
93
        $tmpFile = sprintf('%1$s/%2$s.tmp', $path, $baseFile);
94
        if (false === $this->safeDataWrite($data, $tmpFile, $yem)) {
95
            return false;
96
        }
97
        if (false === rename($tmpFile, $pathFile)) {
98
            $mess = sprintf('Could NOT rename %1$s to %2$s', $tmpFile, $pathFile);
99
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
100
            return false;
101
        }
102
        return true;
103
    }
104
    /**
105
     * @param string            $fileName
106
     * @param MediatorInterface $yem
107
     * @param string            $mode
108
     *
109
     * @return bool|resource
110
     * @throws \DomainException
111
     * @throws \InvalidArgumentException
112
     * @throws \LogicException
113
     */
114
    private function acquireLockedHandle(string $fileName, MediatorInterface $yem, string $mode = 'cb+')
115
    {
116
        $handle = fopen($fileName, $mode, false);
117
        if (false === $handle) {
118
            $mess = sprintf('Failed to get %1$s file handle', $fileName);
119
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
120
            return false;
121
        }
122
        if (false === $this->acquiredLock($handle)) {
123
            $mess = sprintf('Failed to get exclusive lock to %1$s', $fileName);
124
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
125
            return false;
126
        }
127
        return $handle;
128
    }
129
    /**
130
     * @param resource $handle
131
     *
132
     * @return bool
133
     */
134
    private function acquiredLock($handle): bool
135
    {
136
        $tries = 0;
137
        //Give max of 10 seconds to try getting lock.
138
        $timeout = time() + 10;
139
        while (!flock($handle, LOCK_EX | LOCK_NB)) {
140
            if (++$tries > 20 || time() > $timeout) {
141
                return false;
142
            }
143
            // Wait 0.01 to 0.5 seconds before trying again.
144
            usleep(random_int(10000, 500000));
145
        }
146
        return true;
147
    }
148
    /**
149
     * Used to delete a file when unlink might fail and it needs to be retried.
150
     *
151
     * @param string            $fileName
152
     * @param MediatorInterface $yem
153
     *
154
     * @return bool
155
     * @throws \DomainException
156
     * @throws \InvalidArgumentException
157
     * @throws \LogicException
158
     */
159
    private function deleteWithRetry(string $fileName, MediatorInterface $yem): bool
160
    {
161
        clearstatcache(true, $fileName);
162
        if (!is_file($fileName)) {
163
            return true;
164
        }
165
        // Acquire exclusive access to file to help prevent conflicts when deleting.
166
        $handle = $this->acquireLockedHandle($fileName, $yem, 'rb+');
167
        $tries = 0;
168
        do {
169
            if (is_resource($handle)) {
170
                ftruncate($handle, 0);
171
                rewind($handle);
172
                flock($handle, LOCK_UN);
173
                fclose($handle);
174
            }
175
            if (++$tries > 10) {
176
                $mess = sprintf('To many retries when trying to delete %1$s', $fileName);
177
                $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
178
                return false;
179
            }
180
            // Wait 0.01 to 0.5 seconds before trying again.
181
            usleep(random_int(10000, 500000));
182
        } while (false === unlink($fileName));
183
        clearstatcache(true, $fileName);
184
        return true;
185
    }
186
    /**
187
     * @param string            $path
188
     *
189
     * @param MediatorInterface $yem
190
     *
191
     * @return bool
192
     * @throws \DomainException
193
     * @throws \InvalidArgumentException
194
     */
195
    private function isWritablePath(string $path, MediatorInterface $yem): bool
196
    {
197
        if (!is_readable($path)) {
198
            $mess = 'Path is NOT readable or does NOT exist, was given ' . $path;
199
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
200
            return false;
201
        }
202
        if (!is_dir($path)) {
203
            $mess = 'Path is NOT a directory, was given ' . $path;
204
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
205
            return false;
206
        }
207
        if (!is_writable($path)) {
208
            $mess = 'Path is NOT writable, was given ' . $path;
209
            $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
210
            return false;
211
        }
212
        return true;
213
    }
214
    /**
215
     * @param resource $handle
216
     *
217
     * @return self Fluent interface.
218
     */
219
    private function releaseHandle($handle)
220
    {
221
        if (is_resource($handle)) {
222
            flock($handle, LOCK_UN);
223
            fclose($handle);
224
        }
225
        return $this;
226
    }
227
    /**
228
     * @param string            $fileName
229
     * @param MediatorInterface $yem
230
     *
231
     * @return bool|string
232
     * @throws \DomainException
233
     * @throws \InvalidArgumentException
234
     * @throws \LogicException
235
     */
236
    private function safeDataRead(string $fileName, MediatorInterface $yem)
237
    {
238
        $handle = $this->acquireLockedHandle($fileName, $yem, 'rb+');
239
        if (false === $handle) {
240
            return false;
241
        }
242
        rewind($handle);
243
        $data = '';
244
        $tries = 0;
245
        //Give 10 seconds to try reading file.
246
        $timeout = time() + 10;
247
        while (!feof($handle)) {
248
            if (++$tries > 10 || time() > $timeout) {
249
                $mess = sprintf('Giving up could NOT finish reading data from %1$s', $fileName);
250
                $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
251
                $this->releaseHandle($handle);
252
                return false;
253
            }
254
            $read = fread($handle, 16384);
255
            // Decrease $tries while making progress but NEVER $tries < 1.
256
            if ('' !== $read && $tries > 0) {
257
                --$tries;
258
            }
259
            $data .= $read;
260
        }
261
        $this->releaseHandle($handle);
262
        return $data;
263
    }
264
    /**
265
     * @param string            $data
266
     * @param string            $fileName
267
     * @param MediatorInterface $yem
268
     *
269
     * @return bool
270
     * @throws \DomainException
271
     * @throws \InvalidArgumentException
272
     * @throws \LogicException
273
     */
274
    private function safeDataWrite(string $data, string $fileName, MediatorInterface $yem): bool
275
    {
276
        $handle = $this->acquireLockedHandle($fileName, $yem);
277
        if (false === $handle) {
278
            return false;
279
        }
280
        $tries = 0;
281
        //Give 10 seconds to try writing file.
282
        $timeout = time() + 10;
283
        while (strlen($data)) {
284
            if (++$tries > 10 || time() > $timeout) {
285
                $mess = sprintf('Giving up could NOT finish writing data to %1$s', $fileName);
286
                $yem->triggerLogEvent('Yapeal.Log.log', Logger::NOTICE, $mess);
287
                $this->releaseHandle($handle)
288
                    ->deleteWithRetry($fileName, $yem);
289
                return false;
290
            }
291
            $written = fwrite($handle, $data);
292
            // Decrease $tries while making progress but NEVER $tries < 1.
293
            if ($written > 0 && $tries > 0) {
294
                --$tries;
295
            }
296
            $data = substr($data, $written);
297
        }
298
        $this->releaseHandle($handle);
299
        return true;
300
    }
301
}
302