Completed
Push — master ( b850cb...4c94e8 )
by Andrii
02:36
created

Storage::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * Asset Packagist.
4
 *
5
 * @link      https://github.com/hiqdev/asset-packagist
6
 * @package   asset-packagist
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2016-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\assetpackagist\components;
12
13
use hiqdev\assetpackagist\exceptions\AssetFileStorageException;
14
use hiqdev\assetpackagist\helpers\Locker;
15
use hiqdev\assetpackagist\models\AssetPackage;
16
use Yii;
17
use yii\base\Component;
18
use yii\helpers\Json;
19
20
class Storage extends Component implements StorageInterface
21
{
22
    protected $_path;
23
    protected $_locker;
24
25
    /**
26
     * {@inheritdoc}
27
     */
28 1
    public function init()
29
    {
30 1
        $this->_path = Yii::getAlias('@storage', false);
31 1
    }
32
33
    protected function getLocker()
34
    {
35
        if ($this->_locker === null) {
36
            $this->_locker = Locker::getInstance($this->buildPath('lock')); // TODO: get rid of singleton
37
        }
38
39
        return $this->_locker;
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function getNextId()
46
    {
47
        $this->getLocker()->lock();
48
        {
49
            $nextID = $this->readLastId() + 1;
50
            $this->writeLastId($nextID);
51
        }
52
        $this->getLocker()->release();
53
54
        return $nextID;
55
    }
56
57
    protected function readLastId()
58
    {
59
        $path = $this->getLastIdPath();
60
61
        return (file_exists($path) ? (int) file_get_contents($path) : 0) ?: 1000000;
62
    }
63
64
    protected function writeLastId($value)
65
    {
66
        if (file_put_contents($this->getLastIdPath(), $value) === false) {
67
            throw new AssetFileStorageException('Filed to write lastId to the storage');
68
        }
69
    }
70
71
    protected function getLastIdPath()
72
    {
73
        return $this->buildPath('lastid');
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     */
79
    public function writePackage(AssetPackage $package)
80
    {
81
        $name = $package->getNormalName();
82
        $data = [
83
            'packages' => [
84
                $name => $package->getReleases(),
85
            ],
86
        ];
87
        $json = Json::encode($data);
88
        $hash = hash('sha256', $json);
89
        $path = $this->buildHashedPath($name, $hash);
90
        $latestPath = $this->buildHashedPath($name);
91
        if (!file_exists($path)) {
92
            $this->getLocker()->lock();
93
            try {
94
                if ($this->mkdir(dirname($path)) === false) {
95
                    throw new AssetFileStorageException('Failed to create a directory for asset-package', $package);
96
                }
97
                if (file_put_contents($path, $json) === false) {
98
                    throw new AssetFileStorageException('Failed to write package', $package);
99
                }
100
                if (file_put_contents($latestPath, $json) === false) {
101
                    throw new AssetFileStorageException('Failed to write file "latest.json" for asset-packge', $package);
102
                }
103
                $this->writeProviderLatest($name, $hash);
104
            } finally {
105
                $this->getLocker()->release();
106
            }
107
        } else {
108
            touch($latestPath);
109
        }
110
111
        return $hash;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function readPackage(AssetPackage $package)
118
    {
119
        $name = $package->getNormalName();
120
        $path = $this->buildHashedPath($name);
121
        clearstatcache(false, $path);
122
        if (!file_exists($path)) {
123
            return null;
124
        }
125
        $json = file_get_contents($path);
126
        $updateTime = filemtime($path);
127
        $hash = hash('sha256', $json);
128
        $data = Json::decode($json);
129
        $releases = isset($data['packages'][$name]) ? $data['packages'][$name] : [];
130
131
        return compact('hash', 'releases', 'updateTime');
132
    }
133
134
    protected function buildPath()
135
    {
136
        $args = func_get_args();
137
        array_unshift($args, $this->_path);
138
139
        return implode(DIRECTORY_SEPARATOR, $args);
140
    }
141
142
    protected function buildHashedPath($name, $hash = 'latest')
143
    {
144
        return $this->buildPath('p', $name, $hash . '.json');
145
    }
146
147
    protected function writeProviderLatest($name, $hash)
148
    {
149
        $latestPath = $this->buildHashedPath('provider-latest');
150
        if (file_exists($latestPath)) {
151
            $data = Json::decode(file_get_contents($latestPath) ?: '[]');
152
        }
153
        if (!isset($data) || !is_array($data)) {
154
            $data = [];
155
        }
156
        if (!isset($data['providers'])) {
157
            $data['providers'] = [];
158
        }
159
        $data['providers'][$name] = ['sha256' => $hash];
160
        $json = Json::encode($data);
161
        $hash = hash('sha256', $json);
162
        $path = $this->buildHashedPath('provider-latest', $hash);
163
164
        if (!file_exists($path)) {
165
            $this->getLocker()->lock();
166
167
            try {
168
                if ($this->mkdir(dirname($path)) === false) {
169
                    throw new AssetFileStorageException('Failed to create a directory for provider-latest storage');
170
                }
171 View Code Duplication
                if (file_put_contents($path, $json) === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
172
                    throw new AssetFileStorageException('Failed to write package to provider-latest storage for package "' . $name . '"');
173
                }
174 View Code Duplication
                if (file_put_contents($latestPath, $json) === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
                    throw new AssetFileStorageException('Failed to write file "latest.json" to provider-latest storage for package "' . $name . '"');
176
                }
177
                $this->writePackagesJson($hash);
178
            } finally {
179
                $this->getLocker()->release();
180
            }
181
        } else {
182
            touch($latestPath);
183
        }
184
185
        return $hash;
186
    }
187
188
    protected function writePackagesJson($hash)
189
    {
190
        $data = [
191
            'providers-url'     => '/p/%package%/%hash%.json',
192
            'provider-includes' => [
193
                'p/provider-latest/%hash%.json' => [
194
                    'sha256' => $hash,
195
                ],
196
            ],
197
        ];
198
        $this->getLocker()->lock();
199
        $filename = $this->buildPath('packages.json');
200
        try {
201
            if (file_put_contents($filename, Json::encode($data)) === false) {
202
                throw new AssetFileStorageException('Failed to write main packages.json');
203
            }
204
            touch($filename);
205
        } finally {
206
            $this->getLocker()->release();
207
        }
208
    }
209
210
    /**
211
     * Creates directory $dir and sets chmod 777.
212
     * @param string $dir
213
     * @return bool whether the directory was created successfully
214
     */
215
    protected function mkdir($dir)
216
    {
217
        if (!file_exists($dir)) {
218
            return @mkdir($dir, 0777, true);
219
        }
220
221
        return true;
222
    }
223
224
    public function readJson($path)
225
    {
226
        return Json::decode(file_get_contents($this->buildPath($path)));
227
    }
228
229
    protected function readPackagesJson()
230
    {
231
        $data = $this->readJson('packages.json');
232
233
        return $data['provider-includes'];
234
    }
235
236
    protected function readProvider($path)
237
    {
238
        $data = $this->readJson($path);
239
240
        return $data['providers'];
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function listPackages()
247
    {
248
        $packages = [];
249
        $providers = $this->readPackagesJson();
250
        foreach ($providers as $path => $data) {
251
            $path = strtr($path, ['%hash%' => $data['sha256']]);
252
            $packages = array_merge($packages, $this->readProvider($path));
253
        }
254
255
        return $packages;
256
    }
257
}
258