1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @author Bart Visscher <[email protected]> |
4
|
|
|
* @author Björn Schießle <[email protected]> |
5
|
|
|
* @author Carlos Cerrillo <[email protected]> |
6
|
|
|
* @author Felix Moeller <[email protected]> |
7
|
|
|
* @author Jörn Friedrich Dreyer <[email protected]> |
8
|
|
|
* @author Lukas Reschke <[email protected]> |
9
|
|
|
* @author Michael Gapczynski <[email protected]> |
10
|
|
|
* @author Morris Jobke <[email protected]> |
11
|
|
|
* @author Philipp Kapfer <[email protected]> |
12
|
|
|
* @author Robin Appelman <[email protected]> |
13
|
|
|
* @author Thomas Müller <[email protected]> |
14
|
|
|
* @author Vincent Petry <[email protected]> |
15
|
|
|
* |
16
|
|
|
* @copyright Copyright (c) 2018, ownCloud GmbH |
17
|
|
|
* @license AGPL-3.0 |
18
|
|
|
* |
19
|
|
|
* This code is free software: you can redistribute it and/or modify |
20
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
21
|
|
|
* as published by the Free Software Foundation. |
22
|
|
|
* |
23
|
|
|
* This program is distributed in the hope that it will be useful, |
24
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
25
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
26
|
|
|
* GNU Affero General Public License for more details. |
27
|
|
|
* |
28
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
29
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
30
|
|
|
* |
31
|
|
|
*/ |
32
|
|
|
|
33
|
|
|
namespace OC\Files\Storage; |
34
|
|
|
|
35
|
|
|
use Exception; |
36
|
|
|
use GuzzleHttp\Exception\RequestException; |
37
|
|
|
use GuzzleHttp\Message\ResponseInterface; |
38
|
|
|
use function GuzzleHttp\Psr7\stream_for; |
39
|
|
|
use OC\Files\Filesystem; |
40
|
|
|
use OC\Files\Stream\Close; |
41
|
|
|
use Icewind\Streams\IteratorDirectory; |
42
|
|
|
use OC\Memcache\ArrayCache; |
43
|
|
|
use OCP\AppFramework\Http; |
44
|
|
|
use OCP\Constants; |
45
|
|
|
use OCP\Files\FileInfo; |
46
|
|
|
use OCP\Files\StorageInvalidException; |
47
|
|
|
use OCP\Files\StorageNotAvailableException; |
48
|
|
|
use OCP\Util; |
49
|
|
|
use Psr\Http\Message\StreamInterface; |
50
|
|
|
use Sabre\DAV\Xml\Property\ResourceType; |
51
|
|
|
use Sabre\HTTP\Client; |
52
|
|
|
use Sabre\HTTP\ClientHttpException; |
53
|
|
|
use Sabre\DAV\Exception\InsufficientStorage; |
54
|
|
|
use OCA\DAV\Connector\Sabre\Exception\Forbidden; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Class DAV |
58
|
|
|
* |
59
|
|
|
* @package OC\Files\Storage |
60
|
|
|
*/ |
61
|
|
|
class DAV extends Common { |
62
|
|
|
/** @var string */ |
63
|
|
|
protected $password; |
64
|
|
|
/** @var string */ |
65
|
|
|
protected $user; |
66
|
|
|
/** @var string */ |
67
|
|
|
protected $authType; |
68
|
|
|
/** @var string */ |
69
|
|
|
protected $host; |
70
|
|
|
/** @var bool */ |
71
|
|
|
protected $secure; |
72
|
|
|
/** @var string */ |
73
|
|
|
protected $root; |
74
|
|
|
/** @var string */ |
75
|
|
|
protected $certPath; |
76
|
|
|
/** @var bool */ |
77
|
|
|
protected $ready; |
78
|
|
|
/** @var Client */ |
79
|
|
|
private $client; |
80
|
|
|
/** @var ArrayCache */ |
81
|
|
|
private $statCache; |
82
|
|
|
/** @var array */ |
83
|
|
|
private static $tempFiles = []; |
84
|
|
|
/** @var \OCP\Http\Client\IClientService */ |
85
|
|
|
private $httpClientService; |
86
|
|
|
|
87
|
|
|
/** @var \OCP\Http\Client\IWebDavClientService */ |
88
|
|
|
private $webDavClientService; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* @param array $params |
92
|
|
|
* @throws \Exception |
93
|
|
|
*/ |
94
|
|
|
public function __construct($params) { |
95
|
|
|
$this->statCache = new ArrayCache(); |
96
|
|
|
$this->httpClientService = \OC::$server->getHTTPClientService(); |
97
|
|
|
$this->webDavClientService = \OC::$server->getWebDavClientService(); |
98
|
|
|
if (isset($params['host'], $params['user'], $params['password'])) { |
99
|
|
|
$host = $params['host']; |
100
|
|
|
//remove leading http[s], will be generated in createBaseUri() |
101
|
|
|
if (\substr($host, 0, 8) == "https://") { |
102
|
|
|
$host = \substr($host, 8); |
103
|
|
|
} elseif (\substr($host, 0, 7) == "http://") { |
104
|
|
|
$host = \substr($host, 7); |
105
|
|
|
} |
106
|
|
|
$this->host = $host; |
107
|
|
|
$this->user = $params['user']; |
108
|
|
|
$this->password = $params['password']; |
109
|
|
|
if (isset($params['authType'])) { |
110
|
|
|
$this->authType = $params['authType']; |
111
|
|
|
} |
112
|
|
|
if (isset($params['secure'])) { |
113
|
|
|
if (\is_string($params['secure'])) { |
114
|
|
|
$this->secure = ($params['secure'] === 'true'); |
115
|
|
|
} else { |
116
|
|
|
$this->secure = (bool)$params['secure']; |
117
|
|
|
} |
118
|
|
|
} else { |
119
|
|
|
$this->secure = false; |
120
|
|
|
} |
121
|
|
|
$this->root = isset($params['root']) ? $params['root'] : '/'; |
122
|
|
View Code Duplication |
if (!$this->root || $this->root[0] != '/') { |
|
|
|
|
123
|
|
|
$this->root = '/' . $this->root; |
124
|
|
|
} |
125
|
|
|
if (\substr($this->root, -1, 1) != '/') { |
126
|
|
|
$this->root .= '/'; |
127
|
|
|
} |
128
|
|
|
} else { |
129
|
|
|
throw new \InvalidArgumentException('Invalid webdav storage configuration'); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
protected function init() { |
134
|
|
|
if ($this->ready) { |
135
|
|
|
return; |
136
|
|
|
} |
137
|
|
|
$this->ready = true; |
138
|
|
|
|
139
|
|
|
$settings = [ |
140
|
|
|
'baseUri' => $this->createBaseUri(), |
141
|
|
|
'userName' => $this->user, |
142
|
|
|
'password' => $this->password |
143
|
|
|
]; |
144
|
|
|
if (isset($this->authType)) { |
145
|
|
|
$settings['authType'] = $this->authType; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
$this->client = $this->webDavClientService->newClient($settings); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Clear the stat cache |
153
|
|
|
*/ |
154
|
|
|
public function clearStatCache() { |
155
|
|
|
$this->statCache->clear(); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** {@inheritdoc} */ |
159
|
|
|
public function getId() { |
160
|
|
|
return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** {@inheritdoc} */ |
164
|
|
|
public function createBaseUri() { |
165
|
|
|
$baseUri = 'http'; |
166
|
|
|
if ($this->secure) { |
167
|
|
|
$baseUri .= 's'; |
168
|
|
|
} |
169
|
|
|
$baseUri .= '://' . $this->host . $this->root; |
170
|
|
|
return $baseUri; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** {@inheritdoc} */ |
174
|
|
|
public function mkdir($path) { |
175
|
|
|
$this->init(); |
176
|
|
|
$path = $this->cleanPath($path); |
177
|
|
|
$result = $this->simpleResponse('MKCOL', $path, null, 201); |
178
|
|
|
if ($result) { |
179
|
|
|
$this->statCache->set($path, true); |
180
|
|
|
} |
181
|
|
|
return $result; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** {@inheritdoc} */ |
185
|
|
View Code Duplication |
public function rmdir($path) { |
186
|
|
|
$this->init(); |
187
|
|
|
$path = $this->cleanPath($path); |
188
|
|
|
// FIXME: some WebDAV impl return 403 when trying to DELETE |
189
|
|
|
// a non-empty folder |
190
|
|
|
$result = $this->simpleResponse('DELETE', $path . '/', null, 204); |
191
|
|
|
$this->statCache->clear($path . '/'); |
192
|
|
|
$this->statCache->remove($path); |
193
|
|
|
return $result; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** {@inheritdoc} */ |
197
|
|
|
public function opendir($path) { |
198
|
|
|
$this->init(); |
199
|
|
|
$path = $this->cleanPath($path); |
200
|
|
|
try { |
201
|
|
|
$response = $this->client->propfind( |
|
|
|
|
202
|
|
|
$this->encodePath($path), |
203
|
|
|
[], |
204
|
|
|
1 |
205
|
|
|
); |
206
|
|
|
if ($response === false) { |
207
|
|
|
return false; |
208
|
|
|
} |
209
|
|
|
$content = []; |
210
|
|
|
$files = \array_keys($response); |
211
|
|
|
\array_shift($files); //the first entry is the current directory |
212
|
|
|
|
213
|
|
|
if (!$this->statCache->hasKey($path)) { |
214
|
|
|
$this->statCache->set($path, true); |
215
|
|
|
} |
216
|
|
|
foreach ($files as $file) { |
217
|
|
|
$file = \urldecode($file); |
218
|
|
|
// do not store the real entry, we might not have all properties |
219
|
|
|
if (!$this->statCache->hasKey($path)) { |
220
|
|
|
$this->statCache->set($file, true); |
221
|
|
|
} |
222
|
|
|
$file = \basename($file); |
223
|
|
|
$content[] = $file; |
224
|
|
|
} |
225
|
|
|
return IteratorDirectory::wrap($content); |
226
|
|
|
} catch (ClientHttpException $e) { |
227
|
|
View Code Duplication |
if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND) { |
|
|
|
|
228
|
|
|
$this->statCache->clear($path . '/'); |
229
|
|
|
$this->statCache->set($path, false); |
230
|
|
|
return false; |
231
|
|
|
} |
232
|
|
|
$this->convertException($e, $path); |
233
|
|
|
} catch (\Exception $e) { |
234
|
|
|
$this->convertException($e, $path); |
235
|
|
|
} |
236
|
|
|
return false; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Propfind call with cache handling. |
241
|
|
|
* |
242
|
|
|
* First checks if information is cached. |
243
|
|
|
* If not, request it from the server then store to cache. |
244
|
|
|
* |
245
|
|
|
* @param string $path path to propfind |
246
|
|
|
* |
247
|
|
|
* @return array|boolean propfind response or false if the entry was not found |
248
|
|
|
* |
249
|
|
|
* @throws ClientHttpException |
250
|
|
|
*/ |
251
|
|
|
protected function propfind($path) { |
252
|
|
|
$path = $this->cleanPath($path); |
253
|
|
|
$cachedResponse = $this->statCache->get($path); |
254
|
|
|
// we either don't know it, or we know it exists but need more details |
255
|
|
|
if ($cachedResponse === null || $cachedResponse === true) { |
256
|
|
|
$this->init(); |
257
|
|
|
try { |
258
|
|
|
$response = $this->client->propfind( |
|
|
|
|
259
|
|
|
$this->encodePath($path), |
260
|
|
|
[ |
261
|
|
|
'{DAV:}getlastmodified', |
262
|
|
|
'{DAV:}getcontentlength', |
263
|
|
|
'{DAV:}getcontenttype', |
264
|
|
|
'{http://owncloud.org/ns}permissions', |
265
|
|
|
'{http://open-collaboration-services.org/ns}share-permissions', |
266
|
|
|
'{DAV:}resourcetype', |
267
|
|
|
'{DAV:}getetag', |
268
|
|
|
] |
269
|
|
|
); |
270
|
|
|
$this->statCache->set($path, $response); |
271
|
|
|
} catch (ClientHttpException $e) { |
272
|
|
View Code Duplication |
if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND) { |
|
|
|
|
273
|
|
|
$this->statCache->clear($path . '/'); |
274
|
|
|
$this->statCache->set($path, false); |
275
|
|
|
return false; |
276
|
|
|
} |
277
|
|
|
$this->convertException($e, $path); |
278
|
|
|
} catch (\Exception $e) { |
279
|
|
|
$this->convertException($e, $path); |
280
|
|
|
} |
281
|
|
|
} else { |
282
|
|
|
$response = $cachedResponse; |
283
|
|
|
} |
284
|
|
|
return $response; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** {@inheritdoc} */ |
288
|
|
|
public function filetype($path) { |
289
|
|
|
try { |
290
|
|
|
$response = $this->propfind($path); |
291
|
|
|
if ($response === false) { |
292
|
|
|
return false; |
293
|
|
|
} |
294
|
|
|
$responseType = []; |
295
|
|
|
if (isset($response["{DAV:}resourcetype"])) { |
296
|
|
|
/** @var ResourceType[] $response */ |
297
|
|
|
$responseType = $response["{DAV:}resourcetype"]->getValue(); |
298
|
|
|
} |
299
|
|
|
return (\count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; |
300
|
|
|
} catch (\Exception $e) { |
301
|
|
|
$this->convertException($e, $path); |
302
|
|
|
} |
303
|
|
|
return false; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** {@inheritdoc} */ |
307
|
|
|
public function file_exists($path) { |
308
|
|
|
try { |
309
|
|
|
$path = $this->cleanPath($path); |
310
|
|
|
$cachedState = $this->statCache->get($path); |
311
|
|
|
if ($cachedState === false) { |
312
|
|
|
// we know the file doesn't exist |
313
|
|
|
return false; |
314
|
|
|
} elseif ($cachedState !== null) { |
315
|
|
|
return true; |
316
|
|
|
} |
317
|
|
|
// need to get from server |
318
|
|
|
return ($this->propfind($path) !== false); |
319
|
|
|
} catch (\Exception $e) { |
320
|
|
|
$this->convertException($e, $path); |
321
|
|
|
} |
322
|
|
|
return false; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** {@inheritdoc} */ |
326
|
|
View Code Duplication |
public function unlink($path) { |
327
|
|
|
$this->init(); |
328
|
|
|
$path = $this->cleanPath($path); |
329
|
|
|
$result = $this->simpleResponse('DELETE', $path, null, 204); |
330
|
|
|
$this->statCache->clear($path . '/'); |
331
|
|
|
$this->statCache->remove($path); |
332
|
|
|
return $result; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** {@inheritdoc} */ |
336
|
|
|
public function fopen($path, $mode) { |
337
|
|
|
$this->init(); |
338
|
|
|
$path = $this->cleanPath($path); |
339
|
|
|
switch ($mode) { |
340
|
|
|
case 'r': |
341
|
|
|
case 'rb': |
342
|
|
|
try { |
343
|
|
|
$response = $this->httpClientService |
344
|
|
|
->newClient() |
345
|
|
|
->get($this->createBaseUri() . $this->encodePath($path), [ |
346
|
|
|
'auth' => [$this->user, $this->password], |
347
|
|
|
'stream' => true |
348
|
|
|
]); |
349
|
|
|
} catch (RequestException $e) { |
350
|
|
|
if ($e->getResponse() instanceof ResponseInterface |
351
|
|
|
&& $e->getResponse()->getStatusCode() === Http::STATUS_NOT_FOUND) { |
352
|
|
|
return false; |
353
|
|
|
} else { |
354
|
|
|
$this->convertException($e); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
if ($response->getStatusCode() !== Http::STATUS_OK) { |
359
|
|
|
if ($response->getStatusCode() === Http::STATUS_LOCKED) { |
360
|
|
|
throw new \OCP\Lock\LockedException($path); |
361
|
|
|
} else { |
362
|
|
|
Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR); |
363
|
|
|
// FIXME: why not returning false here ?! |
364
|
|
|
} |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
return $response->getBody(); |
|
|
|
|
368
|
|
|
case 'w': |
369
|
|
|
case 'wb': |
370
|
|
|
case 'a': |
371
|
|
|
case 'ab': |
372
|
|
|
case 'r+': |
373
|
|
|
case 'w+': |
374
|
|
|
case 'wb+': |
375
|
|
|
case 'a+': |
376
|
|
|
case 'x': |
377
|
|
|
case 'x+': |
378
|
|
|
case 'c': |
379
|
|
|
case 'c+': |
380
|
|
|
//emulate these |
381
|
|
|
$tempManager = \OC::$server->getTempManager(); |
382
|
|
|
if (\strrpos($path, '.') !== false) { |
383
|
|
|
$ext = \substr($path, \strrpos($path, '.')); |
384
|
|
|
} else { |
385
|
|
|
$ext = ''; |
386
|
|
|
} |
387
|
|
|
if ($this->file_exists($path)) { |
388
|
|
|
if (!$this->isUpdatable($path)) { |
389
|
|
|
return false; |
390
|
|
|
} |
391
|
|
|
if ($mode === 'w' or $mode === 'w+') { |
392
|
|
|
$tmpFile = $tempManager->getTemporaryFile($ext); |
393
|
|
|
} else { |
394
|
|
|
$tmpFile = $this->getCachedFile($path); |
395
|
|
|
} |
396
|
|
|
} else { |
397
|
|
|
if (!$this->isCreatable(\dirname($path))) { |
398
|
|
|
return false; |
399
|
|
|
} |
400
|
|
|
$tmpFile = $tempManager->getTemporaryFile($ext); |
401
|
|
|
} |
402
|
|
|
Close::registerCallback($tmpFile, [$this, 'writeBack']); |
403
|
|
|
self::$tempFiles[$tmpFile] = $path; |
404
|
|
|
return \fopen('close://' . $tmpFile, $mode); |
405
|
|
|
} |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* @param string $tmpFile |
410
|
|
|
*/ |
411
|
|
|
public function writeBack($tmpFile) { |
412
|
|
View Code Duplication |
if (isset(self::$tempFiles[$tmpFile])) { |
|
|
|
|
413
|
|
|
$this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]); |
414
|
|
|
\unlink($tmpFile); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** {@inheritdoc} */ |
419
|
|
|
public function free_space($path) { |
420
|
|
|
$this->init(); |
421
|
|
|
$path = $this->cleanPath($path); |
422
|
|
|
try { |
423
|
|
|
// TODO: cacheable ? |
424
|
|
|
$response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']); |
|
|
|
|
425
|
|
|
if ($response === false) { |
426
|
|
|
return FileInfo::SPACE_UNKNOWN; |
427
|
|
|
} |
428
|
|
|
if (isset($response['{DAV:}quota-available-bytes'])) { |
429
|
|
|
return (int)$response['{DAV:}quota-available-bytes']; |
430
|
|
|
} else { |
431
|
|
|
return FileInfo::SPACE_UNKNOWN; |
432
|
|
|
} |
433
|
|
|
} catch (\Exception $e) { |
434
|
|
|
return FileInfo::SPACE_UNKNOWN; |
435
|
|
|
} |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** {@inheritdoc} */ |
439
|
|
|
public function touch($path, $mtime = null) { |
440
|
|
|
$this->init(); |
441
|
|
|
if ($mtime === null) { |
442
|
|
|
$mtime = \OC::$server->getTimeFactory()->getTime(); |
443
|
|
|
} |
444
|
|
|
$path = $this->cleanPath($path); |
445
|
|
|
|
446
|
|
|
// if file exists, update the mtime, else create a new empty file |
447
|
|
|
if ($this->file_exists($path)) { |
448
|
|
|
try { |
449
|
|
|
$this->statCache->remove($path); |
450
|
|
|
$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]); |
|
|
|
|
451
|
|
|
// non-owncloud clients might not have accepted the property, need to recheck it |
452
|
|
|
$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0); |
|
|
|
|
453
|
|
|
if ($response === false) { |
454
|
|
|
// file disappeared since ? |
455
|
|
|
return false; |
456
|
|
|
} |
457
|
|
|
if (isset($response['{DAV:}getlastmodified'])) { |
458
|
|
|
$remoteMtime = \strtotime($response['{DAV:}getlastmodified']); |
459
|
|
|
if ($remoteMtime !== $mtime) { |
460
|
|
|
// server has not accepted the mtime |
461
|
|
|
return false; |
462
|
|
|
} |
463
|
|
|
} |
464
|
|
|
} catch (ClientHttpException $e) { |
465
|
|
|
if ($e->getHttpStatus() === 501) { |
466
|
|
|
return false; |
467
|
|
|
} |
468
|
|
|
$this->convertException($e, $path); |
469
|
|
|
return false; |
470
|
|
|
} catch (\Exception $e) { |
471
|
|
|
$this->convertException($e, $path); |
472
|
|
|
return false; |
473
|
|
|
} |
474
|
|
|
} else { |
475
|
|
|
$this->file_put_contents($path, ''); |
476
|
|
|
} |
477
|
|
|
return true; |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* @param string $path |
482
|
|
|
* @param string $data |
483
|
|
|
* @return int |
484
|
|
|
*/ |
485
|
|
|
public function file_put_contents($path, $data) { |
486
|
|
|
$path = $this->cleanPath($path); |
487
|
|
|
$result = parent::file_put_contents($path, $data); |
488
|
|
|
$this->statCache->remove($path); |
489
|
|
|
return $result; |
|
|
|
|
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* @param string $path |
494
|
|
|
* @param string $target |
495
|
|
|
*/ |
496
|
|
|
protected function uploadFile($path, $target) { |
497
|
|
|
$this->init(); |
498
|
|
|
|
499
|
|
|
// invalidate |
500
|
|
|
$target = $this->cleanPath($target); |
501
|
|
|
$this->statCache->remove($target); |
502
|
|
|
$source = \fopen($path, 'r'); |
503
|
|
|
|
504
|
|
|
$this->removeCachedFile($target); |
505
|
|
|
try { |
506
|
|
|
$this->httpClientService |
507
|
|
|
->newClient() |
508
|
|
|
->put($this->createBaseUri() . $this->encodePath($target), [ |
509
|
|
|
'body' => $source, |
510
|
|
|
'auth' => [$this->user, $this->password] |
511
|
|
|
]); |
512
|
|
|
} catch (\Exception $e) { |
513
|
|
|
$this->convertException($e); |
514
|
|
|
} |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** {@inheritdoc} */ |
518
|
|
|
public function rename($path1, $path2) { |
519
|
|
|
$this->init(); |
520
|
|
|
$path1 = $this->cleanPath($path1); |
521
|
|
|
$path2 = $this->cleanPath($path2); |
522
|
|
|
try { |
523
|
|
|
// overwrite directory ? |
524
|
|
|
if ($this->is_dir($path2)) { |
525
|
|
|
// needs trailing slash in destination |
526
|
|
|
$path2 = \rtrim($path2, '/') . '/'; |
527
|
|
|
} |
528
|
|
|
$this->client->request( |
|
|
|
|
529
|
|
|
'MOVE', |
530
|
|
|
$this->encodePath($path1), |
531
|
|
|
null, |
532
|
|
|
[ |
533
|
|
|
'Destination' => $this->createBaseUri() . $this->encodePath($path2), |
534
|
|
|
] |
535
|
|
|
); |
536
|
|
|
$this->statCache->clear($path1 . '/'); |
537
|
|
|
$this->statCache->clear($path2 . '/'); |
538
|
|
|
$this->statCache->set($path1, false); |
539
|
|
|
$this->statCache->set($path2, true); |
540
|
|
|
$this->removeCachedFile($path1); |
541
|
|
|
$this->removeCachedFile($path2); |
542
|
|
|
return true; |
543
|
|
|
} catch (\Exception $e) { |
544
|
|
|
$this->convertException($e); |
545
|
|
|
} |
546
|
|
|
return false; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
/** {@inheritdoc} */ |
550
|
|
|
public function copy($path1, $path2) { |
551
|
|
|
$this->init(); |
552
|
|
|
$path1 = $this->cleanPath($path1); |
553
|
|
|
$path2 = $this->cleanPath($path2); |
554
|
|
|
try { |
555
|
|
|
// overwrite directory ? |
556
|
|
|
if ($this->is_dir($path2)) { |
557
|
|
|
// needs trailing slash in destination |
558
|
|
|
$path2 = \rtrim($path2, '/') . '/'; |
559
|
|
|
} |
560
|
|
|
$this->client->request( |
|
|
|
|
561
|
|
|
'COPY', |
562
|
|
|
$this->encodePath($path1), |
563
|
|
|
null, |
564
|
|
|
[ |
565
|
|
|
'Destination' => $this->createBaseUri() . $this->encodePath($path2), |
566
|
|
|
] |
567
|
|
|
); |
568
|
|
|
$this->statCache->clear($path2 . '/'); |
569
|
|
|
$this->statCache->set($path2, true); |
570
|
|
|
$this->removeCachedFile($path2); |
571
|
|
|
return true; |
572
|
|
|
} catch (\Exception $e) { |
573
|
|
|
$this->convertException($e); |
574
|
|
|
} |
575
|
|
|
return false; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** {@inheritdoc} */ |
579
|
|
|
public function stat($path) { |
580
|
|
|
try { |
581
|
|
|
$response = $this->propfind($path); |
582
|
|
|
if ($response === false) { |
583
|
|
|
return []; |
584
|
|
|
} |
585
|
|
|
return [ |
586
|
|
|
'mtime' => \strtotime($response['{DAV:}getlastmodified']), |
587
|
|
|
'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0, |
588
|
|
|
]; |
589
|
|
|
} catch (\Exception $e) { |
590
|
|
|
$this->convertException($e, $path); |
591
|
|
|
} |
592
|
|
|
return []; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** {@inheritdoc} */ |
596
|
|
|
public function getMimeType($path) { |
597
|
|
|
try { |
598
|
|
|
$response = $this->propfind($path); |
599
|
|
|
if ($response === false) { |
600
|
|
|
return false; |
601
|
|
|
} |
602
|
|
|
$responseType = []; |
603
|
|
|
if (isset($response["{DAV:}resourcetype"])) { |
604
|
|
|
/** @var ResourceType[] $response */ |
605
|
|
|
$responseType = $response["{DAV:}resourcetype"]->getValue(); |
606
|
|
|
} |
607
|
|
|
$type = (\count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; |
608
|
|
|
if ($type == 'dir') { |
609
|
|
|
return 'httpd/unix-directory'; |
610
|
|
|
} elseif (isset($response['{DAV:}getcontenttype'])) { |
611
|
|
|
return $response['{DAV:}getcontenttype']; |
612
|
|
|
} else { |
613
|
|
|
return false; |
614
|
|
|
} |
615
|
|
|
} catch (\Exception $e) { |
616
|
|
|
$this->convertException($e, $path); |
617
|
|
|
} |
618
|
|
|
return false; |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* @param string $path |
623
|
|
|
* @return string |
624
|
|
|
*/ |
625
|
|
|
public function cleanPath($path) { |
626
|
|
|
if ($path === '') { |
627
|
|
|
return $path; |
628
|
|
|
} |
629
|
|
|
$path = Filesystem::normalizePath($path); |
630
|
|
|
// remove leading slash |
631
|
|
|
return \substr($path, 1); |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
/** |
635
|
|
|
* URL encodes the given path but keeps the slashes |
636
|
|
|
* |
637
|
|
|
* @param string $path to encode |
638
|
|
|
* @return string encoded path |
639
|
|
|
*/ |
640
|
|
|
private function encodePath($path) { |
641
|
|
|
// slashes need to stay |
642
|
|
|
return \str_replace('%2F', '/', \rawurlencode($path)); |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* @param string $method |
647
|
|
|
* @param string $path |
648
|
|
|
* @param string|resource|null $body |
649
|
|
|
* @param int $expected |
650
|
|
|
* @return bool |
651
|
|
|
* @throws StorageInvalidException |
652
|
|
|
* @throws StorageNotAvailableException |
653
|
|
|
*/ |
654
|
|
|
private function simpleResponse($method, $path, $body, $expected) { |
655
|
|
|
$path = $this->cleanPath($path); |
656
|
|
|
try { |
657
|
|
|
$response = $this->client->request($method, $this->encodePath($path), $body); |
|
|
|
|
658
|
|
|
return $response['statusCode'] == $expected; |
659
|
|
|
} catch (ClientHttpException $e) { |
660
|
|
View Code Duplication |
if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND && $method === 'DELETE') { |
|
|
|
|
661
|
|
|
$this->statCache->clear($path . '/'); |
662
|
|
|
$this->statCache->set($path, false); |
663
|
|
|
return false; |
664
|
|
|
} |
665
|
|
|
if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED && $method === 'MKCOL') { |
666
|
|
|
return false; |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
$this->convertException($e, $path); |
670
|
|
|
} catch (\Exception $e) { |
671
|
|
|
$this->convertException($e, $path); |
672
|
|
|
} |
673
|
|
|
return false; |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
/** |
677
|
|
|
* check if curl is installed |
678
|
|
|
*/ |
679
|
|
|
public static function checkDependencies() { |
680
|
|
|
return true; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
/** {@inheritdoc} */ |
684
|
|
|
public function isUpdatable($path) { |
685
|
|
|
return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
/** {@inheritdoc} */ |
689
|
|
|
public function isCreatable($path) { |
690
|
|
|
return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
/** {@inheritdoc} */ |
694
|
|
|
public function isSharable($path) { |
695
|
|
|
return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
/** {@inheritdoc} */ |
699
|
|
|
public function isDeletable($path) { |
700
|
|
|
return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
/** {@inheritdoc} */ |
704
|
|
|
public function getPermissions($path) { |
705
|
|
|
$this->init(); |
706
|
|
|
$path = $this->cleanPath($path); |
707
|
|
|
$response = $this->propfind($path); |
708
|
|
|
if ($response === false) { |
709
|
|
|
return 0; |
710
|
|
|
} |
711
|
|
|
if (isset($response['{http://owncloud.org/ns}permissions'])) { |
712
|
|
|
return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']); |
713
|
|
|
} elseif ($this->is_dir($path)) { |
714
|
|
|
return Constants::PERMISSION_ALL; |
715
|
|
|
} elseif ($this->file_exists($path)) { |
716
|
|
|
return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; |
717
|
|
|
} else { |
718
|
|
|
return 0; |
719
|
|
|
} |
720
|
|
|
} |
721
|
|
|
|
722
|
|
|
/** {@inheritdoc} */ |
723
|
|
|
public function getETag($path) { |
724
|
|
|
$this->init(); |
725
|
|
|
$path = $this->cleanPath($path); |
726
|
|
|
$response = $this->propfind($path); |
727
|
|
|
if ($response === false) { |
728
|
|
|
return null; |
729
|
|
|
} |
730
|
|
|
if (isset($response['{DAV:}getetag'])) { |
731
|
|
|
return \trim($response['{DAV:}getetag'], '"'); |
732
|
|
|
} |
733
|
|
|
return parent::getEtag($path); |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* @param string $permissionsString |
738
|
|
|
* @return int |
739
|
|
|
*/ |
740
|
|
|
protected function parsePermissions($permissionsString) { |
741
|
|
|
$permissions = Constants::PERMISSION_READ; |
742
|
|
|
if (\strpos($permissionsString, 'R') !== false) { |
743
|
|
|
$permissions |= Constants::PERMISSION_SHARE; |
744
|
|
|
} |
745
|
|
|
if (\strpos($permissionsString, 'D') !== false) { |
746
|
|
|
$permissions |= Constants::PERMISSION_DELETE; |
747
|
|
|
} |
748
|
|
|
if (\strpos($permissionsString, 'W') !== false) { |
749
|
|
|
$permissions |= Constants::PERMISSION_UPDATE; |
750
|
|
|
} |
751
|
|
|
if (\strpos($permissionsString, 'CK') !== false) { |
752
|
|
|
$permissions |= Constants::PERMISSION_CREATE; |
753
|
|
|
$permissions |= Constants::PERMISSION_UPDATE; |
754
|
|
|
} |
755
|
|
|
return $permissions; |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
/** |
759
|
|
|
* check if a file or folder has been updated since $time |
760
|
|
|
* |
761
|
|
|
* @param string $path |
762
|
|
|
* @param int $time |
763
|
|
|
* @throws \OCP\Files\StorageNotAvailableException |
764
|
|
|
* @return bool |
765
|
|
|
*/ |
766
|
|
|
public function hasUpdated($path, $time) { |
767
|
|
|
$this->init(); |
768
|
|
|
$path = $this->cleanPath($path); |
769
|
|
|
try { |
770
|
|
|
// force refresh for $path |
771
|
|
|
$this->statCache->remove($path); |
772
|
|
|
$response = $this->propfind($path); |
773
|
|
|
if ($response === false) { |
774
|
|
|
if ($path === '') { |
775
|
|
|
// if root is gone it means the storage is not available |
776
|
|
|
throw new StorageNotAvailableException('remote root vanished'); |
777
|
|
|
} |
778
|
|
|
return false; |
779
|
|
|
} |
780
|
|
|
if (isset($response['{DAV:}getetag'])) { |
781
|
|
|
$cachedData = $this->getCache()->get($path); |
782
|
|
|
$etag = \trim($response['{DAV:}getetag'], '"'); |
783
|
|
|
if (!empty($etag) && $cachedData['etag'] !== $etag) { |
784
|
|
|
return true; |
785
|
|
|
} elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) { |
786
|
|
|
$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions']; |
787
|
|
|
return $sharePermissions !== $cachedData['permissions']; |
788
|
|
|
} elseif (isset($response['{http://owncloud.org/ns}permissions'])) { |
789
|
|
|
$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']); |
790
|
|
|
return $permissions !== $cachedData['permissions']; |
791
|
|
|
} else { |
792
|
|
|
return false; |
793
|
|
|
} |
794
|
|
|
} else { |
795
|
|
|
$remoteMtime = \strtotime($response['{DAV:}getlastmodified']); |
796
|
|
|
return $remoteMtime > $time; |
797
|
|
|
} |
798
|
|
|
} catch (ClientHttpException $e) { |
799
|
|
|
if ($e->getHttpStatus() === 405) { |
800
|
|
|
if ($path === '') { |
801
|
|
|
// if root is gone it means the storage is not available |
802
|
|
|
throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage()); |
803
|
|
|
} |
804
|
|
|
return false; |
805
|
|
|
} |
806
|
|
|
$this->convertException($e, $path); |
807
|
|
|
return false; |
808
|
|
|
} catch (\Exception $e) { |
809
|
|
|
$this->convertException($e, $path); |
810
|
|
|
return false; |
811
|
|
|
} |
812
|
|
|
} |
813
|
|
|
|
814
|
|
|
/** |
815
|
|
|
* Interpret the given exception and decide whether it is due to an |
816
|
|
|
* unavailable storage, invalid storage or other. |
817
|
|
|
* This will either throw StorageInvalidException, StorageNotAvailableException |
818
|
|
|
* or do nothing. |
819
|
|
|
* |
820
|
|
|
* @param Exception $e sabre exception |
821
|
|
|
* @param string $path optional path from the operation |
822
|
|
|
* |
823
|
|
|
* @throws StorageInvalidException if the storage is invalid, for example |
824
|
|
|
* when the authentication expired or is invalid |
825
|
|
|
* @throws StorageNotAvailableException if the storage is not available, |
826
|
|
|
* which might be temporary |
827
|
|
|
*/ |
828
|
|
|
private function convertException(Exception $e, $path = '') { |
829
|
|
|
\OC::$server->getLogger()->logException($e); |
830
|
|
|
Util::writeLog('files_external', $e->getMessage(), Util::ERROR); |
831
|
|
|
if ($e instanceof ClientHttpException) { |
832
|
|
|
$this->throwByStatusCode($e->getHttpStatus(), $e, $path); |
833
|
|
|
} elseif ($e instanceof \GuzzleHttp\Exception\RequestException) { |
834
|
|
|
if ($e->getResponse() instanceof ResponseInterface) { |
835
|
|
|
$this->throwByStatusCode($e->getResponse()->getStatusCode(), $e); |
836
|
|
|
} |
837
|
|
|
// connection timeout or refused, server could be temporarily down |
838
|
|
|
throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage()); |
839
|
|
|
} elseif ($e instanceof \InvalidArgumentException) { |
840
|
|
|
// parse error because the server returned HTML instead of XML, |
841
|
|
|
// possibly temporarily down |
842
|
|
|
throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage()); |
843
|
|
|
} elseif (($e instanceof StorageNotAvailableException) |
844
|
|
|
|| ($e instanceof StorageInvalidException) |
845
|
|
|
|| ($e instanceof \Sabre\DAV\Exception |
846
|
|
|
)) { |
847
|
|
|
// rethrow |
848
|
|
|
throw $e; |
849
|
|
|
} |
850
|
|
|
|
851
|
|
|
// TODO: only log for now, but in the future need to wrap/rethrow exception |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
/** |
855
|
|
|
* Throw exception by status code |
856
|
|
|
* |
857
|
|
|
* @param int $statusCode status code |
858
|
|
|
* @param string $path optional path for some exceptions |
859
|
|
|
* @throws \Exception Sabre or ownCloud exceptions |
860
|
|
|
*/ |
861
|
|
|
private function throwByStatusCode($statusCode, $e, $path = '') { |
862
|
|
|
switch ($statusCode) { |
863
|
|
|
case Http::STATUS_LOCKED: |
864
|
|
|
throw new \OCP\Lock\LockedException($path); |
865
|
|
|
case Http::STATUS_UNAUTHORIZED: |
866
|
|
|
// either password was changed or was invalid all along |
867
|
|
|
throw new StorageInvalidException(\get_class($e) . ': ' . $e->getMessage()); |
868
|
|
|
case Http::STATUS_INSUFFICIENT_STORAGE: |
869
|
|
|
throw new InsufficientStorage(); |
870
|
|
|
case Http::STATUS_FORBIDDEN: |
871
|
|
|
throw new Forbidden('Forbidden'); |
872
|
|
|
} |
873
|
|
|
throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage()); |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
/** |
877
|
|
|
* @param string $path |
878
|
|
|
* @param array $options |
879
|
|
|
* @return StreamInterface |
880
|
|
|
* @since 11.0.0 |
881
|
|
|
*/ |
882
|
|
|
public function readFile(string $path, array $options = []): StreamInterface { |
883
|
|
|
$this->init(); |
884
|
|
|
$path = $this->cleanPath($path); |
885
|
|
|
$response = $this->httpClientService |
886
|
|
|
->newClient() |
887
|
|
|
->get($this->createBaseUri() . $this->encodePath($path), [ |
888
|
|
|
'auth' => [$this->user, $this->password], |
889
|
|
|
'stream' => true |
890
|
|
|
]); |
891
|
|
|
return stream_for($response->getBody()); |
892
|
|
|
} |
893
|
|
|
|
894
|
|
|
/** |
895
|
|
|
* @param string $path |
896
|
|
|
* @param StreamInterface $stream |
897
|
|
|
* @return int |
898
|
|
|
* @since 11.0.0 |
899
|
|
|
*/ |
900
|
|
|
public function writeFile(string $path, StreamInterface $stream): int { |
901
|
|
|
$this->init(); |
902
|
|
|
$path = $this->cleanPath($path); |
903
|
|
|
|
904
|
|
|
if ($this->file_exists($path)) { |
905
|
|
|
if (!$this->isUpdatable($path)) { |
906
|
|
|
throw new \OCP\Files\ForbiddenException(); |
|
|
|
|
907
|
|
|
} |
908
|
|
|
} else { |
909
|
|
|
if (!$this->isCreatable(\dirname($path))) { |
910
|
|
|
throw new \OCP\Files\ForbiddenException(); |
|
|
|
|
911
|
|
|
} |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
try { |
915
|
|
|
$size = $stream->getSize(); |
916
|
|
|
$this->httpClientService |
917
|
|
|
->newClient() |
918
|
|
|
->put($this->createBaseUri() . $this->encodePath($path), [ |
919
|
|
|
'body' => $stream->detach(), |
920
|
|
|
'auth' => [$this->user, $this->password] |
921
|
|
|
]); |
922
|
|
|
return $size; |
923
|
|
|
} catch (\Exception $e) { |
924
|
|
|
$this->convertException($e); |
925
|
|
|
} |
926
|
|
|
} |
927
|
|
|
} |
928
|
|
|
|
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.