Completed
Push — master ( 598fe1...4a905b )
by Michael
03:41
created

CommonFileHandlingTrait::safeDataRead()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 26
ccs 0
cts 18
cp 0
rs 6.7272
cc 7
eloc 18
nc 5
nop 1
crap 56
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
39
/**
40
 * Trait CommonFileHandlingTrait
41
 */
42
trait CommonFileHandlingTrait
43
{
44
    use FilePathNormalizerTrait;
45
    /**
46
     * @param string $fileName
47
     *
48
     * @return false|string
49
     */
50
    protected function safeFileRead(string $fileName)
51
    {
52
        $fileName = $this->getFpn()
53
            ->normalizeFile($fileName);
54
        if (!is_readable($fileName) || !is_file($fileName)) {
55
            return false;
56
        }
57
        return $this->safeDataRead($fileName);
58
    }
59
    /**
60
     * Safely write file using lock and temp file.
61
     *
62
     * @param string $data
63
     * @param string $pathFile
64
     *
65
     * @return bool
66
     */
67
    protected function safeFileWrite(string $data, string $pathFile): bool
68
    {
69
        $pathFile = $this->getFpn()
70
            ->normalizeFile($pathFile);
71
        $path = dirname($pathFile);
72
        $suffix = substr(strrchr($pathFile, '.'), 1);
73
        $baseFile = basename($pathFile, $suffix);
74
        if (false === $this->isWritablePath($path)) {
75
            return false;
76
        }
77
        if (false === $this->deleteWithRetry($pathFile)) {
78
            return false;
79
        }
80
        $tmpFile = sprintf('%1$s/%2$s.tmp', $path, hash('sha1', $baseFile . microtime()));
81
        if (false === $this->safeDataWrite($data, $tmpFile)) {
82
            return false;
83
        }
84
        return rename($tmpFile, $pathFile);
85
    }
86
    /**
87
     * @param string $fileName
88
     * @param string $mode
89
     *
90
     * @return bool|resource
91
     */
92
    private function acquireLockedHandle(string $fileName, string $mode = 'cb+')
93
    {
94
        $handle = fopen($fileName, $mode, false);
95
        if (false === $handle || false === $this->acquiredLock($handle)) {
96
            return false;
97
        }
98
        return $handle;
99
    }
100
    /**
101
     * @param resource $handle
102
     *
103
     * @return bool
104
     */
105
    private function acquiredLock($handle): bool
106
    {
107
        $tries = 0;
108
        //Give max of 10 seconds or 20 tries to getting lock.
109
        $timeout = time() + 10;
110
        while (!flock($handle, LOCK_EX | LOCK_NB)) {
111
            if (++$tries > 20 || time() > $timeout) {
112
                return false;
113
            }
114
            // Wait 0.01 to 0.5 seconds before trying again.
115
            usleep(random_int(10000, 500000));
116
        }
117
        return true;
118
    }
119
    /**
120
     * Used to delete a file when unlink might fail and it needs to be retried.
121
     *
122
     * @param string $fileName
123
     *
124
     * @return bool
125
     */
126
    private function deleteWithRetry(string $fileName): bool
127
    {
128
        clearstatcache(true, $fileName);
129
        if (!is_file($fileName)) {
130
            return true;
131
        }
132
        // Acquire exclusive access to file to help prevent conflicts when deleting.
133
        $handle = $this->acquireLockedHandle($fileName, 'rb+');
134
        $tries = 0;
135
        do {
136
            if (is_resource($handle)) {
137
                ftruncate($handle, 0);
138
                rewind($handle);
139
                flock($handle, LOCK_UN);
140
                fclose($handle);
141
            }
142
            if (++$tries > 10) {
143
                return false;
144
            }
145
            // Wait 0.01 to 0.5 seconds before trying again.
146
            usleep(random_int(10000, 500000));
147
        } while (false === unlink($fileName));
148
        clearstatcache(true, $fileName);
149
        return true;
150
    }
151
    /**
152
     * @param string $path
153
     *
154
     * @return bool
155
     */
156
    private function isWritablePath(string $path): bool
157
    {
158
        return is_readable($path) && is_dir($path) && is_writable($path);
159
    }
160
    /**
161
     * @param resource $handle
162
     *
163
     * @return self Fluent interface.
164
     */
165
    private function releaseHandle($handle)
166
    {
167
        if (is_resource($handle)) {
168
            flock($handle, LOCK_UN);
169
            fclose($handle);
170
        }
171
        return $this;
172
    }
173
    /**
174
     * @param string $fileName
175
     *
176
     * @return bool|string
177
     */
178
    private function safeDataRead(string $fileName)
179
    {
180
        $handle = $this->acquireLockedHandle($fileName, 'rb+');
181
        if (false === $handle) {
182
            return false;
183
        }
184
        rewind($handle);
185
        $data = '';
186
        $tries = 0;
187
        //Give 10 seconds to try reading file.
188
        $timeout = time() + 10;
189
        while (!feof($handle)) {
190
            if (++$tries > 10 || time() > $timeout) {
191
                $this->releaseHandle($handle);
192
                return false;
193
            }
194
            $read = fread($handle, 16384);
195
            // Decrease $tries while making progress but NEVER $tries < 1.
196
            if ('' !== $read && $tries > 0) {
197
                --$tries;
198
            }
199
            $data .= $read;
200
        }
201
        $this->releaseHandle($handle);
202
        return $data;
203
    }
204
    /**
205
     * @param string $data
206
     * @param string $fileName
207
     *
208
     * @return bool
209
     */
210
    private function safeDataWrite(string $data, string $fileName): bool
211
    {
212
        $handle = $this->acquireLockedHandle($fileName);
213
        if (false === $handle) {
214
            return false;
215
        }
216
        $tries = 0;
217
        //Give 10 seconds to try writing file.
218
        $timeout = time() + 10;
219
        while (strlen($data)) {
220
            if (++$tries > 10 || time() > $timeout) {
221
                $this->releaseHandle($handle)
222
                    ->deleteWithRetry($fileName);
223
                return false;
224
            }
225
            $written = fwrite($handle, $data);
226
            // Decrease $tries while making progress but NEVER $tries < 1.
227
            if ($written > 0 && $tries > 0) {
228
                --$tries;
229
            }
230
            $data = substr($data, $written);
231
        }
232
        $this->releaseHandle($handle);
233
        return true;
234
    }
235
}
236