Completed
Push — master ( df2080...98a4e1 )
by Mark
68:27 queued 33:33
created

CloudFileExtension::createLocalIfNeeded()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 6.6038
cc 8
eloc 12
nc 6
nop 0
1
<?php
2
/**
3
 * 
4
 *
5
 * @author Mark Guinn <[email protected]>
6
 * @date 01.10.2014
7
 * @package cloudassets
8
 */
9
class CloudFileExtension extends DataExtension
0 ignored issues
show
Complexity introduced by
This class has a complexity of 67 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

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

Loading history...
10
{
11
    private static $db = array(
12
        'CloudStatus'   => "Enum('Local,Live,Error','Local')",
13
        'CloudSize'     => 'Int',
14
        'CloudMetaJson' => 'Text',      // saves any bucket or file-type specific information
15
    );
16
17
18
    private $inUpdate = false;
19
20
21
    /**
22
     * Handle renames
23
     */
24
    public function onBeforeWrite()
25
    {
26
        $bucket = CloudAssets::inst()->map($this->owner->getFilename());
27
        if ($bucket) {
28
            if (!$this->owner->isChanged('Filename')) {
29
                return;
30
            }
31
32
            $changedFields = $this->owner->getChangedFields();
33
            $pathBefore = $changedFields['Filename']['before'];
34
            $pathAfter = $changedFields['Filename']['after'];
35
36
            // If the file or folder didn't exist before, don't rename - its created
37
            if (!$pathBefore) {
38
                return;
39
            }
40
41
            // Tell the remote to rename the file (or delete and recreate or whatever)
42
            if ($this->owner->hasMethod('onBeforeCloudRename')) {
43
                $this->owner->onAfterCloudRename($pathBefore, $pathAfter);
44
            }
45
            CloudAssets::inst()->getLogger()->info("CloudAssets: Renaming $pathBefore to $pathAfter");
46
            $bucket->rename($this->owner, $pathBefore, $pathAfter);
47
            if ($this->owner->hasMethod('onAfterCloudRename')) {
48
                $this->owner->onAfterCloudRename($pathBefore, $pathAfter);
49
            }
50
        }
51
    }
52
53
54
    /**
55
     * Update cloud status any time the file is written
56
     */
57
    public function onAfterWrite()
58
    {
59
        $this->updateCloudStatus();
60
    }
61
62
63
    /**
64
     * Delete the file from the cloud (if it was ever there)
65
     */
66
    public function onAfterDelete()
67
    {
68
        $bucket = CloudAssets::inst()->map($this->owner->getFilename());
69
        if ($bucket && !Config::inst()->get('CloudAssets', 'uploads_disabled')) {
70
            if ($this->owner->hasMethod('onBeforeCloudDelete')) {
71
                $this->owner->onBeforeCloudDelete();
72
            }
73
74
            try {
75
                CloudAssets::inst()->getLogger()->info("CloudAssets: deleting {$this->owner->getFilename()}");
76
                $bucket->delete($this->owner);
77
            } catch (Exception $e) {
78
                CloudAssets::inst()->getLogger()->error("CloudAssets: Failed bucket delete: " . $e->getMessage() . " for " . $this->owner->getFullPath());
79
            }
80
81
            if ($this->owner->hasMethod('onAfterCloudDelete')) {
82
                $this->owner->onAfterCloudDelete();
83
            }
84
        }
85
    }
86
87
88
    /**
89
     * Performs two functions:
90
     * 1. Wraps this object in CloudFile (etc) by changing the classname if it should be and is not
91
     * 2. Uploads the file to the cloud storage if it doesn't contain the placeholder
92
     *
93
     * @return File
94
     */
95
    public function updateCloudStatus()
96
    {
97
        if ($this->inUpdate) {
98
            return;
99
        }
100
        $this->inUpdate = true;
101
        $cloud  = CloudAssets::inst();
102
103
        // does this file fall under a cloud bucket?
104
        $bucket = $cloud->map($this->owner->getFilename());
105
        if ($bucket) {
106
            // does this file need to be wrapped?
107
            $wrapClass = $cloud->getWrapperClass($this->owner->ClassName);
108
            if (!empty($wrapClass)) {
109
                if ($wrapClass != $this->owner->ClassName) {
110
                    $cloud->getLogger()->debug("CloudAssets: wrapping {$this->owner->ClassName} to $wrapClass. ID={$this->owner->ID}");
111
                    $this->owner->ClassName = $wrapClass;
112
                    $this->owner->write();
113
                    $wrapped = DataObject::get($wrapClass)->byID($this->owner->ID);
114
                    if ($wrapped->hasMethod('onAfterCloudWrap')) {
115
                        $wrapped->onAfterCloudWrap();
116
                    }
117
                } else {
118
                    $wrapped = $this->owner;
119
                }
120
121
                // does this file need to be uploaded to storage?
122
                if ($wrapped->canBeInCloud() && $wrapped->isCloudPutNeeded() && !Config::inst()->get('CloudAssets', 'uploads_disabled')) {
123
                    try {
124
                        if ($wrapped->hasMethod('onBeforeCloudPut')) {
125
                            $wrapped->onBeforeCloudPut();
126
                        }
127
                        $cloud->getLogger()->debug("CloudAssets: uploading file ".$wrapped->getFilename());
128
                        $bucket->put($wrapped);
0 ignored issues
show
Documentation introduced by
$wrapped is of type null|object, but the function expects a object<File>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
129
130
                        $wrapped->setCloudMeta('LastPut', time());
131
                        $wrapped->CloudStatus = 'Live';
132
                        $wrapped->CloudSize   = filesize($this->owner->getFullPath());
133
                        $wrapped->write();
134
135
                        $wrapped->convertToPlaceholder();
136
                        if ($wrapped->hasMethod('onAfterCloudPut')) {
137
                            $wrapped->onAfterCloudPut();
138
                        }
139
                    } catch (Exception $e) {
140
                        $wrapped->CloudStatus = 'Error';
141
                        $wrapped->write();
142
                        $cloud->getLogger()->error("CloudAssets: Failed bucket upload: " . $e->getMessage() . " for " . $wrapped->getFullPath());
143
                        // Fail silently for now. This will cause the local copy to be served.
144
                    }
145
                } elseif ($wrapped->CloudStatus !== 'Live' && $wrapped->containsPlaceholder()) {
146
                    // If this is a duplicate file, update the status
147
                    // This shouldn't happen ever and won't happen often but when it does this will be helpful
148
                    $dup = File::get()->filter(array(
149
                        'Filename'      => $wrapped->Filename,
150
                        'CloudStatus'   => 'Live',
151
                    ))->first();
152
153
                    if ($dup && $dup->exists()) {
154
                        $cloud->getLogger()->warn("CloudAssets: fixing status for duplicate file: {$wrapped->ID} and {$dup->ID}");
155
                        $wrapped->CloudStatus   = $dup->CloudStatus;
156
                        $wrapped->CloudSize     = $dup->CloudSize;
157
                        $wrapped->CloudMetaJson = $dup->CloudMetaJson;
158
                        $wrapped->write();
159
                    }
160
                }
161
162
                $this->inUpdate = false;
163
                return $wrapped;
164
            }
165
        }
166
167
        $this->inUpdate = false;
168
        return $this->owner;
169
    }
170
171
172
    /**
173
     * @return bool
174
     */
175
    public function canBeInCloud()
176
    {
177
        if ($this->owner instanceof Folder) {
178
            return false;
179
        }
180
        if (!file_exists($this->owner->getFullPath())) {
181
            return false;
182
        }
183
        return true;
184
    }
185
186
187
    /**
188
     * @return bool
189
     */
190
    public function containsPlaceholder()
191
    {
192
        $placeholder = Config::inst()->get('CloudAssets', 'file_placeholder');
193
        $path = $this->owner->getFullPath();
194
195
        // check the size first to avoid reading crazy huge files into memory
196
        return (file_exists($path) && filesize($path) == strlen($placeholder) && file_get_contents($path) == $placeholder);
197
    }
198
199
200
    /**
201
     * Wipes out the contents of this file and replaces with placeholder text
202
     */
203
    public function convertToPlaceholder()
204
    {
205
        $bucket = $this->getCloudBucket();
206
        if ($bucket && !$bucket->isLocalCopyEnabled()) {
207
            $path = $this->owner->getFullPath();
208
            CloudAssets::inst()->getLogger()->debug("CloudAssets: converting $path to placeholder");
209
            Filesystem::makeFolder(dirname($path));
210
            file_put_contents($path, Config::inst()->get('CloudAssets', 'file_placeholder'));
211
        }
212
213
        return $this->owner;
214
    }
215
216
217
    /**
218
     * @return CloudBucket
219
     */
220
    public function getCloudBucket()
221
    {
222
        return CloudAssets::inst()->map($this->owner);
223
    }
224
225
226
    /**
227
     * @param int $linkType [optional] - see CloudBucket::LINK_XXX constants
228
     * @return string
229
     */
230
    public function getCloudURL($linkType = CloudBucket::LINK_SMART)
231
    {
232
        $bucket = $this->getCloudBucket();
233
        return $bucket ? $bucket->getLinkFor($this->owner, $linkType) : '';
234
    }
235
236
237
    /**
238
     * @param string $key [optional] - if not present returns the whole array
239
     * @return array
240
     */
241
    public function getCloudMeta($key = null)
242
    {
243
        $data = json_decode($this->owner->CloudMetaJson, true);
244
        if (empty($data) || !is_array($data)) {
245
            $data = array();
246
        }
247
248
        if (!empty($key)) {
249
            return isset($data[$key]) ? $data[$key] : null;
250
        } else {
251
            return $data;
252
        }
253
    }
254
255
256
    /**
257
     * @param string|array $key - passing an array as the first argument replaces the meta data entirely
258
     * @param mixed        $val
259
     * @return File - chainable
260
     */
261
    public function setCloudMeta($key, $val = null)
262
    {
263
        if (is_array($key)) {
264
            $data = $key;
265
        } else {
266
            $data = $this->getCloudMeta();
267
            $data[$key] = $val;
268
        }
269
270
        $this->owner->CloudMetaJson = json_encode($data);
271
        return $this->owner;
272
    }
273
274
275
    /**
276
     * If this file is stored in the cloud, downloads the cloud
277
     * copy and replaces whatever is local.
278
     */
279
    public function downloadFromCloud()
280
    {
281
        if ($this->owner->CloudStatus === 'Live') {
282
            $bucket   = $this->owner->getCloudBucket();
283
            if ($bucket) {
284
                $contents = $bucket->getContents($this->owner);
285
                $path     = $this->owner->getFullPath();
286
                Filesystem::makeFolder(dirname($path));
287
                CloudAssets::inst()->getLogger()->debug("CloudAssets: downloading $path from cloud (size=".strlen($contents).")");
288
                // if there was an error and we overwrote the local file with empty or null, it could delete the remote
289
                // file as well. Better to err on the side of not writing locally when we should than that.
290
                if (!empty($contents)) {
291
                    file_put_contents($path, $contents);
292
                }
293
            }
294
        }
295
    }
296
297
298
    /**
299
     * If the file is present in the database and the cloud but not
300
     * locally, create a placeholder for it. This can happen in a lot
301
     * of cases such as load balanced servers and local development.
302
     */
303
    public function createLocalIfNeeded()
304
    {
305
        if ($this->owner->CloudStatus === 'Live') {
306
            $bucket = $this->getCloudBucket();
307
            if ($bucket && $bucket->isLocalCopyEnabled()) {
308
                if (!file_exists($this->owner->getFullPath()) || $this->containsPlaceholder()) {
309
                    try {
310
                        $this->downloadFromCloud();
311
                    } catch (Exception $e) {
312
                        // I'm not sure what the correct behaviour is here
313
                        // Pretty sure it'd be better to have a broken image
314
                        // link than a 500 error though.
315
                        CloudAssets::inst()->getLogger()->error("CloudAssets: Failed bucket download: " . $e->getMessage() . " for " . $this->owner->getFullPath());
316
                    }
317
                }
318
            } else {
319
                if (!file_exists($this->owner->getFullPath())) {
320
                    $this->convertToPlaceholder();
321
                }
322
            }
323
        }
324
    }
325
326
327
    /**
328
     * @return bool
329
     */
330
    public function isCloudPutNeeded()
331
    {
332
        // we never want to upload the placeholder
333
        if ($this->containsPlaceholder()) {
334
            return false;
335
        }
336
337
        // we never want to upload an empty file
338
        $path = $this->owner->getFullPath();
339
        if (!file_exists($path)) {
340
            return false;
341
        }
342
343
        // we always want to upload if it's the first time
344
        $lastPut = $this->getCloudMeta('LastPut');
345
        if (!$lastPut) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lastPut of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
346
            return true;
347
        }
348
349
        // additionally, we want to upload if the file has been changed or replaced
350
        $mtime = filemtime($path);
351
        if ($mtime > $lastPut) {
352
            return true;
353
        }
354
355
        return false;
356
    }
357
358
359
    /**
360
     * Returns true if the local file is not available
361
     * @return bool
362
     */
363
    public function isLocalMissing()
364
    {
365
        return !file_exists($this->owner->getFullPath()) || $this->containsPlaceholder();
366
    }
367
}
368