Completed
Push — master ( afaf42...8ac772 )
by Raffael
20:10 queued 16:21
created

Smb::readonly()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 10
cp 0
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Storage\Adapter;
13
14
use Balloon\Filesystem\Node\Collection;
15
use Balloon\Filesystem\Node\File;
16
use Balloon\Filesystem\Node\NodeInterface;
17
use Balloon\Filesystem\Storage\Exception;
18
use Balloon\Server\User;
19
use Icewind\SMB\Exception as SMBException;
20
use Icewind\SMB\IFileInfo;
21
use Icewind\SMB\IShare;
22
use InvalidArgumentException;
23
use MongoDB\BSON\ObjectId;
24
use Psr\Log\LoggerInterface;
25
26
class Smb implements AdapterInterface
27
{
28
    /**
29
     * Options.
30
     */
31
    public const OPTION_SYSTEM_FOLDER = 'system_folder';
32
    public const OPTION_ROOT = 'root';
33
34
    /**
35
     * System folders.
36
     */
37
    protected const SYSTEM_TRASH = 'trash';
38
    protected const SYSTEM_TEMP = 'temp';
39
40
    /**
41
     * SMB share.
42
     *
43
     * @var IShare
44
     */
45
    protected $share;
46
47
    /**
48
     * Logger.
49
     *
50
     * @var LoggerInterface
51
     */
52
    protected $logger;
53
54
    /**
55
     * SMB Root directory within share.
56
     *
57
     * @var string
58
     */
59
    protected $root = '';
60
61
    /**
62
     * Balloon system folder.
63
     *
64
     * @var string
65
     */
66
    protected $system_folder = '.balloon';
67
68
    /**
69
     * SMB storage.
70
     */
71
    public function __construct(IShare $share, LoggerInterface $logger, array $config = [])
72
    {
73
        $this->share = $share;
74
        $this->logger = $logger;
75
        $this->setOptions($config);
76
    }
77
78
    /**
79
     * Get SMB share.
80
     */
81
    public function getShare(): IShare
82
    {
83
        return $this->share;
84
    }
85
86
    /**
87
     * Get root path.
88
     */
89
    public function getRoot(): string
90
    {
91
        return $this->root;
92
    }
93
94
    /**
95
     * Get system folder.
96
     */
97
    public function getSystemFolder(): string
98
    {
99
        return $this->system_folder;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function hasNode(NodeInterface $node): bool
106
    {
107
        $attributes = $node->getAttributes();
108
109
        try {
110
            if (isset($attributes['storage']['path'])) {
111
                return (bool) $this->share->stat($attributes['storage']['path']);
112
            }
113
        } catch (SMBException\NotFoundException $e) {
0 ignored issues
show
Bug introduced by
The class Icewind\SMB\Exception\NotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
114
            return false;
115
        }
116
117
        return false;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function deleteFile(File $file, ?int $version = null): ?array
124
    {
125
        return $this->deleteNode($file);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function readonly(NodeInterface $node, bool $readonly = true): ?array
132
    {
133
        $path = $this->getPath($node);
134
135
        if ($readonly === true) {
136
            $this->share->setMode($path, IFileInfo::MODE_READONLY);
137
138
            return $node->getAttributes()['storage'];
139
        }
140
141
        $this->share->setMode($path, IFileInfo::MODE_NORMAL);
142
143
        return $node->getAttributes()['storage'];
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function forceDeleteFile(File $file, ?int $version = null): bool
150
    {
151
        if (false === $this->hasNode($file)) {
152
            $this->logger->debug('smb blob for file ['.$file->getId().'] was not found', [
153
                'category' => get_class($this),
154
            ]);
155
156
            return false;
157
        }
158
159
        return $this->share->del($this->getPath($file));
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function openReadStream(File $file)
166
    {
167
        return $this->share->read($this->getPath($file));
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function storeFile(File $file, ObjectId $session): array
174
    {
175
        $path = $this->getSystemPath(self::SYSTEM_TEMP).DIRECTORY_SEPARATOR.$session;
176
177
        $current = $file->getPath();
178
        $mount = $file->getFilesystem()->findNodeById($file->getMount())->getPath();
179
        $dest = substr($current, strlen($mount));
180
181
        $this->logger->debug('copy file from session ['.$session.'] in ['.$path.'] to ['.$dest.']', [
182
            'category' => get_class($this),
183
        ]);
184
185
        $hash = hash_init('md5');
186
        $size = 0;
187
        $stream = $this->share->read($path);
188
189
        while (!feof($stream)) {
190
            $buffer = fgets($stream, 65536);
191
192
            if ($buffer === false) {
193
                continue;
194
            }
195
196
            $size += mb_strlen($buffer, '8bit');
197
            hash_update($hash, $buffer);
198
        }
199
200
        fclose($stream);
201
        $md5 = hash_final($hash);
202
203
        $this->logger->debug('calculated hash ['.$md5.'] for temporary file ['.$session.']', [
204
            'category' => get_class($this),
205
        ]);
206
207
        $this->share->rename($path, $dest);
208
209
        return [
210
            'reference' => [
211
                'path' => $dest,
212
            ],
213
            'size' => $size,
214
            'hash' => $md5,
215
        ];
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function createCollection(Collection $parent, string $name): array
222
    {
223
        $path = $this->getPath($parent).DIRECTORY_SEPARATOR.$name;
224
        $this->share->mkdir($path);
225
226
        return [
227
            'path' => $path,
228
        ];
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234
    public function deleteCollection(Collection $collection): ?array
235
    {
236
        return $this->deleteNode($collection);
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242
    public function forceDeleteCollection(Collection $collection): bool
243
    {
244
        if (false === $this->hasNode($collection)) {
245
            $this->logger->debug('smb collection ['.$collection->getId().'] was not found', [
246
                'category' => get_class($this),
247
            ]);
248
249
            return false;
250
        }
251
252
        return $this->share->rmdir($this->getPath($collection));
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function rename(NodeInterface $node, string $new_name): ?array
259
    {
260
        $reference = $node->getAttributes()['storage'];
261
262
        if (!$node->isDeleted()) {
263
            $path = join('/', [rtrim(dirname($this->getPath($node)), '/'), $new_name]);
264
            $this->share->rename($this->getPath($node), $path);
265
            $reference['path'] = $path;
266
        }
267
268
        return $reference;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function move(NodeInterface $node, Collection $parent): ?array
275
    {
276
        $reference = $node->getAttributes()['storage'];
277
278
        if (!$node->isDeleted()) {
279
            $path = join('/', [rtrim($this->getPath($parent), '/'), $node->getName()]);
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
280
            $this->share->rename($this->getPath($node), $path);
281
            $reference['path'] = $path;
282
        }
283
284
        return $reference;
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290
    public function undelete(NodeInterface $node): ?array
291
    {
292
        if (false === $this->hasNode($node)) {
293
            $this->logger->debug('smb node ['.$this->getPath($node).'] was not found for reference=['.$node->getId().']', [
294
                'category' => get_class($this),
295
            ]);
296
297
            return $node->getAttributes()['storage'];
298
        }
299
300
        $current = $node->getPath();
301
        $mount = $node->getFilesystem()->findNodeById($node->getMount())->getPath();
302
        $restore = substr($current, strlen($mount));
303
304
        $this->share->rename($this->getPath($node), $restore);
305
        $reference = $node->getAttributes()['storage'];
306
        $reference['path'] = $restore;
307
308
        return $reference;
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function storeTemporaryFile($stream, User $user, ?ObjectId $session = null): ObjectId
315
    {
316
        $exists = $session;
0 ignored issues
show
Unused Code introduced by
$exists is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
317
318
        if ($session === null) {
319
            $session = new ObjectId();
320
321
            $this->logger->info('create new tempory storage file ['.$session.']', [
322
                'category' => get_class($this),
323
            ]);
324
            $path = $this->getSystemPath(self::SYSTEM_TEMP).DIRECTORY_SEPARATOR.$session;
325
        } else {
326
            $path = $this->getSystemPath(self::SYSTEM_TEMP).DIRECTORY_SEPARATOR.$session;
327
328
            try {
329
                $this->share->stat($path);
330
            } catch (SMBException\NotFoundException $e) {
0 ignored issues
show
Bug introduced by
The class Icewind\SMB\Exception\NotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
331
                throw new Exception\SessionNotFound('temporary storage for this file is gone');
332
            }
333
        }
334
335
        $this->storeStream($stream, $path);
336
337
        return $session;
338
    }
339
340
    /**
341
     * Test connection to storage.
342
     */
343
    public function test(): bool
344
    {
345
        $test = $this->share->notify($this->root);
346
        $changes = $test->getChanges();
0 ignored issues
show
Unused Code introduced by
$changes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
347
        $test->stop();
348
349
        return true;
350
    }
351
352
    /**
353
     * Set options.
354
     */
355
    protected function setOptions(array $config = []): self
356
    {
357
        foreach ($config as $option => $value) {
358
            switch ($option) {
359
                case self::OPTION_SYSTEM_FOLDER:
360
                case self::OPTION_ROOT:
361
                    $this->{$option} = (string) $value;
362
363
                break;
364
                default:
365
                    throw new InvalidArgumentException('unknown option '.$option.' given');
366
            }
367
        }
368
369
        return $this;
370
    }
371
372
    /**
373
     * Create system folder if not exists.
374
     */
375
    protected function getSystemPath(string $name): string
376
    {
377
        $path = $this->root.DIRECTORY_SEPARATOR.$this->system_folder;
378
379
        try {
380
            $this->share->stat($path);
381
        } catch (SMBException\NotFoundException $e) {
0 ignored issues
show
Bug introduced by
The class Icewind\SMB\Exception\NotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
382
            $this->logger->debug('create smb system folder ['.$path.']', [
383
                'category' => get_class($this),
384
            ]);
385
386
            $this->share->mkdir($path);
387
            $this->share->setMode($path, IFileInfo::MODE_HIDDEN);
388
        }
389
390
        $path .= DIRECTORY_SEPARATOR.$name;
391
392
        try {
393
            $this->share->stat($path);
394
        } catch (SMBException\NotFoundException $e) {
0 ignored issues
show
Bug introduced by
The class Icewind\SMB\Exception\NotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
395
            $this->logger->debug('create smb system folder ['.$path.']', [
396
                'category' => get_class($this),
397
            ]);
398
399
            $this->share->mkdir($path);
400
        }
401
402
        return $path;
403
    }
404
405
    /**
406
     * Move node to trash folder.
407
     */
408
    protected function deleteNode(NodeInterface $node): array
409
    {
410
        $reference = $node->getAttributes()['storage'];
411
412
        if ($this->hasNode($node) === false) {
413
            $this->logger->debug('smb node ['.$this->getPath($node).'] was not found for reference=['.$node->getId().']', [
414
                'category' => get_class($this),
415
            ]);
416
417
            return $reference;
418
        }
419
420
        $path = $this->getSystemPath(self::SYSTEM_TRASH).DIRECTORY_SEPARATOR.$node->getId();
421
        $this->share->rename($this->getPath($node), $path);
422
        $reference['path'] = $path;
423
424
        return $reference;
425
    }
426
427
    /**
428
     * Get SMB path from node.
429
     */
430
    protected function getPath(NodeInterface $node): string
431
    {
432
        $attributes = $node->getAttributes();
433
434
        if (isset($attributes['mount']) && count($attributes['mount']) !== 0) {
435
            return $this->root;
436
        }
437
438
        if (!isset($attributes['storage']['path'])) {
439
            throw new Exception\BlobNotFound('no storage.path given for smb storage definiton');
440
        }
441
442
        return $attributes['storage']['path'];
443
    }
444
445
    /**
446
     * Store stream content.
447
     */
448
    protected function storeStream($stream, string $path): int
449
    {
450
        if ($stream === null) {
451
            $dest = $this->share->write($path);
0 ignored issues
show
Unused Code introduced by
$dest is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
452
453
            return 0;
454
        }
455
456
        $dest = $this->share->append($path);
457
        $bytes = stream_copy_to_stream($stream, $dest);
458
        fclose($dest);
459
460
        return $bytes;
461
    }
462
}
463