1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc. |
4
|
|
|
* |
5
|
|
|
* @author Arthur Schiwon <[email protected]> |
6
|
|
|
* @author Christoph Wurst <[email protected]> |
7
|
|
|
* @author Jesús Macias <[email protected]> |
8
|
|
|
* @author Jörn Friedrich Dreyer <[email protected]> |
9
|
|
|
* @author Juan Pablo Villafañez <[email protected]> |
10
|
|
|
* @author Juan Pablo Villafáñez <[email protected]> |
11
|
|
|
* @author Julius Härtl <[email protected]> |
12
|
|
|
* @author Michael Gapczynski <[email protected]> |
13
|
|
|
* @author Morris Jobke <[email protected]> |
14
|
|
|
* @author Philipp Kapfer <[email protected]> |
15
|
|
|
* @author Robin Appelman <[email protected]> |
16
|
|
|
* @author Robin McCorkell <[email protected]> |
17
|
|
|
* @author Roeland Jago Douma <[email protected]> |
18
|
|
|
* @author Roland Tapken <[email protected]> |
19
|
|
|
* @author Thomas Müller <[email protected]> |
20
|
|
|
* @author Vincent Petry <[email protected]> |
21
|
|
|
* |
22
|
|
|
* @license AGPL-3.0 |
23
|
|
|
* |
24
|
|
|
* This code is free software: you can redistribute it and/or modify |
25
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
26
|
|
|
* as published by the Free Software Foundation. |
27
|
|
|
* |
28
|
|
|
* This program is distributed in the hope that it will be useful, |
29
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
30
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
31
|
|
|
* GNU Affero General Public License for more details. |
32
|
|
|
* |
33
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
34
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
35
|
|
|
* |
36
|
|
|
*/ |
37
|
|
|
namespace OCA\Files_External\Lib\Storage; |
38
|
|
|
|
39
|
|
|
use Icewind\SMB\ACL; |
40
|
|
|
use Icewind\SMB\BasicAuth; |
41
|
|
|
use Icewind\SMB\Exception\AlreadyExistsException; |
42
|
|
|
use Icewind\SMB\Exception\ConnectException; |
43
|
|
|
use Icewind\SMB\Exception\Exception; |
44
|
|
|
use Icewind\SMB\Exception\ForbiddenException; |
45
|
|
|
use Icewind\SMB\Exception\InvalidArgumentException; |
46
|
|
|
use Icewind\SMB\Exception\InvalidTypeException; |
47
|
|
|
use Icewind\SMB\Exception\NotFoundException; |
48
|
|
|
use Icewind\SMB\Exception\OutOfSpaceException; |
49
|
|
|
use Icewind\SMB\Exception\TimedOutException; |
50
|
|
|
use Icewind\SMB\IFileInfo; |
51
|
|
|
use Icewind\SMB\Native\NativeServer; |
52
|
|
|
use Icewind\SMB\Options; |
53
|
|
|
use Icewind\SMB\ServerFactory; |
54
|
|
|
use Icewind\SMB\System; |
55
|
|
|
use Icewind\Streams\CallbackWrapper; |
56
|
|
|
use Icewind\Streams\IteratorDirectory; |
57
|
|
|
use OCP\Cache\CappedMemoryCache; |
58
|
|
|
use OC\Files\Filesystem; |
59
|
|
|
use OC\Files\Storage\Common; |
60
|
|
|
use OCA\Files_External\Lib\Notify\SMBNotifyHandler; |
61
|
|
|
use OCP\Constants; |
62
|
|
|
use OCP\Files\EntityTooLargeException; |
63
|
|
|
use OCP\Files\Notify\IChange; |
64
|
|
|
use OCP\Files\Notify\IRenameChange; |
65
|
|
|
use OCP\Files\NotPermittedException; |
66
|
|
|
use OCP\Files\Storage\INotifyStorage; |
67
|
|
|
use OCP\Files\StorageAuthException; |
68
|
|
|
use OCP\Files\StorageNotAvailableException; |
69
|
|
|
use OCP\ILogger; |
70
|
|
|
|
71
|
|
|
class SMB extends Common implements INotifyStorage { |
72
|
|
|
/** |
73
|
|
|
* @var \Icewind\SMB\IServer |
74
|
|
|
*/ |
75
|
|
|
protected $server; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @var \Icewind\SMB\IShare |
79
|
|
|
*/ |
80
|
|
|
protected $share; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* @var string |
84
|
|
|
*/ |
85
|
|
|
protected $root; |
86
|
|
|
|
87
|
|
|
/** @var CappedMemoryCache<IFileInfo> */ |
88
|
|
|
protected CappedMemoryCache $statCache; |
89
|
|
|
|
90
|
|
|
/** @var ILogger */ |
91
|
|
|
protected $logger; |
92
|
|
|
|
93
|
|
|
/** @var bool */ |
94
|
|
|
protected $showHidden; |
95
|
|
|
|
96
|
|
|
/** @var bool */ |
97
|
|
|
protected $checkAcl; |
98
|
|
|
|
99
|
|
|
public function __construct($params) { |
100
|
|
|
if (!isset($params['host'])) { |
101
|
|
|
throw new \Exception('Invalid configuration, no host provided'); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
if (isset($params['auth'])) { |
105
|
|
|
$auth = $params['auth']; |
106
|
|
|
} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) { |
107
|
|
|
[$workgroup, $user] = $this->splitUser($params['user']); |
108
|
|
|
$auth = new BasicAuth($user, $workgroup, $params['password']); |
109
|
|
|
} else { |
110
|
|
|
throw new \Exception('Invalid configuration, no credentials provided'); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
if (isset($params['logger'])) { |
114
|
|
|
$this->logger = $params['logger']; |
115
|
|
|
} else { |
116
|
|
|
$this->logger = \OC::$server->getLogger(); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
$options = new Options(); |
120
|
|
|
if (isset($params['timeout'])) { |
121
|
|
|
$timeout = (int)$params['timeout']; |
122
|
|
|
if ($timeout > 0) { |
123
|
|
|
$options->setTimeout($timeout); |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
$serverFactory = new ServerFactory($options); |
127
|
|
|
$this->server = $serverFactory->createServer($params['host'], $auth); |
128
|
|
|
$this->share = $this->server->getShare(trim($params['share'], '/')); |
129
|
|
|
|
130
|
|
|
$this->root = $params['root'] ?? '/'; |
131
|
|
|
$this->root = '/' . ltrim($this->root, '/'); |
132
|
|
|
$this->root = rtrim($this->root, '/') . '/'; |
133
|
|
|
|
134
|
|
|
$this->showHidden = isset($params['show_hidden']) && $params['show_hidden']; |
135
|
|
|
$this->checkAcl = isset($params['check_acl']) && $params['check_acl']; |
136
|
|
|
|
137
|
|
|
$this->statCache = new CappedMemoryCache(); |
138
|
|
|
parent::__construct($params); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
private function splitUser($user) { |
142
|
|
|
if (str_contains($user, '/')) { |
143
|
|
|
return explode('/', $user, 2); |
144
|
|
|
} elseif (str_contains($user, '\\')) { |
145
|
|
|
return explode('\\', $user); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
return [null, $user]; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @return string |
153
|
|
|
*/ |
154
|
|
|
public function getId() { |
155
|
|
|
// FIXME: double slash to keep compatible with the old storage ids, |
156
|
|
|
// failure to do so will lead to creation of a new storage id and |
157
|
|
|
// loss of shares from the storage |
158
|
|
|
return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @param string $path |
163
|
|
|
* @return string |
164
|
|
|
*/ |
165
|
|
|
protected function buildPath($path) { |
166
|
|
|
return Filesystem::normalizePath($this->root . '/' . $path, true, false, true); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
protected function relativePath($fullPath) { |
170
|
|
|
if ($fullPath === $this->root) { |
171
|
|
|
return ''; |
172
|
|
|
} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) { |
173
|
|
|
return substr($fullPath, strlen($this->root)); |
174
|
|
|
} else { |
175
|
|
|
return null; |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @param string $path |
181
|
|
|
* @return IFileInfo |
182
|
|
|
* @throws StorageAuthException |
183
|
|
|
*/ |
184
|
|
|
protected function getFileInfo($path) { |
185
|
|
|
try { |
186
|
|
|
$path = $this->buildPath($path); |
187
|
|
|
$cached = $this->statCache[$path] ?? null; |
188
|
|
|
if ($cached instanceof IFileInfo) { |
189
|
|
|
return $cached; |
190
|
|
|
} else { |
191
|
|
|
$stat = $this->share->stat($path); |
192
|
|
|
$this->statCache[$path] = $stat; |
193
|
|
|
return $stat; |
194
|
|
|
} |
195
|
|
|
} catch (ConnectException $e) { |
196
|
|
|
$this->throwUnavailable($e); |
197
|
|
|
} catch (NotFoundException $e) { |
198
|
|
|
throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e); |
199
|
|
|
} catch (ForbiddenException $e) { |
200
|
|
|
// with php-smbclient, this exception is thrown when the provided password is invalid. |
201
|
|
|
// Possible is also ForbiddenException with a different error code, so we check it. |
202
|
|
|
if ($e->getCode() === 1) { |
203
|
|
|
$this->throwUnavailable($e); |
204
|
|
|
} |
205
|
|
|
throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @param \Exception $e |
211
|
|
|
* @return never |
212
|
|
|
* @throws StorageAuthException |
213
|
|
|
*/ |
214
|
|
|
protected function throwUnavailable(\Exception $e) { |
215
|
|
|
$this->logger->logException($e, ['message' => 'Error while getting file info']); |
216
|
|
|
throw new StorageAuthException($e->getMessage(), $e); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* get the acl from fileinfo that is relevant for the configured user |
221
|
|
|
* |
222
|
|
|
* @param IFileInfo $file |
223
|
|
|
* @return ACL|null |
224
|
|
|
*/ |
225
|
|
|
private function getACL(IFileInfo $file): ?ACL { |
226
|
|
|
$acls = $file->getAcls(); |
227
|
|
|
foreach ($acls as $user => $acl) { |
228
|
|
|
[, $user] = $this->splitUser($user); // strip domain |
229
|
|
|
if ($user === $this->server->getAuth()->getUsername()) { |
230
|
|
|
return $acl; |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return null; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @param string $path |
239
|
|
|
* @return \Generator<IFileInfo> |
240
|
|
|
* @throws StorageNotAvailableException |
241
|
|
|
*/ |
242
|
|
|
protected function getFolderContents($path): iterable { |
243
|
|
|
try { |
244
|
|
|
$path = ltrim($this->buildPath($path), '/'); |
245
|
|
|
try { |
246
|
|
|
$files = $this->share->dir($path); |
247
|
|
|
} catch (ForbiddenException $e) { |
248
|
|
|
$this->logger->critical($e->getMessage(), ['exception' => $e]); |
249
|
|
|
throw new NotPermittedException(); |
250
|
|
|
} catch (InvalidTypeException $e) { |
251
|
|
|
return; |
252
|
|
|
} |
253
|
|
|
foreach ($files as $file) { |
254
|
|
|
$this->statCache[$path . '/' . $file->getName()] = $file; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
foreach ($files as $file) { |
258
|
|
|
try { |
259
|
|
|
// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch |
260
|
|
|
// so we trigger the below exceptions where applicable |
261
|
|
|
$hide = $file->isHidden() && !$this->showHidden; |
262
|
|
|
|
263
|
|
|
if ($this->checkAcl && $acl = $this->getACL($file)) { |
264
|
|
|
// if there is no explicit deny, we assume it's allowed |
265
|
|
|
// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder |
266
|
|
|
// additionally, it's better to have false negatives here then false positives |
267
|
|
|
if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) { |
268
|
|
|
$this->logger->debug('Hiding non readable entry ' . $file->getName()); |
269
|
|
|
continue; |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
if ($hide) { |
274
|
|
|
$this->logger->debug('hiding hidden file ' . $file->getName()); |
275
|
|
|
} |
276
|
|
|
if (!$hide) { |
277
|
|
|
yield $file; |
278
|
|
|
} |
279
|
|
|
} catch (ForbiddenException $e) { |
280
|
|
|
$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]); |
|
|
|
|
281
|
|
|
} catch (NotFoundException $e) { |
282
|
|
|
$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]); |
|
|
|
|
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
} catch (ConnectException $e) { |
286
|
|
|
$this->logger->logException($e, ['message' => 'Error while getting folder content']); |
287
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
288
|
|
|
} catch (NotFoundException $e) { |
289
|
|
|
throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e); |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* @param IFileInfo $info |
295
|
|
|
* @return array |
296
|
|
|
*/ |
297
|
|
|
protected function formatInfo($info) { |
298
|
|
|
$result = [ |
299
|
|
|
'size' => $info->getSize(), |
300
|
|
|
'mtime' => $info->getMTime(), |
301
|
|
|
]; |
302
|
|
|
if ($info->isDirectory()) { |
303
|
|
|
$result['type'] = 'dir'; |
304
|
|
|
} else { |
305
|
|
|
$result['type'] = 'file'; |
306
|
|
|
} |
307
|
|
|
return $result; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Rename the files. If the source or the target is the root, the rename won't happen. |
312
|
|
|
* |
313
|
|
|
* @param string $source the old name of the path |
314
|
|
|
* @param string $target the new name of the path |
315
|
|
|
* @return bool true if the rename is successful, false otherwise |
316
|
|
|
*/ |
317
|
|
|
public function rename($source, $target, $retry = true): bool { |
318
|
|
|
if ($this->isRootDir($source) || $this->isRootDir($target)) { |
319
|
|
|
return false; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
$absoluteSource = $this->buildPath($source); |
323
|
|
|
$absoluteTarget = $this->buildPath($target); |
324
|
|
|
try { |
325
|
|
|
$result = $this->share->rename($absoluteSource, $absoluteTarget); |
326
|
|
|
} catch (AlreadyExistsException $e) { |
327
|
|
|
if ($retry) { |
328
|
|
|
$this->remove($target); |
329
|
|
|
$result = $this->share->rename($absoluteSource, $absoluteTarget); |
330
|
|
|
} else { |
331
|
|
|
$this->logger->logException($e, ['level' => ILogger::WARN]); |
|
|
|
|
332
|
|
|
return false; |
333
|
|
|
} |
334
|
|
|
} catch (InvalidArgumentException $e) { |
335
|
|
|
if ($retry) { |
336
|
|
|
$this->remove($target); |
337
|
|
|
$result = $this->share->rename($absoluteSource, $absoluteTarget); |
338
|
|
|
} else { |
339
|
|
|
$this->logger->logException($e, ['level' => ILogger::WARN]); |
|
|
|
|
340
|
|
|
return false; |
341
|
|
|
} |
342
|
|
|
} catch (\Exception $e) { |
343
|
|
|
$this->logger->logException($e, ['level' => ILogger::WARN]); |
|
|
|
|
344
|
|
|
return false; |
345
|
|
|
} |
346
|
|
|
unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]); |
347
|
|
|
return $result; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
public function stat($path, $retry = true) { |
351
|
|
|
try { |
352
|
|
|
$result = $this->formatInfo($this->getFileInfo($path)); |
353
|
|
|
} catch (ForbiddenException $e) { |
354
|
|
|
return false; |
355
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
356
|
|
|
return false; |
357
|
|
|
} catch (TimedOutException $e) { |
358
|
|
|
if ($retry) { |
359
|
|
|
return $this->stat($path, false); |
360
|
|
|
} else { |
361
|
|
|
throw $e; |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
if ($this->remoteIsShare() && $this->isRootDir($path)) { |
365
|
|
|
$result['mtime'] = $this->shareMTime(); |
366
|
|
|
} |
367
|
|
|
return $result; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* get the best guess for the modification time of the share |
372
|
|
|
* |
373
|
|
|
* @return int |
374
|
|
|
*/ |
375
|
|
|
private function shareMTime() { |
376
|
|
|
$highestMTime = 0; |
377
|
|
|
$files = $this->share->dir($this->root); |
378
|
|
|
foreach ($files as $fileInfo) { |
379
|
|
|
try { |
380
|
|
|
if ($fileInfo->getMTime() > $highestMTime) { |
381
|
|
|
$highestMTime = $fileInfo->getMTime(); |
382
|
|
|
} |
383
|
|
|
} catch (NotFoundException $e) { |
384
|
|
|
// Ignore this, can happen on unavailable DFS shares |
385
|
|
|
} catch (ForbiddenException $e) { |
386
|
|
|
// Ignore this too - it's a symlink |
387
|
|
|
} |
388
|
|
|
} |
389
|
|
|
return $highestMTime; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Check if the path is our root dir (not the smb one) |
394
|
|
|
* |
395
|
|
|
* @param string $path the path |
396
|
|
|
* @return bool |
397
|
|
|
*/ |
398
|
|
|
private function isRootDir($path) { |
399
|
|
|
return $path === '' || $path === '/' || $path === '.'; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* Check if our root points to a smb share |
404
|
|
|
* |
405
|
|
|
* @return bool true if our root points to a share false otherwise |
406
|
|
|
*/ |
407
|
|
|
private function remoteIsShare() { |
408
|
|
|
return $this->share->getName() && (!$this->root || $this->root === '/'); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* @param string $path |
413
|
|
|
* @return bool |
414
|
|
|
*/ |
415
|
|
|
public function unlink($path) { |
416
|
|
|
if ($this->isRootDir($path)) { |
417
|
|
|
return false; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
try { |
421
|
|
|
if ($this->is_dir($path)) { |
422
|
|
|
return $this->rmdir($path); |
423
|
|
|
} else { |
424
|
|
|
$path = $this->buildPath($path); |
425
|
|
|
unset($this->statCache[$path]); |
426
|
|
|
$this->share->del($path); |
427
|
|
|
return true; |
428
|
|
|
} |
429
|
|
|
} catch (NotFoundException $e) { |
430
|
|
|
return false; |
431
|
|
|
} catch (ForbiddenException $e) { |
432
|
|
|
return false; |
433
|
|
|
} catch (ConnectException $e) { |
434
|
|
|
$this->logger->logException($e, ['message' => 'Error while deleting file']); |
435
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
436
|
|
|
} |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
/** |
440
|
|
|
* check if a file or folder has been updated since $time |
441
|
|
|
* |
442
|
|
|
* @param string $path |
443
|
|
|
* @param int $time |
444
|
|
|
* @return bool |
445
|
|
|
*/ |
446
|
|
|
public function hasUpdated($path, $time) { |
447
|
|
|
if (!$path and $this->root === '/') { |
448
|
|
|
// mtime doesn't work for shares, but giving the nature of the backend, |
449
|
|
|
// doing a full update is still just fast enough |
450
|
|
|
return true; |
451
|
|
|
} else { |
452
|
|
|
$actualTime = $this->filemtime($path); |
453
|
|
|
return $actualTime > $time; |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* @param string $path |
459
|
|
|
* @param string $mode |
460
|
|
|
* @return resource|bool |
461
|
|
|
*/ |
462
|
|
|
public function fopen($path, $mode) { |
463
|
|
|
$fullPath = $this->buildPath($path); |
464
|
|
|
try { |
465
|
|
|
switch ($mode) { |
466
|
|
|
case 'r': |
467
|
|
|
case 'rb': |
468
|
|
|
if (!$this->file_exists($path)) { |
469
|
|
|
return false; |
470
|
|
|
} |
471
|
|
|
return $this->share->read($fullPath); |
472
|
|
|
case 'w': |
473
|
|
|
case 'wb': |
474
|
|
|
$source = $this->share->write($fullPath); |
475
|
|
|
return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) { |
476
|
|
|
unset($this->statCache[$fullPath]); |
477
|
|
|
}); |
478
|
|
|
case 'a': |
479
|
|
|
case 'ab': |
480
|
|
|
case 'r+': |
481
|
|
|
case 'w+': |
482
|
|
|
case 'wb+': |
483
|
|
|
case 'a+': |
484
|
|
|
case 'x': |
485
|
|
|
case 'x+': |
486
|
|
|
case 'c': |
487
|
|
|
case 'c+': |
488
|
|
|
//emulate these |
489
|
|
|
if (strrpos($path, '.') !== false) { |
490
|
|
|
$ext = substr($path, strrpos($path, '.')); |
491
|
|
|
} else { |
492
|
|
|
$ext = ''; |
493
|
|
|
} |
494
|
|
|
if ($this->file_exists($path)) { |
495
|
|
|
if (!$this->isUpdatable($path)) { |
496
|
|
|
return false; |
497
|
|
|
} |
498
|
|
|
$tmpFile = $this->getCachedFile($path); |
499
|
|
|
} else { |
500
|
|
|
if (!$this->isCreatable(dirname($path))) { |
501
|
|
|
return false; |
502
|
|
|
} |
503
|
|
|
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext); |
504
|
|
|
} |
505
|
|
|
$source = fopen($tmpFile, $mode); |
506
|
|
|
$share = $this->share; |
507
|
|
|
return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) { |
508
|
|
|
unset($this->statCache[$fullPath]); |
509
|
|
|
$share->put($tmpFile, $fullPath); |
510
|
|
|
unlink($tmpFile); |
511
|
|
|
}); |
512
|
|
|
} |
513
|
|
|
return false; |
514
|
|
|
} catch (NotFoundException $e) { |
515
|
|
|
return false; |
516
|
|
|
} catch (ForbiddenException $e) { |
517
|
|
|
return false; |
518
|
|
|
} catch (OutOfSpaceException $e) { |
519
|
|
|
throw new EntityTooLargeException("not enough available space to create file", 0, $e); |
520
|
|
|
} catch (ConnectException $e) { |
521
|
|
|
$this->logger->logException($e, ['message' => 'Error while opening file']); |
522
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
523
|
|
|
} |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
public function rmdir($path) { |
527
|
|
|
if ($this->isRootDir($path)) { |
528
|
|
|
return false; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
try { |
532
|
|
|
$this->statCache = new CappedMemoryCache(); |
533
|
|
|
$content = $this->share->dir($this->buildPath($path)); |
534
|
|
|
foreach ($content as $file) { |
535
|
|
|
if ($file->isDirectory()) { |
536
|
|
|
$this->rmdir($path . '/' . $file->getName()); |
537
|
|
|
} else { |
538
|
|
|
$this->share->del($file->getPath()); |
539
|
|
|
} |
540
|
|
|
} |
541
|
|
|
$this->share->rmdir($this->buildPath($path)); |
542
|
|
|
return true; |
543
|
|
|
} catch (NotFoundException $e) { |
544
|
|
|
return false; |
545
|
|
|
} catch (ForbiddenException $e) { |
546
|
|
|
return false; |
547
|
|
|
} catch (ConnectException $e) { |
548
|
|
|
$this->logger->logException($e, ['message' => 'Error while removing folder']); |
549
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
550
|
|
|
} |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
public function touch($path, $mtime = null) { |
554
|
|
|
try { |
555
|
|
|
if (!$this->file_exists($path)) { |
556
|
|
|
$fh = $this->share->write($this->buildPath($path)); |
557
|
|
|
fclose($fh); |
558
|
|
|
return true; |
559
|
|
|
} |
560
|
|
|
return false; |
561
|
|
|
} catch (OutOfSpaceException $e) { |
562
|
|
|
throw new EntityTooLargeException("not enough available space to create file", 0, $e); |
563
|
|
|
} catch (ConnectException $e) { |
564
|
|
|
$this->logger->logException($e, ['message' => 'Error while creating file']); |
565
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
566
|
|
|
} |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
public function getMetaData($path) { |
570
|
|
|
try { |
571
|
|
|
$fileInfo = $this->getFileInfo($path); |
572
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
573
|
|
|
return null; |
574
|
|
|
} catch (ForbiddenException $e) { |
575
|
|
|
return null; |
576
|
|
|
} |
577
|
|
|
if (!$fileInfo) { |
|
|
|
|
578
|
|
|
return null; |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
return $this->getMetaDataFromFileInfo($fileInfo); |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
private function getMetaDataFromFileInfo(IFileInfo $fileInfo) { |
585
|
|
|
$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE; |
586
|
|
|
|
587
|
|
|
if ( |
588
|
|
|
!$fileInfo->isReadOnly() || $fileInfo->isDirectory() |
589
|
|
|
) { |
590
|
|
|
$permissions += Constants::PERMISSION_DELETE; |
591
|
|
|
$permissions += Constants::PERMISSION_UPDATE; |
592
|
|
|
if ($fileInfo->isDirectory()) { |
593
|
|
|
$permissions += Constants::PERMISSION_CREATE; |
594
|
|
|
} |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
$data = []; |
598
|
|
|
if ($fileInfo->isDirectory()) { |
599
|
|
|
$data['mimetype'] = 'httpd/unix-directory'; |
600
|
|
|
} else { |
601
|
|
|
$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath()); |
602
|
|
|
} |
603
|
|
|
$data['mtime'] = $fileInfo->getMTime(); |
604
|
|
|
if ($fileInfo->isDirectory()) { |
605
|
|
|
$data['size'] = -1; //unknown |
606
|
|
|
} else { |
607
|
|
|
$data['size'] = $fileInfo->getSize(); |
608
|
|
|
} |
609
|
|
|
$data['etag'] = $this->getETag($fileInfo->getPath()); |
610
|
|
|
$data['storage_mtime'] = $data['mtime']; |
611
|
|
|
$data['permissions'] = $permissions; |
612
|
|
|
$data['name'] = $fileInfo->getName(); |
613
|
|
|
|
614
|
|
|
return $data; |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
public function opendir($path) { |
618
|
|
|
try { |
619
|
|
|
$files = $this->getFolderContents($path); |
620
|
|
|
} catch (NotFoundException $e) { |
621
|
|
|
return false; |
622
|
|
|
} catch (NotPermittedException $e) { |
623
|
|
|
return false; |
624
|
|
|
} |
625
|
|
|
$names = array_map(function ($info) { |
626
|
|
|
/** @var IFileInfo $info */ |
627
|
|
|
return $info->getName(); |
628
|
|
|
}, iterator_to_array($files)); |
629
|
|
|
return IteratorDirectory::wrap($names); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
public function getDirectoryContent($directory): \Traversable { |
633
|
|
|
try { |
634
|
|
|
$files = $this->getFolderContents($directory); |
635
|
|
|
foreach ($files as $file) { |
636
|
|
|
yield $this->getMetaDataFromFileInfo($file); |
637
|
|
|
} |
638
|
|
|
} catch (NotFoundException $e) { |
639
|
|
|
return; |
640
|
|
|
} catch (NotPermittedException $e) { |
641
|
|
|
return; |
642
|
|
|
} |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
public function filetype($path) { |
646
|
|
|
try { |
647
|
|
|
return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file'; |
648
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
649
|
|
|
return false; |
650
|
|
|
} catch (ForbiddenException $e) { |
651
|
|
|
return false; |
652
|
|
|
} |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
public function mkdir($path) { |
656
|
|
|
$path = $this->buildPath($path); |
657
|
|
|
try { |
658
|
|
|
$this->share->mkdir($path); |
659
|
|
|
return true; |
660
|
|
|
} catch (ConnectException $e) { |
661
|
|
|
$this->logger->logException($e, ['message' => 'Error while creating folder']); |
662
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
663
|
|
|
} catch (Exception $e) { |
664
|
|
|
return false; |
665
|
|
|
} |
666
|
|
|
} |
667
|
|
|
|
668
|
|
|
public function file_exists($path) { |
669
|
|
|
try { |
670
|
|
|
$this->getFileInfo($path); |
671
|
|
|
return true; |
672
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
673
|
|
|
return false; |
674
|
|
|
} catch (ForbiddenException $e) { |
675
|
|
|
return false; |
676
|
|
|
} catch (ConnectException $e) { |
677
|
|
|
throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e); |
678
|
|
|
} |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
public function isReadable($path) { |
682
|
|
|
try { |
683
|
|
|
$info = $this->getFileInfo($path); |
684
|
|
|
return $this->showHidden || !$info->isHidden(); |
685
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
686
|
|
|
return false; |
687
|
|
|
} catch (ForbiddenException $e) { |
688
|
|
|
return false; |
689
|
|
|
} |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
public function isUpdatable($path) { |
693
|
|
|
try { |
694
|
|
|
$info = $this->getFileInfo($path); |
695
|
|
|
// following windows behaviour for read-only folders: they can be written into |
696
|
|
|
// (https://support.microsoft.com/en-us/kb/326549 - "cause" section) |
697
|
|
|
return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory()); |
698
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
699
|
|
|
return false; |
700
|
|
|
} catch (ForbiddenException $e) { |
701
|
|
|
return false; |
702
|
|
|
} |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
public function isDeletable($path) { |
706
|
|
|
try { |
707
|
|
|
$info = $this->getFileInfo($path); |
708
|
|
|
return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly(); |
709
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
710
|
|
|
return false; |
711
|
|
|
} catch (ForbiddenException $e) { |
712
|
|
|
return false; |
713
|
|
|
} |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
/** |
717
|
|
|
* check if smbclient is installed |
718
|
|
|
*/ |
719
|
|
|
public static function checkDependencies() { |
720
|
|
|
return ( |
721
|
|
|
(bool)\OC_Helper::findBinaryPath('smbclient') |
722
|
|
|
|| NativeServer::available(new System()) |
723
|
|
|
) ? true : ['smbclient']; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
/** |
727
|
|
|
* Test a storage for availability |
728
|
|
|
* |
729
|
|
|
* @return bool |
730
|
|
|
*/ |
731
|
|
|
public function test() { |
732
|
|
|
try { |
733
|
|
|
return parent::test(); |
734
|
|
|
} catch (StorageAuthException $e) { |
735
|
|
|
return false; |
736
|
|
|
} catch (ForbiddenException $e) { |
737
|
|
|
return false; |
738
|
|
|
} catch (Exception $e) { |
739
|
|
|
$this->logger->logException($e); |
740
|
|
|
return false; |
741
|
|
|
} |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
public function listen($path, callable $callback) { |
745
|
|
|
$this->notify($path)->listen(function (IChange $change) use ($callback) { |
746
|
|
|
if ($change instanceof IRenameChange) { |
747
|
|
|
return $callback($change->getType(), $change->getPath(), $change->getTargetPath()); |
748
|
|
|
} else { |
749
|
|
|
return $callback($change->getType(), $change->getPath()); |
750
|
|
|
} |
751
|
|
|
}); |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
public function notify($path) { |
755
|
|
|
$path = '/' . ltrim($path, '/'); |
756
|
|
|
$shareNotifyHandler = $this->share->notify($this->buildPath($path)); |
757
|
|
|
return new SMBNotifyHandler($shareNotifyHandler, $this->root); |
758
|
|
|
} |
759
|
|
|
} |
760
|
|
|
|
This class constant has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.