|
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) { |
|
|
|
|
|
|
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()]); |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
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
|
|
|
|
Scrutinizer analyzes your
composer.json/composer.lockfile 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.