Passed
Push — master ( 60f946...983435 )
by Robin
12:53 queued 12s
created
apps/files_sharing/lib/External/Storage.php 1 patch
Indentation   +383 added lines, -383 removed lines patch added patch discarded remove patch
@@ -50,387 +50,387 @@
 block discarded – undo
50 50
 use OCP\Http\Client\IClientService;
51 51
 
52 52
 class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, IReliableEtagStorage {
53
-	/** @var ICloudId */
54
-	private $cloudId;
55
-	/** @var string */
56
-	private $mountPoint;
57
-	/** @var string */
58
-	private $token;
59
-	/** @var \OCP\ICacheFactory */
60
-	private $memcacheFactory;
61
-	/** @var \OCP\Http\Client\IClientService */
62
-	private $httpClient;
63
-	/** @var bool */
64
-	private $updateChecked = false;
65
-
66
-	/** @var ExternalShareManager */
67
-	private $manager;
68
-
69
-	/**
70
-	 * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options
71
-	 */
72
-	public function __construct($options) {
73
-		$this->memcacheFactory = \OC::$server->getMemCacheFactory();
74
-		$this->httpClient = $options['HttpClientService'];
75
-
76
-		$this->manager = $options['manager'];
77
-		$this->cloudId = $options['cloudId'];
78
-		$discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class);
79
-
80
-		[$protocol, $remote] = explode('://', $this->cloudId->getRemote());
81
-		if (strpos($remote, '/')) {
82
-			[$host, $root] = explode('/', $remote, 2);
83
-		} else {
84
-			$host = $remote;
85
-			$root = '';
86
-		}
87
-		$secure = $protocol === 'https';
88
-		$federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
89
-		$webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
90
-		$root = rtrim($root, '/') . $webDavEndpoint;
91
-		$this->mountPoint = $options['mountpoint'];
92
-		$this->token = $options['token'];
93
-
94
-		parent::__construct([
95
-			'secure' => $secure,
96
-			'host' => $host,
97
-			'root' => $root,
98
-			'user' => $options['token'],
99
-			'password' => (string)$options['password']
100
-		]);
101
-	}
102
-
103
-	public function getWatcher($path = '', $storage = null) {
104
-		if (!$storage) {
105
-			$storage = $this;
106
-		}
107
-		if (!isset($this->watcher)) {
108
-			$this->watcher = new Watcher($storage);
109
-			$this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
110
-		}
111
-		return $this->watcher;
112
-	}
113
-
114
-	public function getRemoteUser(): string {
115
-		return $this->cloudId->getUser();
116
-	}
117
-
118
-	public function getRemote(): string {
119
-		return $this->cloudId->getRemote();
120
-	}
121
-
122
-	public function getMountPoint(): string {
123
-		return $this->mountPoint;
124
-	}
125
-
126
-	public function getToken(): string {
127
-		return $this->token;
128
-	}
129
-
130
-	public function getPassword(): ?string {
131
-		return $this->password;
132
-	}
133
-
134
-	/**
135
-	 * Get id of the mount point.
136
-	 * @return string
137
-	 */
138
-	public function getId() {
139
-		return 'shared::' . md5($this->token . '@' . $this->getRemote());
140
-	}
141
-
142
-	public function getCache($path = '', $storage = null) {
143
-		if (is_null($this->cache)) {
144
-			$this->cache = new Cache($this, $this->cloudId);
145
-		}
146
-		return $this->cache;
147
-	}
148
-
149
-	/**
150
-	 * @param string $path
151
-	 * @param \OC\Files\Storage\Storage $storage
152
-	 * @return \OCA\Files_Sharing\External\Scanner
153
-	 */
154
-	public function getScanner($path = '', $storage = null) {
155
-		if (!$storage) {
156
-			$storage = $this;
157
-		}
158
-		if (!isset($this->scanner)) {
159
-			$this->scanner = new Scanner($storage);
160
-		}
161
-		return $this->scanner;
162
-	}
163
-
164
-	/**
165
-	 * Check if a file or folder has been updated since $time
166
-	 *
167
-	 * @param string $path
168
-	 * @param int $time
169
-	 * @throws \OCP\Files\StorageNotAvailableException
170
-	 * @throws \OCP\Files\StorageInvalidException
171
-	 * @return bool
172
-	 */
173
-	public function hasUpdated($path, $time) {
174
-		// since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
175
-		// because of that we only do one check for the entire storage per request
176
-		if ($this->updateChecked) {
177
-			return false;
178
-		}
179
-		$this->updateChecked = true;
180
-		try {
181
-			return parent::hasUpdated('', $time);
182
-		} catch (StorageInvalidException $e) {
183
-			// check if it needs to be removed
184
-			$this->checkStorageAvailability();
185
-			throw $e;
186
-		} catch (StorageNotAvailableException $e) {
187
-			// check if it needs to be removed or just temp unavailable
188
-			$this->checkStorageAvailability();
189
-			throw $e;
190
-		}
191
-	}
192
-
193
-	public function test() {
194
-		try {
195
-			return parent::test();
196
-		} catch (StorageInvalidException $e) {
197
-			// check if it needs to be removed
198
-			$this->checkStorageAvailability();
199
-			throw $e;
200
-		} catch (StorageNotAvailableException $e) {
201
-			// check if it needs to be removed or just temp unavailable
202
-			$this->checkStorageAvailability();
203
-			throw $e;
204
-		}
205
-	}
206
-
207
-	/**
208
-	 * Check whether this storage is permanently or temporarily
209
-	 * unavailable
210
-	 *
211
-	 * @throws \OCP\Files\StorageNotAvailableException
212
-	 * @throws \OCP\Files\StorageInvalidException
213
-	 */
214
-	public function checkStorageAvailability() {
215
-		// see if we can find out why the share is unavailable
216
-		try {
217
-			$this->getShareInfo();
218
-		} catch (NotFoundException $e) {
219
-			// a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
220
-			if ($this->testRemote()) {
221
-				// valid Nextcloud instance means that the public share no longer exists
222
-				// since this is permanent (re-sharing the file will create a new token)
223
-				// we remove the invalid storage
224
-				$this->manager->removeShare($this->mountPoint);
225
-				$this->manager->getMountManager()->removeMount($this->mountPoint);
226
-				throw new StorageInvalidException("Remote share not found", 0, $e);
227
-			} else {
228
-				// Nextcloud instance is gone, likely to be a temporary server configuration error
229
-				throw new StorageNotAvailableException("No nextcloud instance found at remote", 0, $e);
230
-			}
231
-		} catch (ForbiddenException $e) {
232
-			// auth error, remove share for now (provide a dialog in the future)
233
-			$this->manager->removeShare($this->mountPoint);
234
-			$this->manager->getMountManager()->removeMount($this->mountPoint);
235
-			throw new StorageInvalidException("Auth error when getting remote share");
236
-		} catch (\GuzzleHttp\Exception\ConnectException $e) {
237
-			throw new StorageNotAvailableException("Failed to connect to remote instance", 0, $e);
238
-		} catch (\GuzzleHttp\Exception\RequestException $e) {
239
-			throw new StorageNotAvailableException("Error while sending request to remote instance", 0, $e);
240
-		}
241
-	}
242
-
243
-	public function file_exists($path) {
244
-		if ($path === '') {
245
-			return true;
246
-		} else {
247
-			return parent::file_exists($path);
248
-		}
249
-	}
250
-
251
-	/**
252
-	 * Check if the configured remote is a valid federated share provider
253
-	 *
254
-	 * @return bool
255
-	 */
256
-	protected function testRemote(): bool {
257
-		try {
258
-			return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
259
-				|| $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
260
-				|| $this->testRemoteUrl($this->getRemote() . '/status.php');
261
-		} catch (\Exception $e) {
262
-			return false;
263
-		}
264
-	}
265
-
266
-	private function testRemoteUrl(string $url): bool {
267
-		$cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url');
268
-		if ($cache->hasKey($url)) {
269
-			return (bool)$cache->get($url);
270
-		}
271
-
272
-		$client = $this->httpClient->newClient();
273
-		try {
274
-			$result = $client->get($url, [
275
-				'timeout' => 10,
276
-				'connect_timeout' => 10,
277
-			])->getBody();
278
-			$data = json_decode($result);
279
-			$returnValue = (is_object($data) && !empty($data->version));
280
-		} catch (ConnectException $e) {
281
-			$returnValue = false;
282
-		} catch (ClientException $e) {
283
-			$returnValue = false;
284
-		} catch (RequestException $e) {
285
-			$returnValue = false;
286
-		}
287
-
288
-		$cache->set($url, $returnValue, 60 * 60 * 24);
289
-		return $returnValue;
290
-	}
291
-
292
-	/**
293
-	 * Check whether the remote is an ownCloud/Nextcloud. This is needed since some sharing
294
-	 * features are not standardized.
295
-	 *
296
-	 * @throws LocalServerException
297
-	 */
298
-	public function remoteIsOwnCloud(): bool {
299
-		if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
300
-			return false;
301
-		}
302
-		return true;
303
-	}
304
-
305
-	/**
306
-	 * @return mixed
307
-	 * @throws ForbiddenException
308
-	 * @throws NotFoundException
309
-	 * @throws \Exception
310
-	 */
311
-	public function getShareInfo() {
312
-		$remote = $this->getRemote();
313
-		$token = $this->getToken();
314
-		$password = $this->getPassword();
315
-
316
-		try {
317
-			// If remote is not an ownCloud do not try to get any share info
318
-			if (!$this->remoteIsOwnCloud()) {
319
-				return ['status' => 'unsupported'];
320
-			}
321
-		} catch (LocalServerException $e) {
322
-			// throw this to be on the safe side: the share will still be visible
323
-			// in the UI in case the failure is intermittent, and the user will
324
-			// be able to decide whether to remove it if it's really gone
325
-			throw new StorageNotAvailableException();
326
-		}
327
-
328
-		$url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
329
-
330
-		// TODO: DI
331
-		$client = \OC::$server->getHTTPClientService()->newClient();
332
-		try {
333
-			$response = $client->post($url, [
334
-				'body' => ['password' => $password],
335
-				'timeout' => 10,
336
-				'connect_timeout' => 10,
337
-			]);
338
-		} catch (\GuzzleHttp\Exception\RequestException $e) {
339
-			if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
340
-				throw new ForbiddenException();
341
-			}
342
-			if ($e->getCode() === Http::STATUS_NOT_FOUND) {
343
-				throw new NotFoundException();
344
-			}
345
-			// throw this to be on the safe side: the share will still be visible
346
-			// in the UI in case the failure is intermittent, and the user will
347
-			// be able to decide whether to remove it if it's really gone
348
-			throw new StorageNotAvailableException();
349
-		}
350
-
351
-		return json_decode($response->getBody(), true);
352
-	}
353
-
354
-	public function getOwner($path) {
355
-		return $this->cloudId->getDisplayId();
356
-	}
357
-
358
-	public function isSharable($path) {
359
-		if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
360
-			return false;
361
-		}
362
-		return ($this->getPermissions($path) & Constants::PERMISSION_SHARE);
363
-	}
364
-
365
-	public function getPermissions($path) {
366
-		$response = $this->propfind($path);
367
-		// old federated sharing permissions
368
-		if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
369
-			$permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
370
-		} elseif (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) {
371
-			// permissions provided by the OCM API
372
-			$permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions'], $path);
373
-		} elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
374
-			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
375
-		} else {
376
-			// use default permission if remote server doesn't provide the share permissions
377
-			$permissions = $this->getDefaultPermissions($path);
378
-		}
379
-
380
-		return $permissions;
381
-	}
382
-
383
-	public function needsPartFile() {
384
-		return false;
385
-	}
386
-
387
-	/**
388
-	 * Translate OCM Permissions to Nextcloud permissions
389
-	 *
390
-	 * @param string $ocmPermissions json encoded OCM permissions
391
-	 * @param string $path path to file
392
-	 * @return int
393
-	 */
394
-	protected function ocmPermissions2ncPermissions(string $ocmPermissions, string $path): int {
395
-		try {
396
-			$ocmPermissions = json_decode($ocmPermissions);
397
-			$ncPermissions = 0;
398
-			foreach ($ocmPermissions as $permission) {
399
-				switch (strtolower($permission)) {
400
-					case 'read':
401
-						$ncPermissions += Constants::PERMISSION_READ;
402
-						break;
403
-					case 'write':
404
-						$ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE;
405
-						break;
406
-					case 'share':
407
-						$ncPermissions += Constants::PERMISSION_SHARE;
408
-						break;
409
-					default:
410
-						throw new \Exception();
411
-				}
412
-			}
413
-		} catch (\Exception $e) {
414
-			$ncPermissions = $this->getDefaultPermissions($path);
415
-		}
416
-
417
-		return $ncPermissions;
418
-	}
419
-
420
-	/**
421
-	 * Calculate the default permissions in case no permissions are provided
422
-	 */
423
-	protected function getDefaultPermissions(string $path): int {
424
-		if ($this->is_dir($path)) {
425
-			$permissions = Constants::PERMISSION_ALL;
426
-		} else {
427
-			$permissions = Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE;
428
-		}
429
-
430
-		return $permissions;
431
-	}
432
-
433
-	public function free_space($path) {
434
-		return parent::free_space("");
435
-	}
53
+    /** @var ICloudId */
54
+    private $cloudId;
55
+    /** @var string */
56
+    private $mountPoint;
57
+    /** @var string */
58
+    private $token;
59
+    /** @var \OCP\ICacheFactory */
60
+    private $memcacheFactory;
61
+    /** @var \OCP\Http\Client\IClientService */
62
+    private $httpClient;
63
+    /** @var bool */
64
+    private $updateChecked = false;
65
+
66
+    /** @var ExternalShareManager */
67
+    private $manager;
68
+
69
+    /**
70
+     * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options
71
+     */
72
+    public function __construct($options) {
73
+        $this->memcacheFactory = \OC::$server->getMemCacheFactory();
74
+        $this->httpClient = $options['HttpClientService'];
75
+
76
+        $this->manager = $options['manager'];
77
+        $this->cloudId = $options['cloudId'];
78
+        $discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class);
79
+
80
+        [$protocol, $remote] = explode('://', $this->cloudId->getRemote());
81
+        if (strpos($remote, '/')) {
82
+            [$host, $root] = explode('/', $remote, 2);
83
+        } else {
84
+            $host = $remote;
85
+            $root = '';
86
+        }
87
+        $secure = $protocol === 'https';
88
+        $federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
89
+        $webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
90
+        $root = rtrim($root, '/') . $webDavEndpoint;
91
+        $this->mountPoint = $options['mountpoint'];
92
+        $this->token = $options['token'];
93
+
94
+        parent::__construct([
95
+            'secure' => $secure,
96
+            'host' => $host,
97
+            'root' => $root,
98
+            'user' => $options['token'],
99
+            'password' => (string)$options['password']
100
+        ]);
101
+    }
102
+
103
+    public function getWatcher($path = '', $storage = null) {
104
+        if (!$storage) {
105
+            $storage = $this;
106
+        }
107
+        if (!isset($this->watcher)) {
108
+            $this->watcher = new Watcher($storage);
109
+            $this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
110
+        }
111
+        return $this->watcher;
112
+    }
113
+
114
+    public function getRemoteUser(): string {
115
+        return $this->cloudId->getUser();
116
+    }
117
+
118
+    public function getRemote(): string {
119
+        return $this->cloudId->getRemote();
120
+    }
121
+
122
+    public function getMountPoint(): string {
123
+        return $this->mountPoint;
124
+    }
125
+
126
+    public function getToken(): string {
127
+        return $this->token;
128
+    }
129
+
130
+    public function getPassword(): ?string {
131
+        return $this->password;
132
+    }
133
+
134
+    /**
135
+     * Get id of the mount point.
136
+     * @return string
137
+     */
138
+    public function getId() {
139
+        return 'shared::' . md5($this->token . '@' . $this->getRemote());
140
+    }
141
+
142
+    public function getCache($path = '', $storage = null) {
143
+        if (is_null($this->cache)) {
144
+            $this->cache = new Cache($this, $this->cloudId);
145
+        }
146
+        return $this->cache;
147
+    }
148
+
149
+    /**
150
+     * @param string $path
151
+     * @param \OC\Files\Storage\Storage $storage
152
+     * @return \OCA\Files_Sharing\External\Scanner
153
+     */
154
+    public function getScanner($path = '', $storage = null) {
155
+        if (!$storage) {
156
+            $storage = $this;
157
+        }
158
+        if (!isset($this->scanner)) {
159
+            $this->scanner = new Scanner($storage);
160
+        }
161
+        return $this->scanner;
162
+    }
163
+
164
+    /**
165
+     * Check if a file or folder has been updated since $time
166
+     *
167
+     * @param string $path
168
+     * @param int $time
169
+     * @throws \OCP\Files\StorageNotAvailableException
170
+     * @throws \OCP\Files\StorageInvalidException
171
+     * @return bool
172
+     */
173
+    public function hasUpdated($path, $time) {
174
+        // since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
175
+        // because of that we only do one check for the entire storage per request
176
+        if ($this->updateChecked) {
177
+            return false;
178
+        }
179
+        $this->updateChecked = true;
180
+        try {
181
+            return parent::hasUpdated('', $time);
182
+        } catch (StorageInvalidException $e) {
183
+            // check if it needs to be removed
184
+            $this->checkStorageAvailability();
185
+            throw $e;
186
+        } catch (StorageNotAvailableException $e) {
187
+            // check if it needs to be removed or just temp unavailable
188
+            $this->checkStorageAvailability();
189
+            throw $e;
190
+        }
191
+    }
192
+
193
+    public function test() {
194
+        try {
195
+            return parent::test();
196
+        } catch (StorageInvalidException $e) {
197
+            // check if it needs to be removed
198
+            $this->checkStorageAvailability();
199
+            throw $e;
200
+        } catch (StorageNotAvailableException $e) {
201
+            // check if it needs to be removed or just temp unavailable
202
+            $this->checkStorageAvailability();
203
+            throw $e;
204
+        }
205
+    }
206
+
207
+    /**
208
+     * Check whether this storage is permanently or temporarily
209
+     * unavailable
210
+     *
211
+     * @throws \OCP\Files\StorageNotAvailableException
212
+     * @throws \OCP\Files\StorageInvalidException
213
+     */
214
+    public function checkStorageAvailability() {
215
+        // see if we can find out why the share is unavailable
216
+        try {
217
+            $this->getShareInfo();
218
+        } catch (NotFoundException $e) {
219
+            // a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
220
+            if ($this->testRemote()) {
221
+                // valid Nextcloud instance means that the public share no longer exists
222
+                // since this is permanent (re-sharing the file will create a new token)
223
+                // we remove the invalid storage
224
+                $this->manager->removeShare($this->mountPoint);
225
+                $this->manager->getMountManager()->removeMount($this->mountPoint);
226
+                throw new StorageInvalidException("Remote share not found", 0, $e);
227
+            } else {
228
+                // Nextcloud instance is gone, likely to be a temporary server configuration error
229
+                throw new StorageNotAvailableException("No nextcloud instance found at remote", 0, $e);
230
+            }
231
+        } catch (ForbiddenException $e) {
232
+            // auth error, remove share for now (provide a dialog in the future)
233
+            $this->manager->removeShare($this->mountPoint);
234
+            $this->manager->getMountManager()->removeMount($this->mountPoint);
235
+            throw new StorageInvalidException("Auth error when getting remote share");
236
+        } catch (\GuzzleHttp\Exception\ConnectException $e) {
237
+            throw new StorageNotAvailableException("Failed to connect to remote instance", 0, $e);
238
+        } catch (\GuzzleHttp\Exception\RequestException $e) {
239
+            throw new StorageNotAvailableException("Error while sending request to remote instance", 0, $e);
240
+        }
241
+    }
242
+
243
+    public function file_exists($path) {
244
+        if ($path === '') {
245
+            return true;
246
+        } else {
247
+            return parent::file_exists($path);
248
+        }
249
+    }
250
+
251
+    /**
252
+     * Check if the configured remote is a valid federated share provider
253
+     *
254
+     * @return bool
255
+     */
256
+    protected function testRemote(): bool {
257
+        try {
258
+            return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
259
+                || $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
260
+                || $this->testRemoteUrl($this->getRemote() . '/status.php');
261
+        } catch (\Exception $e) {
262
+            return false;
263
+        }
264
+    }
265
+
266
+    private function testRemoteUrl(string $url): bool {
267
+        $cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url');
268
+        if ($cache->hasKey($url)) {
269
+            return (bool)$cache->get($url);
270
+        }
271
+
272
+        $client = $this->httpClient->newClient();
273
+        try {
274
+            $result = $client->get($url, [
275
+                'timeout' => 10,
276
+                'connect_timeout' => 10,
277
+            ])->getBody();
278
+            $data = json_decode($result);
279
+            $returnValue = (is_object($data) && !empty($data->version));
280
+        } catch (ConnectException $e) {
281
+            $returnValue = false;
282
+        } catch (ClientException $e) {
283
+            $returnValue = false;
284
+        } catch (RequestException $e) {
285
+            $returnValue = false;
286
+        }
287
+
288
+        $cache->set($url, $returnValue, 60 * 60 * 24);
289
+        return $returnValue;
290
+    }
291
+
292
+    /**
293
+     * Check whether the remote is an ownCloud/Nextcloud. This is needed since some sharing
294
+     * features are not standardized.
295
+     *
296
+     * @throws LocalServerException
297
+     */
298
+    public function remoteIsOwnCloud(): bool {
299
+        if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
300
+            return false;
301
+        }
302
+        return true;
303
+    }
304
+
305
+    /**
306
+     * @return mixed
307
+     * @throws ForbiddenException
308
+     * @throws NotFoundException
309
+     * @throws \Exception
310
+     */
311
+    public function getShareInfo() {
312
+        $remote = $this->getRemote();
313
+        $token = $this->getToken();
314
+        $password = $this->getPassword();
315
+
316
+        try {
317
+            // If remote is not an ownCloud do not try to get any share info
318
+            if (!$this->remoteIsOwnCloud()) {
319
+                return ['status' => 'unsupported'];
320
+            }
321
+        } catch (LocalServerException $e) {
322
+            // throw this to be on the safe side: the share will still be visible
323
+            // in the UI in case the failure is intermittent, and the user will
324
+            // be able to decide whether to remove it if it's really gone
325
+            throw new StorageNotAvailableException();
326
+        }
327
+
328
+        $url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
329
+
330
+        // TODO: DI
331
+        $client = \OC::$server->getHTTPClientService()->newClient();
332
+        try {
333
+            $response = $client->post($url, [
334
+                'body' => ['password' => $password],
335
+                'timeout' => 10,
336
+                'connect_timeout' => 10,
337
+            ]);
338
+        } catch (\GuzzleHttp\Exception\RequestException $e) {
339
+            if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
340
+                throw new ForbiddenException();
341
+            }
342
+            if ($e->getCode() === Http::STATUS_NOT_FOUND) {
343
+                throw new NotFoundException();
344
+            }
345
+            // throw this to be on the safe side: the share will still be visible
346
+            // in the UI in case the failure is intermittent, and the user will
347
+            // be able to decide whether to remove it if it's really gone
348
+            throw new StorageNotAvailableException();
349
+        }
350
+
351
+        return json_decode($response->getBody(), true);
352
+    }
353
+
354
+    public function getOwner($path) {
355
+        return $this->cloudId->getDisplayId();
356
+    }
357
+
358
+    public function isSharable($path) {
359
+        if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
360
+            return false;
361
+        }
362
+        return ($this->getPermissions($path) & Constants::PERMISSION_SHARE);
363
+    }
364
+
365
+    public function getPermissions($path) {
366
+        $response = $this->propfind($path);
367
+        // old federated sharing permissions
368
+        if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
369
+            $permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
370
+        } elseif (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) {
371
+            // permissions provided by the OCM API
372
+            $permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions'], $path);
373
+        } elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
374
+            return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
375
+        } else {
376
+            // use default permission if remote server doesn't provide the share permissions
377
+            $permissions = $this->getDefaultPermissions($path);
378
+        }
379
+
380
+        return $permissions;
381
+    }
382
+
383
+    public function needsPartFile() {
384
+        return false;
385
+    }
386
+
387
+    /**
388
+     * Translate OCM Permissions to Nextcloud permissions
389
+     *
390
+     * @param string $ocmPermissions json encoded OCM permissions
391
+     * @param string $path path to file
392
+     * @return int
393
+     */
394
+    protected function ocmPermissions2ncPermissions(string $ocmPermissions, string $path): int {
395
+        try {
396
+            $ocmPermissions = json_decode($ocmPermissions);
397
+            $ncPermissions = 0;
398
+            foreach ($ocmPermissions as $permission) {
399
+                switch (strtolower($permission)) {
400
+                    case 'read':
401
+                        $ncPermissions += Constants::PERMISSION_READ;
402
+                        break;
403
+                    case 'write':
404
+                        $ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE;
405
+                        break;
406
+                    case 'share':
407
+                        $ncPermissions += Constants::PERMISSION_SHARE;
408
+                        break;
409
+                    default:
410
+                        throw new \Exception();
411
+                }
412
+            }
413
+        } catch (\Exception $e) {
414
+            $ncPermissions = $this->getDefaultPermissions($path);
415
+        }
416
+
417
+        return $ncPermissions;
418
+    }
419
+
420
+    /**
421
+     * Calculate the default permissions in case no permissions are provided
422
+     */
423
+    protected function getDefaultPermissions(string $path): int {
424
+        if ($this->is_dir($path)) {
425
+            $permissions = Constants::PERMISSION_ALL;
426
+        } else {
427
+            $permissions = Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE;
428
+        }
429
+
430
+        return $permissions;
431
+    }
432
+
433
+    public function free_space($path) {
434
+        return parent::free_space("");
435
+    }
436 436
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/DAV.php 1 patch
Indentation   +809 added lines, -809 removed lines patch added patch discarded remove patch
@@ -64,813 +64,813 @@
 block discarded – undo
64 64
  * @package OC\Files\Storage
65 65
  */
66 66
 class DAV extends Common {
67
-	/** @var string */
68
-	protected $password;
69
-	/** @var string */
70
-	protected $user;
71
-	/** @var string|null */
72
-	protected $authType;
73
-	/** @var string */
74
-	protected $host;
75
-	/** @var bool */
76
-	protected $secure;
77
-	/** @var string */
78
-	protected $root;
79
-	/** @var string */
80
-	protected $certPath;
81
-	/** @var bool */
82
-	protected $ready;
83
-	/** @var Client */
84
-	protected $client;
85
-	/** @var ArrayCache */
86
-	protected $statCache;
87
-	/** @var IClientService */
88
-	protected $httpClientService;
89
-	/** @var ICertificateManager */
90
-	protected $certManager;
91
-
92
-	/**
93
-	 * @param array $params
94
-	 * @throws \Exception
95
-	 */
96
-	public function __construct($params) {
97
-		$this->statCache = new ArrayCache();
98
-		$this->httpClientService = \OC::$server->getHTTPClientService();
99
-		if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
100
-			$host = $params['host'];
101
-			//remove leading http[s], will be generated in createBaseUri()
102
-			if (substr($host, 0, 8) == "https://") {
103
-				$host = substr($host, 8);
104
-			} elseif (substr($host, 0, 7) == "http://") {
105
-				$host = substr($host, 7);
106
-			}
107
-			$this->host = $host;
108
-			$this->user = $params['user'];
109
-			$this->password = $params['password'];
110
-			if (isset($params['authType'])) {
111
-				$this->authType = $params['authType'];
112
-			}
113
-			if (isset($params['secure'])) {
114
-				if (is_string($params['secure'])) {
115
-					$this->secure = ($params['secure'] === 'true');
116
-				} else {
117
-					$this->secure = (bool)$params['secure'];
118
-				}
119
-			} else {
120
-				$this->secure = false;
121
-			}
122
-			if ($this->secure === true) {
123
-				// inject mock for testing
124
-				$this->certManager = \OC::$server->getCertificateManager();
125
-			}
126
-			$this->root = $params['root'] ?? '/';
127
-			$this->root = '/' . ltrim($this->root, '/');
128
-			$this->root = rtrim($this->root, '/') . '/';
129
-		} else {
130
-			throw new \Exception('Invalid webdav storage configuration');
131
-		}
132
-	}
133
-
134
-	protected function init() {
135
-		if ($this->ready) {
136
-			return;
137
-		}
138
-		$this->ready = true;
139
-
140
-		$settings = [
141
-			'baseUri' => $this->createBaseUri(),
142
-			'userName' => $this->user,
143
-			'password' => $this->password,
144
-		];
145
-		if ($this->authType !== null) {
146
-			$settings['authType'] = $this->authType;
147
-		}
148
-
149
-		$proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
150
-		if ($proxy !== '') {
151
-			$settings['proxy'] = $proxy;
152
-		}
153
-
154
-		$this->client = new Client($settings);
155
-		$this->client->setThrowExceptions(true);
156
-
157
-		if ($this->secure === true) {
158
-			$certPath = $this->certManager->getAbsoluteBundlePath();
159
-			if (file_exists($certPath)) {
160
-				$this->certPath = $certPath;
161
-			}
162
-			if ($this->certPath) {
163
-				$this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
164
-			}
165
-		}
166
-	}
167
-
168
-	/**
169
-	 * Clear the stat cache
170
-	 */
171
-	public function clearStatCache() {
172
-		$this->statCache->clear();
173
-	}
174
-
175
-	/** {@inheritdoc} */
176
-	public function getId() {
177
-		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
178
-	}
179
-
180
-	/** {@inheritdoc} */
181
-	public function createBaseUri() {
182
-		$baseUri = 'http';
183
-		if ($this->secure) {
184
-			$baseUri .= 's';
185
-		}
186
-		$baseUri .= '://' . $this->host . $this->root;
187
-		return $baseUri;
188
-	}
189
-
190
-	/** {@inheritdoc} */
191
-	public function mkdir($path) {
192
-		$this->init();
193
-		$path = $this->cleanPath($path);
194
-		$result = $this->simpleResponse('MKCOL', $path, null, 201);
195
-		if ($result) {
196
-			$this->statCache->set($path, true);
197
-		}
198
-		return $result;
199
-	}
200
-
201
-	/** {@inheritdoc} */
202
-	public function rmdir($path) {
203
-		$this->init();
204
-		$path = $this->cleanPath($path);
205
-		// FIXME: some WebDAV impl return 403 when trying to DELETE
206
-		// a non-empty folder
207
-		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
208
-		$this->statCache->clear($path . '/');
209
-		$this->statCache->remove($path);
210
-		return $result;
211
-	}
212
-
213
-	/** {@inheritdoc} */
214
-	public function opendir($path) {
215
-		$this->init();
216
-		$path = $this->cleanPath($path);
217
-		try {
218
-			$response = $this->client->propFind(
219
-				$this->encodePath($path),
220
-				['{DAV:}getetag'],
221
-				1
222
-			);
223
-			if ($response === false) {
224
-				return false;
225
-			}
226
-			$content = [];
227
-			$files = array_keys($response);
228
-			array_shift($files); //the first entry is the current directory
229
-
230
-			if (!$this->statCache->hasKey($path)) {
231
-				$this->statCache->set($path, true);
232
-			}
233
-			foreach ($files as $file) {
234
-				$file = urldecode($file);
235
-				// do not store the real entry, we might not have all properties
236
-				if (!$this->statCache->hasKey($path)) {
237
-					$this->statCache->set($file, true);
238
-				}
239
-				$file = basename($file);
240
-				$content[] = $file;
241
-			}
242
-			return IteratorDirectory::wrap($content);
243
-		} catch (\Exception $e) {
244
-			$this->convertException($e, $path);
245
-		}
246
-		return false;
247
-	}
248
-
249
-	/**
250
-	 * Propfind call with cache handling.
251
-	 *
252
-	 * First checks if information is cached.
253
-	 * If not, request it from the server then store to cache.
254
-	 *
255
-	 * @param string $path path to propfind
256
-	 *
257
-	 * @return array|boolean propfind response or false if the entry was not found
258
-	 *
259
-	 * @throws ClientHttpException
260
-	 */
261
-	protected function propfind($path) {
262
-		$path = $this->cleanPath($path);
263
-		$cachedResponse = $this->statCache->get($path);
264
-		// we either don't know it, or we know it exists but need more details
265
-		if (is_null($cachedResponse) || $cachedResponse === true) {
266
-			$this->init();
267
-			try {
268
-				$response = $this->client->propFind(
269
-					$this->encodePath($path),
270
-					[
271
-						'{DAV:}getlastmodified',
272
-						'{DAV:}getcontentlength',
273
-						'{DAV:}getcontenttype',
274
-						'{http://owncloud.org/ns}permissions',
275
-						'{http://open-collaboration-services.org/ns}share-permissions',
276
-						'{DAV:}resourcetype',
277
-						'{DAV:}getetag',
278
-						'{DAV:}quota-available-bytes',
279
-					]
280
-				);
281
-				$this->statCache->set($path, $response);
282
-			} catch (ClientHttpException $e) {
283
-				if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
284
-					$this->statCache->clear($path . '/');
285
-					$this->statCache->set($path, false);
286
-					return false;
287
-				}
288
-				$this->convertException($e, $path);
289
-			} catch (\Exception $e) {
290
-				$this->convertException($e, $path);
291
-			}
292
-		} else {
293
-			$response = $cachedResponse;
294
-		}
295
-		return $response;
296
-	}
297
-
298
-	/** {@inheritdoc} */
299
-	public function filetype($path) {
300
-		try {
301
-			$response = $this->propfind($path);
302
-			if ($response === false) {
303
-				return false;
304
-			}
305
-			$responseType = [];
306
-			if (isset($response["{DAV:}resourcetype"])) {
307
-				/** @var ResourceType[] $response */
308
-				$responseType = $response["{DAV:}resourcetype"]->getValue();
309
-			}
310
-			return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
311
-		} catch (\Exception $e) {
312
-			$this->convertException($e, $path);
313
-		}
314
-		return false;
315
-	}
316
-
317
-	/** {@inheritdoc} */
318
-	public function file_exists($path) {
319
-		try {
320
-			$path = $this->cleanPath($path);
321
-			$cachedState = $this->statCache->get($path);
322
-			if ($cachedState === false) {
323
-				// we know the file doesn't exist
324
-				return false;
325
-			} elseif (!is_null($cachedState)) {
326
-				return true;
327
-			}
328
-			// need to get from server
329
-			return ($this->propfind($path) !== false);
330
-		} catch (\Exception $e) {
331
-			$this->convertException($e, $path);
332
-		}
333
-		return false;
334
-	}
335
-
336
-	/** {@inheritdoc} */
337
-	public function unlink($path) {
338
-		$this->init();
339
-		$path = $this->cleanPath($path);
340
-		$result = $this->simpleResponse('DELETE', $path, null, 204);
341
-		$this->statCache->clear($path . '/');
342
-		$this->statCache->remove($path);
343
-		return $result;
344
-	}
345
-
346
-	/** {@inheritdoc} */
347
-	public function fopen($path, $mode) {
348
-		$this->init();
349
-		$path = $this->cleanPath($path);
350
-		switch ($mode) {
351
-			case 'r':
352
-			case 'rb':
353
-				try {
354
-					$response = $this->httpClientService
355
-						->newClient()
356
-						->get($this->createBaseUri() . $this->encodePath($path), [
357
-							'auth' => [$this->user, $this->password],
358
-							'stream' => true
359
-						]);
360
-				} catch (\GuzzleHttp\Exception\ClientException $e) {
361
-					if ($e->getResponse() instanceof ResponseInterface
362
-						&& $e->getResponse()->getStatusCode() === 404) {
363
-						return false;
364
-					} else {
365
-						throw $e;
366
-					}
367
-				}
368
-
369
-				if ($response->getStatusCode() !== Http::STATUS_OK) {
370
-					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
371
-						throw new \OCP\Lock\LockedException($path);
372
-					} else {
373
-						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);
374
-					}
375
-				}
376
-
377
-				return $response->getBody();
378
-			case 'w':
379
-			case 'wb':
380
-			case 'a':
381
-			case 'ab':
382
-			case 'r+':
383
-			case 'w+':
384
-			case 'wb+':
385
-			case 'a+':
386
-			case 'x':
387
-			case 'x+':
388
-			case 'c':
389
-			case 'c+':
390
-				//emulate these
391
-				$tempManager = \OC::$server->getTempManager();
392
-				if (strrpos($path, '.') !== false) {
393
-					$ext = substr($path, strrpos($path, '.'));
394
-				} else {
395
-					$ext = '';
396
-				}
397
-				if ($this->file_exists($path)) {
398
-					if (!$this->isUpdatable($path)) {
399
-						return false;
400
-					}
401
-					if ($mode === 'w' or $mode === 'w+') {
402
-						$tmpFile = $tempManager->getTemporaryFile($ext);
403
-					} else {
404
-						$tmpFile = $this->getCachedFile($path);
405
-					}
406
-				} else {
407
-					if (!$this->isCreatable(dirname($path))) {
408
-						return false;
409
-					}
410
-					$tmpFile = $tempManager->getTemporaryFile($ext);
411
-				}
412
-				$handle = fopen($tmpFile, $mode);
413
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
414
-					$this->writeBack($tmpFile, $path);
415
-				});
416
-		}
417
-	}
418
-
419
-	/**
420
-	 * @param string $tmpFile
421
-	 */
422
-	public function writeBack($tmpFile, $path) {
423
-		$this->uploadFile($tmpFile, $path);
424
-		unlink($tmpFile);
425
-	}
426
-
427
-	/** {@inheritdoc} */
428
-	public function free_space($path) {
429
-		$this->init();
430
-		$path = $this->cleanPath($path);
431
-		try {
432
-			$response = $this->propfind($path);
433
-			if ($response === false) {
434
-				return FileInfo::SPACE_UNKNOWN;
435
-			}
436
-			if (isset($response['{DAV:}quota-available-bytes'])) {
437
-				return (int)$response['{DAV:}quota-available-bytes'];
438
-			} else {
439
-				return FileInfo::SPACE_UNKNOWN;
440
-			}
441
-		} catch (\Exception $e) {
442
-			return FileInfo::SPACE_UNKNOWN;
443
-		}
444
-	}
445
-
446
-	/** {@inheritdoc} */
447
-	public function touch($path, $mtime = null) {
448
-		$this->init();
449
-		if (is_null($mtime)) {
450
-			$mtime = time();
451
-		}
452
-		$path = $this->cleanPath($path);
453
-
454
-		// if file exists, update the mtime, else create a new empty file
455
-		if ($this->file_exists($path)) {
456
-			try {
457
-				$this->statCache->remove($path);
458
-				$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
459
-				// non-owncloud clients might not have accepted the property, need to recheck it
460
-				$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
461
-				if ($response === false) {
462
-					return false;
463
-				}
464
-				if (isset($response['{DAV:}getlastmodified'])) {
465
-					$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
466
-					if ($remoteMtime !== $mtime) {
467
-						// server has not accepted the mtime
468
-						return false;
469
-					}
470
-				}
471
-			} catch (ClientHttpException $e) {
472
-				if ($e->getHttpStatus() === 501) {
473
-					return false;
474
-				}
475
-				$this->convertException($e, $path);
476
-				return false;
477
-			} catch (\Exception $e) {
478
-				$this->convertException($e, $path);
479
-				return false;
480
-			}
481
-		} else {
482
-			$this->file_put_contents($path, '');
483
-		}
484
-		return true;
485
-	}
486
-
487
-	/**
488
-	 * @param string $path
489
-	 * @param mixed $data
490
-	 * @return int|false
491
-	 */
492
-	public function file_put_contents($path, $data) {
493
-		$path = $this->cleanPath($path);
494
-		$result = parent::file_put_contents($path, $data);
495
-		$this->statCache->remove($path);
496
-		return $result;
497
-	}
498
-
499
-	/**
500
-	 * @param string $path
501
-	 * @param string $target
502
-	 */
503
-	protected function uploadFile($path, $target) {
504
-		$this->init();
505
-
506
-		// invalidate
507
-		$target = $this->cleanPath($target);
508
-		$this->statCache->remove($target);
509
-		$source = fopen($path, 'r');
510
-
511
-		$this->httpClientService
512
-			->newClient()
513
-			->put($this->createBaseUri() . $this->encodePath($target), [
514
-				'body' => $source,
515
-				'auth' => [$this->user, $this->password]
516
-			]);
517
-
518
-		$this->removeCachedFile($target);
519
-	}
520
-
521
-	/** {@inheritdoc} */
522
-	public function rename($path1, $path2) {
523
-		$this->init();
524
-		$path1 = $this->cleanPath($path1);
525
-		$path2 = $this->cleanPath($path2);
526
-		try {
527
-			// overwrite directory ?
528
-			if ($this->is_dir($path2)) {
529
-				// needs trailing slash in destination
530
-				$path2 = rtrim($path2, '/') . '/';
531
-			}
532
-			$this->client->request(
533
-				'MOVE',
534
-				$this->encodePath($path1),
535
-				null,
536
-				[
537
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
538
-				]
539
-			);
540
-			$this->statCache->clear($path1 . '/');
541
-			$this->statCache->clear($path2 . '/');
542
-			$this->statCache->set($path1, false);
543
-			$this->statCache->set($path2, true);
544
-			$this->removeCachedFile($path1);
545
-			$this->removeCachedFile($path2);
546
-			return true;
547
-		} catch (\Exception $e) {
548
-			$this->convertException($e);
549
-		}
550
-		return false;
551
-	}
552
-
553
-	/** {@inheritdoc} */
554
-	public function copy($path1, $path2) {
555
-		$this->init();
556
-		$path1 = $this->cleanPath($path1);
557
-		$path2 = $this->cleanPath($path2);
558
-		try {
559
-			// overwrite directory ?
560
-			if ($this->is_dir($path2)) {
561
-				// needs trailing slash in destination
562
-				$path2 = rtrim($path2, '/') . '/';
563
-			}
564
-			$this->client->request(
565
-				'COPY',
566
-				$this->encodePath($path1),
567
-				null,
568
-				[
569
-					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
570
-				]
571
-			);
572
-			$this->statCache->clear($path2 . '/');
573
-			$this->statCache->set($path2, true);
574
-			$this->removeCachedFile($path2);
575
-			return true;
576
-		} catch (\Exception $e) {
577
-			$this->convertException($e);
578
-		}
579
-		return false;
580
-	}
581
-
582
-	/** {@inheritdoc} */
583
-	public function stat($path) {
584
-		try {
585
-			$response = $this->propfind($path);
586
-			if (!$response) {
587
-				return false;
588
-			}
589
-			return [
590
-				'mtime' => isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null,
591
-				'size' => (int)($response['{DAV:}getcontentlength'] ?? 0),
592
-			];
593
-		} catch (\Exception $e) {
594
-			$this->convertException($e, $path);
595
-		}
596
-		return [];
597
-	}
598
-
599
-	/** {@inheritdoc} */
600
-	public function getMimeType($path) {
601
-		$remoteMimetype = $this->getMimeTypeFromRemote($path);
602
-		if ($remoteMimetype === 'application/octet-stream') {
603
-			return \OC::$server->getMimeTypeDetector()->detectPath($path);
604
-		} else {
605
-			return $remoteMimetype;
606
-		}
607
-	}
608
-
609
-	public function getMimeTypeFromRemote($path) {
610
-		try {
611
-			$response = $this->propfind($path);
612
-			if ($response === false) {
613
-				return false;
614
-			}
615
-			$responseType = [];
616
-			if (isset($response["{DAV:}resourcetype"])) {
617
-				/** @var ResourceType[] $response */
618
-				$responseType = $response["{DAV:}resourcetype"]->getValue();
619
-			}
620
-			$type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
621
-			if ($type == 'dir') {
622
-				return 'httpd/unix-directory';
623
-			} elseif (isset($response['{DAV:}getcontenttype'])) {
624
-				return $response['{DAV:}getcontenttype'];
625
-			} else {
626
-				return 'application/octet-stream';
627
-			}
628
-		} catch (\Exception $e) {
629
-			return false;
630
-		}
631
-	}
632
-
633
-	/**
634
-	 * @param string $path
635
-	 * @return string
636
-	 */
637
-	public function cleanPath($path) {
638
-		if ($path === '') {
639
-			return $path;
640
-		}
641
-		$path = Filesystem::normalizePath($path);
642
-		// remove leading slash
643
-		return substr($path, 1);
644
-	}
645
-
646
-	/**
647
-	 * URL encodes the given path but keeps the slashes
648
-	 *
649
-	 * @param string $path to encode
650
-	 * @return string encoded path
651
-	 */
652
-	protected function encodePath($path) {
653
-		// slashes need to stay
654
-		return str_replace('%2F', '/', rawurlencode($path));
655
-	}
656
-
657
-	/**
658
-	 * @param string $method
659
-	 * @param string $path
660
-	 * @param string|resource|null $body
661
-	 * @param int $expected
662
-	 * @return bool
663
-	 * @throws StorageInvalidException
664
-	 * @throws StorageNotAvailableException
665
-	 */
666
-	protected function simpleResponse($method, $path, $body, $expected) {
667
-		$path = $this->cleanPath($path);
668
-		try {
669
-			$response = $this->client->request($method, $this->encodePath($path), $body);
670
-			return $response['statusCode'] == $expected;
671
-		} catch (ClientHttpException $e) {
672
-			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
673
-				$this->statCache->clear($path . '/');
674
-				$this->statCache->set($path, false);
675
-				return false;
676
-			}
677
-
678
-			$this->convertException($e, $path);
679
-		} catch (\Exception $e) {
680
-			$this->convertException($e, $path);
681
-		}
682
-		return false;
683
-	}
684
-
685
-	/**
686
-	 * check if curl is installed
687
-	 */
688
-	public static function checkDependencies() {
689
-		return true;
690
-	}
691
-
692
-	/** {@inheritdoc} */
693
-	public function isUpdatable($path) {
694
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
695
-	}
696
-
697
-	/** {@inheritdoc} */
698
-	public function isCreatable($path) {
699
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
700
-	}
701
-
702
-	/** {@inheritdoc} */
703
-	public function isSharable($path) {
704
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
705
-	}
706
-
707
-	/** {@inheritdoc} */
708
-	public function isDeletable($path) {
709
-		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
710
-	}
711
-
712
-	/** {@inheritdoc} */
713
-	public function getPermissions($path) {
714
-		$this->init();
715
-		$path = $this->cleanPath($path);
716
-		$response = $this->propfind($path);
717
-		if ($response === false) {
718
-			return 0;
719
-		}
720
-		if (isset($response['{http://owncloud.org/ns}permissions'])) {
721
-			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
722
-		} elseif ($this->is_dir($path)) {
723
-			return Constants::PERMISSION_ALL;
724
-		} elseif ($this->file_exists($path)) {
725
-			return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
726
-		} else {
727
-			return 0;
728
-		}
729
-	}
730
-
731
-	/** {@inheritdoc} */
732
-	public function getETag($path) {
733
-		$this->init();
734
-		$path = $this->cleanPath($path);
735
-		$response = $this->propfind($path);
736
-		if ($response === false) {
737
-			return null;
738
-		}
739
-		if (isset($response['{DAV:}getetag'])) {
740
-			$etag = trim($response['{DAV:}getetag'], '"');
741
-			if (strlen($etag) > 40) {
742
-				$etag = md5($etag);
743
-			}
744
-			return $etag;
745
-		}
746
-		return parent::getEtag($path);
747
-	}
748
-
749
-	/**
750
-	 * @param string $permissionsString
751
-	 * @return int
752
-	 */
753
-	protected function parsePermissions($permissionsString) {
754
-		$permissions = Constants::PERMISSION_READ;
755
-		if (strpos($permissionsString, 'R') !== false) {
756
-			$permissions |= Constants::PERMISSION_SHARE;
757
-		}
758
-		if (strpos($permissionsString, 'D') !== false) {
759
-			$permissions |= Constants::PERMISSION_DELETE;
760
-		}
761
-		if (strpos($permissionsString, 'W') !== false) {
762
-			$permissions |= Constants::PERMISSION_UPDATE;
763
-		}
764
-		if (strpos($permissionsString, 'CK') !== false) {
765
-			$permissions |= Constants::PERMISSION_CREATE;
766
-			$permissions |= Constants::PERMISSION_UPDATE;
767
-		}
768
-		return $permissions;
769
-	}
770
-
771
-	/**
772
-	 * check if a file or folder has been updated since $time
773
-	 *
774
-	 * @param string $path
775
-	 * @param int $time
776
-	 * @throws \OCP\Files\StorageNotAvailableException
777
-	 * @return bool
778
-	 */
779
-	public function hasUpdated($path, $time) {
780
-		$this->init();
781
-		$path = $this->cleanPath($path);
782
-		try {
783
-			// force refresh for $path
784
-			$this->statCache->remove($path);
785
-			$response = $this->propfind($path);
786
-			if ($response === false) {
787
-				if ($path === '') {
788
-					// if root is gone it means the storage is not available
789
-					throw new StorageNotAvailableException('root is gone');
790
-				}
791
-				return false;
792
-			}
793
-			if (isset($response['{DAV:}getetag'])) {
794
-				$cachedData = $this->getCache()->get($path);
795
-				$etag = trim($response['{DAV:}getetag'], '"');
796
-				if (($cachedData === false) || (!empty($etag) && ($cachedData['etag'] !== $etag))) {
797
-					return true;
798
-				} elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
799
-					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
800
-					return $sharePermissions !== $cachedData['permissions'];
801
-				} elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
802
-					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
803
-					return $permissions !== $cachedData['permissions'];
804
-				} else {
805
-					return false;
806
-				}
807
-			} elseif (isset($response['{DAV:}getlastmodified'])) {
808
-				$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
809
-				return $remoteMtime > $time;
810
-			} else {
811
-				// neither `getetag` nor `getlastmodified` is set
812
-				return false;
813
-			}
814
-		} catch (ClientHttpException $e) {
815
-			if ($e->getHttpStatus() === 405) {
816
-				if ($path === '') {
817
-					// if root is gone it means the storage is not available
818
-					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
819
-				}
820
-				return false;
821
-			}
822
-			$this->convertException($e, $path);
823
-			return false;
824
-		} catch (\Exception $e) {
825
-			$this->convertException($e, $path);
826
-			return false;
827
-		}
828
-	}
829
-
830
-	/**
831
-	 * Interpret the given exception and decide whether it is due to an
832
-	 * unavailable storage, invalid storage or other.
833
-	 * This will either throw StorageInvalidException, StorageNotAvailableException
834
-	 * or do nothing.
835
-	 *
836
-	 * @param Exception $e sabre exception
837
-	 * @param string $path optional path from the operation
838
-	 *
839
-	 * @throws StorageInvalidException if the storage is invalid, for example
840
-	 * when the authentication expired or is invalid
841
-	 * @throws StorageNotAvailableException if the storage is not available,
842
-	 * which might be temporary
843
-	 * @throws ForbiddenException if the action is not allowed
844
-	 */
845
-	protected function convertException(Exception $e, $path = '') {
846
-		\OC::$server->getLogger()->logException($e, ['app' => 'files_external', 'level' => ILogger::DEBUG]);
847
-		if ($e instanceof ClientHttpException) {
848
-			if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
849
-				throw new \OCP\Lock\LockedException($path);
850
-			}
851
-			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
852
-				// either password was changed or was invalid all along
853
-				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
854
-			} elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
855
-				// ignore exception for MethodNotAllowed, false will be returned
856
-				return;
857
-			} elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
858
-				// The operation is forbidden. Fail somewhat gracefully
859
-				throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
860
-			}
861
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
862
-		} elseif ($e instanceof ClientException) {
863
-			// connection timeout or refused, server could be temporarily down
864
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
865
-		} elseif ($e instanceof \InvalidArgumentException) {
866
-			// parse error because the server returned HTML instead of XML,
867
-			// possibly temporarily down
868
-			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
869
-		} elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
870
-			// rethrow
871
-			throw $e;
872
-		}
873
-
874
-		// TODO: only log for now, but in the future need to wrap/rethrow exception
875
-	}
67
+    /** @var string */
68
+    protected $password;
69
+    /** @var string */
70
+    protected $user;
71
+    /** @var string|null */
72
+    protected $authType;
73
+    /** @var string */
74
+    protected $host;
75
+    /** @var bool */
76
+    protected $secure;
77
+    /** @var string */
78
+    protected $root;
79
+    /** @var string */
80
+    protected $certPath;
81
+    /** @var bool */
82
+    protected $ready;
83
+    /** @var Client */
84
+    protected $client;
85
+    /** @var ArrayCache */
86
+    protected $statCache;
87
+    /** @var IClientService */
88
+    protected $httpClientService;
89
+    /** @var ICertificateManager */
90
+    protected $certManager;
91
+
92
+    /**
93
+     * @param array $params
94
+     * @throws \Exception
95
+     */
96
+    public function __construct($params) {
97
+        $this->statCache = new ArrayCache();
98
+        $this->httpClientService = \OC::$server->getHTTPClientService();
99
+        if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
100
+            $host = $params['host'];
101
+            //remove leading http[s], will be generated in createBaseUri()
102
+            if (substr($host, 0, 8) == "https://") {
103
+                $host = substr($host, 8);
104
+            } elseif (substr($host, 0, 7) == "http://") {
105
+                $host = substr($host, 7);
106
+            }
107
+            $this->host = $host;
108
+            $this->user = $params['user'];
109
+            $this->password = $params['password'];
110
+            if (isset($params['authType'])) {
111
+                $this->authType = $params['authType'];
112
+            }
113
+            if (isset($params['secure'])) {
114
+                if (is_string($params['secure'])) {
115
+                    $this->secure = ($params['secure'] === 'true');
116
+                } else {
117
+                    $this->secure = (bool)$params['secure'];
118
+                }
119
+            } else {
120
+                $this->secure = false;
121
+            }
122
+            if ($this->secure === true) {
123
+                // inject mock for testing
124
+                $this->certManager = \OC::$server->getCertificateManager();
125
+            }
126
+            $this->root = $params['root'] ?? '/';
127
+            $this->root = '/' . ltrim($this->root, '/');
128
+            $this->root = rtrim($this->root, '/') . '/';
129
+        } else {
130
+            throw new \Exception('Invalid webdav storage configuration');
131
+        }
132
+    }
133
+
134
+    protected function init() {
135
+        if ($this->ready) {
136
+            return;
137
+        }
138
+        $this->ready = true;
139
+
140
+        $settings = [
141
+            'baseUri' => $this->createBaseUri(),
142
+            'userName' => $this->user,
143
+            'password' => $this->password,
144
+        ];
145
+        if ($this->authType !== null) {
146
+            $settings['authType'] = $this->authType;
147
+        }
148
+
149
+        $proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
150
+        if ($proxy !== '') {
151
+            $settings['proxy'] = $proxy;
152
+        }
153
+
154
+        $this->client = new Client($settings);
155
+        $this->client->setThrowExceptions(true);
156
+
157
+        if ($this->secure === true) {
158
+            $certPath = $this->certManager->getAbsoluteBundlePath();
159
+            if (file_exists($certPath)) {
160
+                $this->certPath = $certPath;
161
+            }
162
+            if ($this->certPath) {
163
+                $this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
164
+            }
165
+        }
166
+    }
167
+
168
+    /**
169
+     * Clear the stat cache
170
+     */
171
+    public function clearStatCache() {
172
+        $this->statCache->clear();
173
+    }
174
+
175
+    /** {@inheritdoc} */
176
+    public function getId() {
177
+        return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
178
+    }
179
+
180
+    /** {@inheritdoc} */
181
+    public function createBaseUri() {
182
+        $baseUri = 'http';
183
+        if ($this->secure) {
184
+            $baseUri .= 's';
185
+        }
186
+        $baseUri .= '://' . $this->host . $this->root;
187
+        return $baseUri;
188
+    }
189
+
190
+    /** {@inheritdoc} */
191
+    public function mkdir($path) {
192
+        $this->init();
193
+        $path = $this->cleanPath($path);
194
+        $result = $this->simpleResponse('MKCOL', $path, null, 201);
195
+        if ($result) {
196
+            $this->statCache->set($path, true);
197
+        }
198
+        return $result;
199
+    }
200
+
201
+    /** {@inheritdoc} */
202
+    public function rmdir($path) {
203
+        $this->init();
204
+        $path = $this->cleanPath($path);
205
+        // FIXME: some WebDAV impl return 403 when trying to DELETE
206
+        // a non-empty folder
207
+        $result = $this->simpleResponse('DELETE', $path . '/', null, 204);
208
+        $this->statCache->clear($path . '/');
209
+        $this->statCache->remove($path);
210
+        return $result;
211
+    }
212
+
213
+    /** {@inheritdoc} */
214
+    public function opendir($path) {
215
+        $this->init();
216
+        $path = $this->cleanPath($path);
217
+        try {
218
+            $response = $this->client->propFind(
219
+                $this->encodePath($path),
220
+                ['{DAV:}getetag'],
221
+                1
222
+            );
223
+            if ($response === false) {
224
+                return false;
225
+            }
226
+            $content = [];
227
+            $files = array_keys($response);
228
+            array_shift($files); //the first entry is the current directory
229
+
230
+            if (!$this->statCache->hasKey($path)) {
231
+                $this->statCache->set($path, true);
232
+            }
233
+            foreach ($files as $file) {
234
+                $file = urldecode($file);
235
+                // do not store the real entry, we might not have all properties
236
+                if (!$this->statCache->hasKey($path)) {
237
+                    $this->statCache->set($file, true);
238
+                }
239
+                $file = basename($file);
240
+                $content[] = $file;
241
+            }
242
+            return IteratorDirectory::wrap($content);
243
+        } catch (\Exception $e) {
244
+            $this->convertException($e, $path);
245
+        }
246
+        return false;
247
+    }
248
+
249
+    /**
250
+     * Propfind call with cache handling.
251
+     *
252
+     * First checks if information is cached.
253
+     * If not, request it from the server then store to cache.
254
+     *
255
+     * @param string $path path to propfind
256
+     *
257
+     * @return array|boolean propfind response or false if the entry was not found
258
+     *
259
+     * @throws ClientHttpException
260
+     */
261
+    protected function propfind($path) {
262
+        $path = $this->cleanPath($path);
263
+        $cachedResponse = $this->statCache->get($path);
264
+        // we either don't know it, or we know it exists but need more details
265
+        if (is_null($cachedResponse) || $cachedResponse === true) {
266
+            $this->init();
267
+            try {
268
+                $response = $this->client->propFind(
269
+                    $this->encodePath($path),
270
+                    [
271
+                        '{DAV:}getlastmodified',
272
+                        '{DAV:}getcontentlength',
273
+                        '{DAV:}getcontenttype',
274
+                        '{http://owncloud.org/ns}permissions',
275
+                        '{http://open-collaboration-services.org/ns}share-permissions',
276
+                        '{DAV:}resourcetype',
277
+                        '{DAV:}getetag',
278
+                        '{DAV:}quota-available-bytes',
279
+                    ]
280
+                );
281
+                $this->statCache->set($path, $response);
282
+            } catch (ClientHttpException $e) {
283
+                if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
284
+                    $this->statCache->clear($path . '/');
285
+                    $this->statCache->set($path, false);
286
+                    return false;
287
+                }
288
+                $this->convertException($e, $path);
289
+            } catch (\Exception $e) {
290
+                $this->convertException($e, $path);
291
+            }
292
+        } else {
293
+            $response = $cachedResponse;
294
+        }
295
+        return $response;
296
+    }
297
+
298
+    /** {@inheritdoc} */
299
+    public function filetype($path) {
300
+        try {
301
+            $response = $this->propfind($path);
302
+            if ($response === false) {
303
+                return false;
304
+            }
305
+            $responseType = [];
306
+            if (isset($response["{DAV:}resourcetype"])) {
307
+                /** @var ResourceType[] $response */
308
+                $responseType = $response["{DAV:}resourcetype"]->getValue();
309
+            }
310
+            return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
311
+        } catch (\Exception $e) {
312
+            $this->convertException($e, $path);
313
+        }
314
+        return false;
315
+    }
316
+
317
+    /** {@inheritdoc} */
318
+    public function file_exists($path) {
319
+        try {
320
+            $path = $this->cleanPath($path);
321
+            $cachedState = $this->statCache->get($path);
322
+            if ($cachedState === false) {
323
+                // we know the file doesn't exist
324
+                return false;
325
+            } elseif (!is_null($cachedState)) {
326
+                return true;
327
+            }
328
+            // need to get from server
329
+            return ($this->propfind($path) !== false);
330
+        } catch (\Exception $e) {
331
+            $this->convertException($e, $path);
332
+        }
333
+        return false;
334
+    }
335
+
336
+    /** {@inheritdoc} */
337
+    public function unlink($path) {
338
+        $this->init();
339
+        $path = $this->cleanPath($path);
340
+        $result = $this->simpleResponse('DELETE', $path, null, 204);
341
+        $this->statCache->clear($path . '/');
342
+        $this->statCache->remove($path);
343
+        return $result;
344
+    }
345
+
346
+    /** {@inheritdoc} */
347
+    public function fopen($path, $mode) {
348
+        $this->init();
349
+        $path = $this->cleanPath($path);
350
+        switch ($mode) {
351
+            case 'r':
352
+            case 'rb':
353
+                try {
354
+                    $response = $this->httpClientService
355
+                        ->newClient()
356
+                        ->get($this->createBaseUri() . $this->encodePath($path), [
357
+                            'auth' => [$this->user, $this->password],
358
+                            'stream' => true
359
+                        ]);
360
+                } catch (\GuzzleHttp\Exception\ClientException $e) {
361
+                    if ($e->getResponse() instanceof ResponseInterface
362
+                        && $e->getResponse()->getStatusCode() === 404) {
363
+                        return false;
364
+                    } else {
365
+                        throw $e;
366
+                    }
367
+                }
368
+
369
+                if ($response->getStatusCode() !== Http::STATUS_OK) {
370
+                    if ($response->getStatusCode() === Http::STATUS_LOCKED) {
371
+                        throw new \OCP\Lock\LockedException($path);
372
+                    } else {
373
+                        Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), ILogger::ERROR);
374
+                    }
375
+                }
376
+
377
+                return $response->getBody();
378
+            case 'w':
379
+            case 'wb':
380
+            case 'a':
381
+            case 'ab':
382
+            case 'r+':
383
+            case 'w+':
384
+            case 'wb+':
385
+            case 'a+':
386
+            case 'x':
387
+            case 'x+':
388
+            case 'c':
389
+            case 'c+':
390
+                //emulate these
391
+                $tempManager = \OC::$server->getTempManager();
392
+                if (strrpos($path, '.') !== false) {
393
+                    $ext = substr($path, strrpos($path, '.'));
394
+                } else {
395
+                    $ext = '';
396
+                }
397
+                if ($this->file_exists($path)) {
398
+                    if (!$this->isUpdatable($path)) {
399
+                        return false;
400
+                    }
401
+                    if ($mode === 'w' or $mode === 'w+') {
402
+                        $tmpFile = $tempManager->getTemporaryFile($ext);
403
+                    } else {
404
+                        $tmpFile = $this->getCachedFile($path);
405
+                    }
406
+                } else {
407
+                    if (!$this->isCreatable(dirname($path))) {
408
+                        return false;
409
+                    }
410
+                    $tmpFile = $tempManager->getTemporaryFile($ext);
411
+                }
412
+                $handle = fopen($tmpFile, $mode);
413
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
414
+                    $this->writeBack($tmpFile, $path);
415
+                });
416
+        }
417
+    }
418
+
419
+    /**
420
+     * @param string $tmpFile
421
+     */
422
+    public function writeBack($tmpFile, $path) {
423
+        $this->uploadFile($tmpFile, $path);
424
+        unlink($tmpFile);
425
+    }
426
+
427
+    /** {@inheritdoc} */
428
+    public function free_space($path) {
429
+        $this->init();
430
+        $path = $this->cleanPath($path);
431
+        try {
432
+            $response = $this->propfind($path);
433
+            if ($response === false) {
434
+                return FileInfo::SPACE_UNKNOWN;
435
+            }
436
+            if (isset($response['{DAV:}quota-available-bytes'])) {
437
+                return (int)$response['{DAV:}quota-available-bytes'];
438
+            } else {
439
+                return FileInfo::SPACE_UNKNOWN;
440
+            }
441
+        } catch (\Exception $e) {
442
+            return FileInfo::SPACE_UNKNOWN;
443
+        }
444
+    }
445
+
446
+    /** {@inheritdoc} */
447
+    public function touch($path, $mtime = null) {
448
+        $this->init();
449
+        if (is_null($mtime)) {
450
+            $mtime = time();
451
+        }
452
+        $path = $this->cleanPath($path);
453
+
454
+        // if file exists, update the mtime, else create a new empty file
455
+        if ($this->file_exists($path)) {
456
+            try {
457
+                $this->statCache->remove($path);
458
+                $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
459
+                // non-owncloud clients might not have accepted the property, need to recheck it
460
+                $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
461
+                if ($response === false) {
462
+                    return false;
463
+                }
464
+                if (isset($response['{DAV:}getlastmodified'])) {
465
+                    $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
466
+                    if ($remoteMtime !== $mtime) {
467
+                        // server has not accepted the mtime
468
+                        return false;
469
+                    }
470
+                }
471
+            } catch (ClientHttpException $e) {
472
+                if ($e->getHttpStatus() === 501) {
473
+                    return false;
474
+                }
475
+                $this->convertException($e, $path);
476
+                return false;
477
+            } catch (\Exception $e) {
478
+                $this->convertException($e, $path);
479
+                return false;
480
+            }
481
+        } else {
482
+            $this->file_put_contents($path, '');
483
+        }
484
+        return true;
485
+    }
486
+
487
+    /**
488
+     * @param string $path
489
+     * @param mixed $data
490
+     * @return int|false
491
+     */
492
+    public function file_put_contents($path, $data) {
493
+        $path = $this->cleanPath($path);
494
+        $result = parent::file_put_contents($path, $data);
495
+        $this->statCache->remove($path);
496
+        return $result;
497
+    }
498
+
499
+    /**
500
+     * @param string $path
501
+     * @param string $target
502
+     */
503
+    protected function uploadFile($path, $target) {
504
+        $this->init();
505
+
506
+        // invalidate
507
+        $target = $this->cleanPath($target);
508
+        $this->statCache->remove($target);
509
+        $source = fopen($path, 'r');
510
+
511
+        $this->httpClientService
512
+            ->newClient()
513
+            ->put($this->createBaseUri() . $this->encodePath($target), [
514
+                'body' => $source,
515
+                'auth' => [$this->user, $this->password]
516
+            ]);
517
+
518
+        $this->removeCachedFile($target);
519
+    }
520
+
521
+    /** {@inheritdoc} */
522
+    public function rename($path1, $path2) {
523
+        $this->init();
524
+        $path1 = $this->cleanPath($path1);
525
+        $path2 = $this->cleanPath($path2);
526
+        try {
527
+            // overwrite directory ?
528
+            if ($this->is_dir($path2)) {
529
+                // needs trailing slash in destination
530
+                $path2 = rtrim($path2, '/') . '/';
531
+            }
532
+            $this->client->request(
533
+                'MOVE',
534
+                $this->encodePath($path1),
535
+                null,
536
+                [
537
+                    'Destination' => $this->createBaseUri() . $this->encodePath($path2),
538
+                ]
539
+            );
540
+            $this->statCache->clear($path1 . '/');
541
+            $this->statCache->clear($path2 . '/');
542
+            $this->statCache->set($path1, false);
543
+            $this->statCache->set($path2, true);
544
+            $this->removeCachedFile($path1);
545
+            $this->removeCachedFile($path2);
546
+            return true;
547
+        } catch (\Exception $e) {
548
+            $this->convertException($e);
549
+        }
550
+        return false;
551
+    }
552
+
553
+    /** {@inheritdoc} */
554
+    public function copy($path1, $path2) {
555
+        $this->init();
556
+        $path1 = $this->cleanPath($path1);
557
+        $path2 = $this->cleanPath($path2);
558
+        try {
559
+            // overwrite directory ?
560
+            if ($this->is_dir($path2)) {
561
+                // needs trailing slash in destination
562
+                $path2 = rtrim($path2, '/') . '/';
563
+            }
564
+            $this->client->request(
565
+                'COPY',
566
+                $this->encodePath($path1),
567
+                null,
568
+                [
569
+                    'Destination' => $this->createBaseUri() . $this->encodePath($path2),
570
+                ]
571
+            );
572
+            $this->statCache->clear($path2 . '/');
573
+            $this->statCache->set($path2, true);
574
+            $this->removeCachedFile($path2);
575
+            return true;
576
+        } catch (\Exception $e) {
577
+            $this->convertException($e);
578
+        }
579
+        return false;
580
+    }
581
+
582
+    /** {@inheritdoc} */
583
+    public function stat($path) {
584
+        try {
585
+            $response = $this->propfind($path);
586
+            if (!$response) {
587
+                return false;
588
+            }
589
+            return [
590
+                'mtime' => isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null,
591
+                'size' => (int)($response['{DAV:}getcontentlength'] ?? 0),
592
+            ];
593
+        } catch (\Exception $e) {
594
+            $this->convertException($e, $path);
595
+        }
596
+        return [];
597
+    }
598
+
599
+    /** {@inheritdoc} */
600
+    public function getMimeType($path) {
601
+        $remoteMimetype = $this->getMimeTypeFromRemote($path);
602
+        if ($remoteMimetype === 'application/octet-stream') {
603
+            return \OC::$server->getMimeTypeDetector()->detectPath($path);
604
+        } else {
605
+            return $remoteMimetype;
606
+        }
607
+    }
608
+
609
+    public function getMimeTypeFromRemote($path) {
610
+        try {
611
+            $response = $this->propfind($path);
612
+            if ($response === false) {
613
+                return false;
614
+            }
615
+            $responseType = [];
616
+            if (isset($response["{DAV:}resourcetype"])) {
617
+                /** @var ResourceType[] $response */
618
+                $responseType = $response["{DAV:}resourcetype"]->getValue();
619
+            }
620
+            $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
621
+            if ($type == 'dir') {
622
+                return 'httpd/unix-directory';
623
+            } elseif (isset($response['{DAV:}getcontenttype'])) {
624
+                return $response['{DAV:}getcontenttype'];
625
+            } else {
626
+                return 'application/octet-stream';
627
+            }
628
+        } catch (\Exception $e) {
629
+            return false;
630
+        }
631
+    }
632
+
633
+    /**
634
+     * @param string $path
635
+     * @return string
636
+     */
637
+    public function cleanPath($path) {
638
+        if ($path === '') {
639
+            return $path;
640
+        }
641
+        $path = Filesystem::normalizePath($path);
642
+        // remove leading slash
643
+        return substr($path, 1);
644
+    }
645
+
646
+    /**
647
+     * URL encodes the given path but keeps the slashes
648
+     *
649
+     * @param string $path to encode
650
+     * @return string encoded path
651
+     */
652
+    protected function encodePath($path) {
653
+        // slashes need to stay
654
+        return str_replace('%2F', '/', rawurlencode($path));
655
+    }
656
+
657
+    /**
658
+     * @param string $method
659
+     * @param string $path
660
+     * @param string|resource|null $body
661
+     * @param int $expected
662
+     * @return bool
663
+     * @throws StorageInvalidException
664
+     * @throws StorageNotAvailableException
665
+     */
666
+    protected function simpleResponse($method, $path, $body, $expected) {
667
+        $path = $this->cleanPath($path);
668
+        try {
669
+            $response = $this->client->request($method, $this->encodePath($path), $body);
670
+            return $response['statusCode'] == $expected;
671
+        } catch (ClientHttpException $e) {
672
+            if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
673
+                $this->statCache->clear($path . '/');
674
+                $this->statCache->set($path, false);
675
+                return false;
676
+            }
677
+
678
+            $this->convertException($e, $path);
679
+        } catch (\Exception $e) {
680
+            $this->convertException($e, $path);
681
+        }
682
+        return false;
683
+    }
684
+
685
+    /**
686
+     * check if curl is installed
687
+     */
688
+    public static function checkDependencies() {
689
+        return true;
690
+    }
691
+
692
+    /** {@inheritdoc} */
693
+    public function isUpdatable($path) {
694
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
695
+    }
696
+
697
+    /** {@inheritdoc} */
698
+    public function isCreatable($path) {
699
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
700
+    }
701
+
702
+    /** {@inheritdoc} */
703
+    public function isSharable($path) {
704
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
705
+    }
706
+
707
+    /** {@inheritdoc} */
708
+    public function isDeletable($path) {
709
+        return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
710
+    }
711
+
712
+    /** {@inheritdoc} */
713
+    public function getPermissions($path) {
714
+        $this->init();
715
+        $path = $this->cleanPath($path);
716
+        $response = $this->propfind($path);
717
+        if ($response === false) {
718
+            return 0;
719
+        }
720
+        if (isset($response['{http://owncloud.org/ns}permissions'])) {
721
+            return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
722
+        } elseif ($this->is_dir($path)) {
723
+            return Constants::PERMISSION_ALL;
724
+        } elseif ($this->file_exists($path)) {
725
+            return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
726
+        } else {
727
+            return 0;
728
+        }
729
+    }
730
+
731
+    /** {@inheritdoc} */
732
+    public function getETag($path) {
733
+        $this->init();
734
+        $path = $this->cleanPath($path);
735
+        $response = $this->propfind($path);
736
+        if ($response === false) {
737
+            return null;
738
+        }
739
+        if (isset($response['{DAV:}getetag'])) {
740
+            $etag = trim($response['{DAV:}getetag'], '"');
741
+            if (strlen($etag) > 40) {
742
+                $etag = md5($etag);
743
+            }
744
+            return $etag;
745
+        }
746
+        return parent::getEtag($path);
747
+    }
748
+
749
+    /**
750
+     * @param string $permissionsString
751
+     * @return int
752
+     */
753
+    protected function parsePermissions($permissionsString) {
754
+        $permissions = Constants::PERMISSION_READ;
755
+        if (strpos($permissionsString, 'R') !== false) {
756
+            $permissions |= Constants::PERMISSION_SHARE;
757
+        }
758
+        if (strpos($permissionsString, 'D') !== false) {
759
+            $permissions |= Constants::PERMISSION_DELETE;
760
+        }
761
+        if (strpos($permissionsString, 'W') !== false) {
762
+            $permissions |= Constants::PERMISSION_UPDATE;
763
+        }
764
+        if (strpos($permissionsString, 'CK') !== false) {
765
+            $permissions |= Constants::PERMISSION_CREATE;
766
+            $permissions |= Constants::PERMISSION_UPDATE;
767
+        }
768
+        return $permissions;
769
+    }
770
+
771
+    /**
772
+     * check if a file or folder has been updated since $time
773
+     *
774
+     * @param string $path
775
+     * @param int $time
776
+     * @throws \OCP\Files\StorageNotAvailableException
777
+     * @return bool
778
+     */
779
+    public function hasUpdated($path, $time) {
780
+        $this->init();
781
+        $path = $this->cleanPath($path);
782
+        try {
783
+            // force refresh for $path
784
+            $this->statCache->remove($path);
785
+            $response = $this->propfind($path);
786
+            if ($response === false) {
787
+                if ($path === '') {
788
+                    // if root is gone it means the storage is not available
789
+                    throw new StorageNotAvailableException('root is gone');
790
+                }
791
+                return false;
792
+            }
793
+            if (isset($response['{DAV:}getetag'])) {
794
+                $cachedData = $this->getCache()->get($path);
795
+                $etag = trim($response['{DAV:}getetag'], '"');
796
+                if (($cachedData === false) || (!empty($etag) && ($cachedData['etag'] !== $etag))) {
797
+                    return true;
798
+                } elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
799
+                    $sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
800
+                    return $sharePermissions !== $cachedData['permissions'];
801
+                } elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
802
+                    $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
803
+                    return $permissions !== $cachedData['permissions'];
804
+                } else {
805
+                    return false;
806
+                }
807
+            } elseif (isset($response['{DAV:}getlastmodified'])) {
808
+                $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
809
+                return $remoteMtime > $time;
810
+            } else {
811
+                // neither `getetag` nor `getlastmodified` is set
812
+                return false;
813
+            }
814
+        } catch (ClientHttpException $e) {
815
+            if ($e->getHttpStatus() === 405) {
816
+                if ($path === '') {
817
+                    // if root is gone it means the storage is not available
818
+                    throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
819
+                }
820
+                return false;
821
+            }
822
+            $this->convertException($e, $path);
823
+            return false;
824
+        } catch (\Exception $e) {
825
+            $this->convertException($e, $path);
826
+            return false;
827
+        }
828
+    }
829
+
830
+    /**
831
+     * Interpret the given exception and decide whether it is due to an
832
+     * unavailable storage, invalid storage or other.
833
+     * This will either throw StorageInvalidException, StorageNotAvailableException
834
+     * or do nothing.
835
+     *
836
+     * @param Exception $e sabre exception
837
+     * @param string $path optional path from the operation
838
+     *
839
+     * @throws StorageInvalidException if the storage is invalid, for example
840
+     * when the authentication expired or is invalid
841
+     * @throws StorageNotAvailableException if the storage is not available,
842
+     * which might be temporary
843
+     * @throws ForbiddenException if the action is not allowed
844
+     */
845
+    protected function convertException(Exception $e, $path = '') {
846
+        \OC::$server->getLogger()->logException($e, ['app' => 'files_external', 'level' => ILogger::DEBUG]);
847
+        if ($e instanceof ClientHttpException) {
848
+            if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
849
+                throw new \OCP\Lock\LockedException($path);
850
+            }
851
+            if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
852
+                // either password was changed or was invalid all along
853
+                throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
854
+            } elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
855
+                // ignore exception for MethodNotAllowed, false will be returned
856
+                return;
857
+            } elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
858
+                // The operation is forbidden. Fail somewhat gracefully
859
+                throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
860
+            }
861
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
862
+        } elseif ($e instanceof ClientException) {
863
+            // connection timeout or refused, server could be temporarily down
864
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
865
+        } elseif ($e instanceof \InvalidArgumentException) {
866
+            // parse error because the server returned HTML instead of XML,
867
+            // possibly temporarily down
868
+            throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
869
+        } elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
870
+            // rethrow
871
+            throw $e;
872
+        }
873
+
874
+        // TODO: only log for now, but in the future need to wrap/rethrow exception
875
+    }
876 876
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Scanner.php 1 patch
Indentation   +508 added lines, -508 removed lines patch added patch discarded remove patch
@@ -57,512 +57,512 @@
 block discarded – undo
57 57
  * @package OC\Files\Cache
58 58
  */
59 59
 class Scanner extends BasicEmitter implements IScanner {
60
-	/**
61
-	 * @var \OC\Files\Storage\Storage $storage
62
-	 */
63
-	protected $storage;
64
-
65
-	/**
66
-	 * @var string $storageId
67
-	 */
68
-	protected $storageId;
69
-
70
-	/**
71
-	 * @var \OC\Files\Cache\Cache $cache
72
-	 */
73
-	protected $cache;
74
-
75
-	/**
76
-	 * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
77
-	 */
78
-	protected $cacheActive;
79
-
80
-	/**
81
-	 * @var bool $useTransactions whether to use transactions
82
-	 */
83
-	protected $useTransactions = true;
84
-
85
-	/**
86
-	 * @var \OCP\Lock\ILockingProvider
87
-	 */
88
-	protected $lockingProvider;
89
-
90
-	public function __construct(\OC\Files\Storage\Storage $storage) {
91
-		$this->storage = $storage;
92
-		$this->storageId = $this->storage->getId();
93
-		$this->cache = $storage->getCache();
94
-		$this->cacheActive = !\OC::$server->getConfig()->getSystemValue('filesystem_cache_readonly', false);
95
-		$this->lockingProvider = \OC::$server->getLockingProvider();
96
-	}
97
-
98
-	/**
99
-	 * Whether to wrap the scanning of a folder in a database transaction
100
-	 * On default transactions are used
101
-	 *
102
-	 * @param bool $useTransactions
103
-	 */
104
-	public function setUseTransactions($useTransactions) {
105
-		$this->useTransactions = $useTransactions;
106
-	}
107
-
108
-	/**
109
-	 * get all the metadata of a file or folder
110
-	 * *
111
-	 *
112
-	 * @param string $path
113
-	 * @return array|null an array of metadata of the file
114
-	 */
115
-	protected function getData($path) {
116
-		$data = $this->storage->getMetaData($path);
117
-		if (is_null($data)) {
118
-			\OCP\Util::writeLog(Scanner::class, "!!! Path '$path' is not accessible or present !!!", ILogger::DEBUG);
119
-		}
120
-		return $data;
121
-	}
122
-
123
-	/**
124
-	 * scan a single file and store it in the cache
125
-	 *
126
-	 * @param string $file
127
-	 * @param int $reuseExisting
128
-	 * @param int $parentId
129
-	 * @param array|null|false $cacheData existing data in the cache for the file to be scanned
130
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
131
-	 * @param null $data the metadata for the file, as returned by the storage
132
-	 * @return array|null an array of metadata of the scanned file
133
-	 * @throws \OCP\Lock\LockedException
134
-	 */
135
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
136
-		if ($file !== '') {
137
-			try {
138
-				$this->storage->verifyPath(dirname($file), basename($file));
139
-			} catch (\Exception $e) {
140
-				return null;
141
-			}
142
-		}
143
-		// only proceed if $file is not a partial file, blacklist is handled by the storage
144
-		if (!self::isPartialFile($file)) {
145
-
146
-			//acquire a lock
147
-			if ($lock) {
148
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
149
-					$this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
150
-				}
151
-			}
152
-
153
-			try {
154
-				$data = $data ?? $this->getData($file);
155
-			} catch (ForbiddenException $e) {
156
-				if ($lock) {
157
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
158
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
159
-					}
160
-				}
161
-
162
-				return null;
163
-			}
164
-
165
-			try {
166
-				if ($data) {
167
-
168
-					// pre-emit only if it was a file. By that we avoid counting/treating folders as files
169
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
170
-						$this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
171
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
172
-					}
173
-
174
-					$parent = dirname($file);
175
-					if ($parent === '.' or $parent === '/') {
176
-						$parent = '';
177
-					}
178
-					if ($parentId === -1) {
179
-						$parentId = $this->cache->getParentId($file);
180
-					}
181
-
182
-					// scan the parent if it's not in the cache (id -1) and the current file is not the root folder
183
-					if ($file and $parentId === -1) {
184
-						$parentData = $this->scanFile($parent);
185
-						if (!$parentData) {
186
-							return null;
187
-						}
188
-						$parentId = $parentData['fileid'];
189
-					}
190
-					if ($parent) {
191
-						$data['parent'] = $parentId;
192
-					}
193
-					if (is_null($cacheData)) {
194
-						/** @var CacheEntry $cacheData */
195
-						$cacheData = $this->cache->get($file);
196
-					}
197
-					if ($cacheData and $reuseExisting and isset($cacheData['fileid'])) {
198
-						// prevent empty etag
199
-						if (empty($cacheData['etag'])) {
200
-							$etag = $data['etag'];
201
-						} else {
202
-							$etag = $cacheData['etag'];
203
-						}
204
-						$fileId = $cacheData['fileid'];
205
-						$data['fileid'] = $fileId;
206
-						// only reuse data if the file hasn't explicitly changed
207
-						if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
208
-							$data['mtime'] = $cacheData['mtime'];
209
-							if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
210
-								$data['size'] = $cacheData['size'];
211
-							}
212
-							if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
213
-								$data['etag'] = $etag;
214
-							}
215
-						}
216
-						// Only update metadata that has changed
217
-						$newData = array_diff_assoc($data, $cacheData->getData());
218
-					} else {
219
-						$newData = $data;
220
-						$fileId = -1;
221
-					}
222
-					if (!empty($newData)) {
223
-						// Reset the checksum if the data has changed
224
-						$newData['checksum'] = '';
225
-						$newData['parent'] = $parentId;
226
-						$data['fileid'] = $this->addToCache($file, $newData, $fileId);
227
-					}
228
-					if ($cacheData && isset($cacheData['size'])) {
229
-						$data['oldSize'] = $cacheData['size'];
230
-					} else {
231
-						$data['oldSize'] = 0;
232
-					}
233
-
234
-					if ($cacheData && isset($cacheData['encrypted'])) {
235
-						$data['encrypted'] = $cacheData['encrypted'];
236
-					}
237
-
238
-					// post-emit only if it was a file. By that we avoid counting/treating folders as files
239
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
240
-						$this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
241
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
242
-					}
243
-				} else {
244
-					$this->removeFromCache($file);
245
-				}
246
-			} catch (\Exception $e) {
247
-				if ($lock) {
248
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
249
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
250
-					}
251
-				}
252
-				throw $e;
253
-			}
254
-
255
-			//release the acquired lock
256
-			if ($lock) {
257
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
258
-					$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
259
-				}
260
-			}
261
-
262
-			if ($data && !isset($data['encrypted'])) {
263
-				$data['encrypted'] = false;
264
-			}
265
-			return $data;
266
-		}
267
-
268
-		return null;
269
-	}
270
-
271
-	protected function removeFromCache($path) {
272
-		\OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
273
-		$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
274
-		if ($this->cacheActive) {
275
-			$this->cache->remove($path);
276
-		}
277
-	}
278
-
279
-	/**
280
-	 * @param string $path
281
-	 * @param array $data
282
-	 * @param int $fileId
283
-	 * @return int the id of the added file
284
-	 */
285
-	protected function addToCache($path, $data, $fileId = -1) {
286
-		if (isset($data['scan_permissions'])) {
287
-			$data['permissions'] = $data['scan_permissions'];
288
-		}
289
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
290
-		$this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
291
-		if ($this->cacheActive) {
292
-			if ($fileId !== -1) {
293
-				$this->cache->update($fileId, $data);
294
-				return $fileId;
295
-			} else {
296
-				return $this->cache->insert($path, $data);
297
-			}
298
-		} else {
299
-			return -1;
300
-		}
301
-	}
302
-
303
-	/**
304
-	 * @param string $path
305
-	 * @param array $data
306
-	 * @param int $fileId
307
-	 */
308
-	protected function updateCache($path, $data, $fileId = -1) {
309
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
310
-		$this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
311
-		if ($this->cacheActive) {
312
-			if ($fileId !== -1) {
313
-				$this->cache->update($fileId, $data);
314
-			} else {
315
-				$this->cache->put($path, $data);
316
-			}
317
-		}
318
-	}
319
-
320
-	/**
321
-	 * scan a folder and all it's children
322
-	 *
323
-	 * @param string $path
324
-	 * @param bool $recursive
325
-	 * @param int $reuse
326
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
327
-	 * @return array|null an array of the meta data of the scanned file or folder
328
-	 */
329
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
330
-		if ($reuse === -1) {
331
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
332
-		}
333
-		if ($lock) {
334
-			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
335
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
336
-				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
337
-			}
338
-		}
339
-		try {
340
-			$data = $this->scanFile($path, $reuse, -1, null, $lock);
341
-			if ($data and $data['mimetype'] === 'httpd/unix-directory') {
342
-				$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock);
343
-				$data['size'] = $size;
344
-			}
345
-		} finally {
346
-			if ($lock) {
347
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
348
-					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
349
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
350
-				}
351
-			}
352
-		}
353
-		return $data;
354
-	}
355
-
356
-	/**
357
-	 * Get the children currently in the cache
358
-	 *
359
-	 * @param int $folderId
360
-	 * @return array[]
361
-	 */
362
-	protected function getExistingChildren($folderId) {
363
-		$existingChildren = [];
364
-		$children = $this->cache->getFolderContentsById($folderId);
365
-		foreach ($children as $child) {
366
-			$existingChildren[$child['name']] = $child;
367
-		}
368
-		return $existingChildren;
369
-	}
370
-
371
-	/**
372
-	 * scan all the files and folders in a folder
373
-	 *
374
-	 * @param string $path
375
-	 * @param bool $recursive
376
-	 * @param int $reuse
377
-	 * @param int $folderId id for the folder to be scanned
378
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
379
-	 * @return int the size of the scanned folder or -1 if the size is unknown at this stage
380
-	 */
381
-	protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true) {
382
-		if ($reuse === -1) {
383
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
384
-		}
385
-		$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
386
-		$size = 0;
387
-		if (!is_null($folderId)) {
388
-			$folderId = $this->cache->getId($path);
389
-		}
390
-		$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
391
-
392
-		foreach ($childQueue as $child => $childId) {
393
-			$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
394
-			if ($childSize === -1) {
395
-				$size = -1;
396
-			} elseif ($size !== -1) {
397
-				$size += $childSize;
398
-			}
399
-		}
400
-		if ($this->cacheActive) {
401
-			$this->cache->update($folderId, ['size' => $size]);
402
-		}
403
-		$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
404
-		return $size;
405
-	}
406
-
407
-	private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
408
-		// we put this in it's own function so it cleans up the memory before we start recursing
409
-		$existingChildren = $this->getExistingChildren($folderId);
410
-		$newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
411
-
412
-		if ($this->useTransactions) {
413
-			\OC::$server->getDatabaseConnection()->beginTransaction();
414
-		}
415
-
416
-		$exceptionOccurred = false;
417
-		$childQueue = [];
418
-		$newChildNames = [];
419
-		foreach ($newChildren as $fileMeta) {
420
-			$permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions'];
421
-			if ($permissions === 0) {
422
-				continue;
423
-			}
424
-			$originalFile = $fileMeta['name'];
425
-			$file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
426
-			if (trim($originalFile, '/') !== $file) {
427
-				// encoding mismatch, might require compatibility wrapper
428
-				\OC::$server->getLogger()->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
429
-				$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
430
-				// skip this entry
431
-				continue;
432
-			}
433
-
434
-			$newChildNames[] = $file;
435
-			$child = $path ? $path . '/' . $file : $file;
436
-			try {
437
-				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
438
-				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
439
-				if ($data) {
440
-					if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE) {
441
-						$childQueue[$child] = $data['fileid'];
442
-					} elseif ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE_INCOMPLETE and $data['size'] === -1) {
443
-						// only recurse into folders which aren't fully scanned
444
-						$childQueue[$child] = $data['fileid'];
445
-					} elseif ($data['size'] === -1) {
446
-						$size = -1;
447
-					} elseif ($size !== -1) {
448
-						$size += $data['size'];
449
-					}
450
-				}
451
-			} catch (Exception $ex) {
452
-				// might happen if inserting duplicate while a scanning
453
-				// process is running in parallel
454
-				// log and ignore
455
-				if ($this->useTransactions) {
456
-					\OC::$server->getDatabaseConnection()->rollback();
457
-					\OC::$server->getDatabaseConnection()->beginTransaction();
458
-				}
459
-				\OC::$server->getLogger()->logException($ex, [
460
-					'message' => 'Exception while scanning file "' . $child . '"',
461
-					'level' => ILogger::DEBUG,
462
-					'app' => 'core',
463
-				]);
464
-				$exceptionOccurred = true;
465
-			} catch (\OCP\Lock\LockedException $e) {
466
-				if ($this->useTransactions) {
467
-					\OC::$server->getDatabaseConnection()->rollback();
468
-				}
469
-				throw $e;
470
-			}
471
-		}
472
-		$removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
473
-		foreach ($removedChildren as $childName) {
474
-			$child = $path ? $path . '/' . $childName : $childName;
475
-			$this->removeFromCache($child);
476
-		}
477
-		if ($this->useTransactions) {
478
-			\OC::$server->getDatabaseConnection()->commit();
479
-		}
480
-		if ($exceptionOccurred) {
481
-			// It might happen that the parallel scan process has already
482
-			// inserted mimetypes but those weren't available yet inside the transaction
483
-			// To make sure to have the updated mime types in such cases,
484
-			// we reload them here
485
-			\OC::$server->getMimeTypeLoader()->reset();
486
-		}
487
-		return $childQueue;
488
-	}
489
-
490
-	/**
491
-	 * check if the file should be ignored when scanning
492
-	 * NOTE: files with a '.part' extension are ignored as well!
493
-	 *       prevents unfinished put requests to be scanned
494
-	 *
495
-	 * @param string $file
496
-	 * @return boolean
497
-	 */
498
-	public static function isPartialFile($file) {
499
-		if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
500
-			return true;
501
-		}
502
-		if (strpos($file, '.part/') !== false) {
503
-			return true;
504
-		}
505
-
506
-		return false;
507
-	}
508
-
509
-	/**
510
-	 * walk over any folders that are not fully scanned yet and scan them
511
-	 */
512
-	public function backgroundScan() {
513
-		if ($this->storage->instanceOfStorage(Jail::class)) {
514
-			// for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
515
-			// this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
516
-			//
517
-			// Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
518
-			// have to be scanned at some point anyway.
519
-			$unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
520
-			$unJailedScanner->backgroundScan();
521
-		} else {
522
-			if (!$this->cache->inCache('')) {
523
-				// if the storage isn't in the cache yet, just scan the root completely
524
-				$this->runBackgroundScanJob(function () {
525
-					$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
526
-				}, '');
527
-			} else {
528
-				$lastPath = null;
529
-				// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
530
-				while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
531
-					$this->runBackgroundScanJob(function () use ($path) {
532
-						$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
533
-					}, $path);
534
-					// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
535
-					// to make this possible
536
-					$lastPath = $path;
537
-				}
538
-			}
539
-		}
540
-	}
541
-
542
-	private function runBackgroundScanJob(callable $callback, $path) {
543
-		try {
544
-			$callback();
545
-			\OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
546
-			if ($this->cacheActive && $this->cache instanceof Cache) {
547
-				$this->cache->correctFolderSize($path, null, true);
548
-			}
549
-		} catch (\OCP\Files\StorageInvalidException $e) {
550
-			// skip unavailable storages
551
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
552
-			// skip unavailable storages
553
-		} catch (\OCP\Files\ForbiddenException $e) {
554
-			// skip forbidden storages
555
-		} catch (\OCP\Lock\LockedException $e) {
556
-			// skip unavailable storages
557
-		}
558
-	}
559
-
560
-	/**
561
-	 * Set whether the cache is affected by scan operations
562
-	 *
563
-	 * @param boolean $active The active state of the cache
564
-	 */
565
-	public function setCacheActive($active) {
566
-		$this->cacheActive = $active;
567
-	}
60
+    /**
61
+     * @var \OC\Files\Storage\Storage $storage
62
+     */
63
+    protected $storage;
64
+
65
+    /**
66
+     * @var string $storageId
67
+     */
68
+    protected $storageId;
69
+
70
+    /**
71
+     * @var \OC\Files\Cache\Cache $cache
72
+     */
73
+    protected $cache;
74
+
75
+    /**
76
+     * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
77
+     */
78
+    protected $cacheActive;
79
+
80
+    /**
81
+     * @var bool $useTransactions whether to use transactions
82
+     */
83
+    protected $useTransactions = true;
84
+
85
+    /**
86
+     * @var \OCP\Lock\ILockingProvider
87
+     */
88
+    protected $lockingProvider;
89
+
90
+    public function __construct(\OC\Files\Storage\Storage $storage) {
91
+        $this->storage = $storage;
92
+        $this->storageId = $this->storage->getId();
93
+        $this->cache = $storage->getCache();
94
+        $this->cacheActive = !\OC::$server->getConfig()->getSystemValue('filesystem_cache_readonly', false);
95
+        $this->lockingProvider = \OC::$server->getLockingProvider();
96
+    }
97
+
98
+    /**
99
+     * Whether to wrap the scanning of a folder in a database transaction
100
+     * On default transactions are used
101
+     *
102
+     * @param bool $useTransactions
103
+     */
104
+    public function setUseTransactions($useTransactions) {
105
+        $this->useTransactions = $useTransactions;
106
+    }
107
+
108
+    /**
109
+     * get all the metadata of a file or folder
110
+     * *
111
+     *
112
+     * @param string $path
113
+     * @return array|null an array of metadata of the file
114
+     */
115
+    protected function getData($path) {
116
+        $data = $this->storage->getMetaData($path);
117
+        if (is_null($data)) {
118
+            \OCP\Util::writeLog(Scanner::class, "!!! Path '$path' is not accessible or present !!!", ILogger::DEBUG);
119
+        }
120
+        return $data;
121
+    }
122
+
123
+    /**
124
+     * scan a single file and store it in the cache
125
+     *
126
+     * @param string $file
127
+     * @param int $reuseExisting
128
+     * @param int $parentId
129
+     * @param array|null|false $cacheData existing data in the cache for the file to be scanned
130
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
131
+     * @param null $data the metadata for the file, as returned by the storage
132
+     * @return array|null an array of metadata of the scanned file
133
+     * @throws \OCP\Lock\LockedException
134
+     */
135
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
136
+        if ($file !== '') {
137
+            try {
138
+                $this->storage->verifyPath(dirname($file), basename($file));
139
+            } catch (\Exception $e) {
140
+                return null;
141
+            }
142
+        }
143
+        // only proceed if $file is not a partial file, blacklist is handled by the storage
144
+        if (!self::isPartialFile($file)) {
145
+
146
+            //acquire a lock
147
+            if ($lock) {
148
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
149
+                    $this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
150
+                }
151
+            }
152
+
153
+            try {
154
+                $data = $data ?? $this->getData($file);
155
+            } catch (ForbiddenException $e) {
156
+                if ($lock) {
157
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
158
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
159
+                    }
160
+                }
161
+
162
+                return null;
163
+            }
164
+
165
+            try {
166
+                if ($data) {
167
+
168
+                    // pre-emit only if it was a file. By that we avoid counting/treating folders as files
169
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
170
+                        $this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
171
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
172
+                    }
173
+
174
+                    $parent = dirname($file);
175
+                    if ($parent === '.' or $parent === '/') {
176
+                        $parent = '';
177
+                    }
178
+                    if ($parentId === -1) {
179
+                        $parentId = $this->cache->getParentId($file);
180
+                    }
181
+
182
+                    // scan the parent if it's not in the cache (id -1) and the current file is not the root folder
183
+                    if ($file and $parentId === -1) {
184
+                        $parentData = $this->scanFile($parent);
185
+                        if (!$parentData) {
186
+                            return null;
187
+                        }
188
+                        $parentId = $parentData['fileid'];
189
+                    }
190
+                    if ($parent) {
191
+                        $data['parent'] = $parentId;
192
+                    }
193
+                    if (is_null($cacheData)) {
194
+                        /** @var CacheEntry $cacheData */
195
+                        $cacheData = $this->cache->get($file);
196
+                    }
197
+                    if ($cacheData and $reuseExisting and isset($cacheData['fileid'])) {
198
+                        // prevent empty etag
199
+                        if (empty($cacheData['etag'])) {
200
+                            $etag = $data['etag'];
201
+                        } else {
202
+                            $etag = $cacheData['etag'];
203
+                        }
204
+                        $fileId = $cacheData['fileid'];
205
+                        $data['fileid'] = $fileId;
206
+                        // only reuse data if the file hasn't explicitly changed
207
+                        if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
208
+                            $data['mtime'] = $cacheData['mtime'];
209
+                            if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
210
+                                $data['size'] = $cacheData['size'];
211
+                            }
212
+                            if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
213
+                                $data['etag'] = $etag;
214
+                            }
215
+                        }
216
+                        // Only update metadata that has changed
217
+                        $newData = array_diff_assoc($data, $cacheData->getData());
218
+                    } else {
219
+                        $newData = $data;
220
+                        $fileId = -1;
221
+                    }
222
+                    if (!empty($newData)) {
223
+                        // Reset the checksum if the data has changed
224
+                        $newData['checksum'] = '';
225
+                        $newData['parent'] = $parentId;
226
+                        $data['fileid'] = $this->addToCache($file, $newData, $fileId);
227
+                    }
228
+                    if ($cacheData && isset($cacheData['size'])) {
229
+                        $data['oldSize'] = $cacheData['size'];
230
+                    } else {
231
+                        $data['oldSize'] = 0;
232
+                    }
233
+
234
+                    if ($cacheData && isset($cacheData['encrypted'])) {
235
+                        $data['encrypted'] = $cacheData['encrypted'];
236
+                    }
237
+
238
+                    // post-emit only if it was a file. By that we avoid counting/treating folders as files
239
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
240
+                        $this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
241
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
242
+                    }
243
+                } else {
244
+                    $this->removeFromCache($file);
245
+                }
246
+            } catch (\Exception $e) {
247
+                if ($lock) {
248
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
249
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
250
+                    }
251
+                }
252
+                throw $e;
253
+            }
254
+
255
+            //release the acquired lock
256
+            if ($lock) {
257
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
258
+                    $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
259
+                }
260
+            }
261
+
262
+            if ($data && !isset($data['encrypted'])) {
263
+                $data['encrypted'] = false;
264
+            }
265
+            return $data;
266
+        }
267
+
268
+        return null;
269
+    }
270
+
271
+    protected function removeFromCache($path) {
272
+        \OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
273
+        $this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
274
+        if ($this->cacheActive) {
275
+            $this->cache->remove($path);
276
+        }
277
+    }
278
+
279
+    /**
280
+     * @param string $path
281
+     * @param array $data
282
+     * @param int $fileId
283
+     * @return int the id of the added file
284
+     */
285
+    protected function addToCache($path, $data, $fileId = -1) {
286
+        if (isset($data['scan_permissions'])) {
287
+            $data['permissions'] = $data['scan_permissions'];
288
+        }
289
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
290
+        $this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
291
+        if ($this->cacheActive) {
292
+            if ($fileId !== -1) {
293
+                $this->cache->update($fileId, $data);
294
+                return $fileId;
295
+            } else {
296
+                return $this->cache->insert($path, $data);
297
+            }
298
+        } else {
299
+            return -1;
300
+        }
301
+    }
302
+
303
+    /**
304
+     * @param string $path
305
+     * @param array $data
306
+     * @param int $fileId
307
+     */
308
+    protected function updateCache($path, $data, $fileId = -1) {
309
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
310
+        $this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
311
+        if ($this->cacheActive) {
312
+            if ($fileId !== -1) {
313
+                $this->cache->update($fileId, $data);
314
+            } else {
315
+                $this->cache->put($path, $data);
316
+            }
317
+        }
318
+    }
319
+
320
+    /**
321
+     * scan a folder and all it's children
322
+     *
323
+     * @param string $path
324
+     * @param bool $recursive
325
+     * @param int $reuse
326
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
327
+     * @return array|null an array of the meta data of the scanned file or folder
328
+     */
329
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
330
+        if ($reuse === -1) {
331
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
332
+        }
333
+        if ($lock) {
334
+            if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
335
+                $this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
336
+                $this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
337
+            }
338
+        }
339
+        try {
340
+            $data = $this->scanFile($path, $reuse, -1, null, $lock);
341
+            if ($data and $data['mimetype'] === 'httpd/unix-directory') {
342
+                $size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock);
343
+                $data['size'] = $size;
344
+            }
345
+        } finally {
346
+            if ($lock) {
347
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
348
+                    $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
349
+                    $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
350
+                }
351
+            }
352
+        }
353
+        return $data;
354
+    }
355
+
356
+    /**
357
+     * Get the children currently in the cache
358
+     *
359
+     * @param int $folderId
360
+     * @return array[]
361
+     */
362
+    protected function getExistingChildren($folderId) {
363
+        $existingChildren = [];
364
+        $children = $this->cache->getFolderContentsById($folderId);
365
+        foreach ($children as $child) {
366
+            $existingChildren[$child['name']] = $child;
367
+        }
368
+        return $existingChildren;
369
+    }
370
+
371
+    /**
372
+     * scan all the files and folders in a folder
373
+     *
374
+     * @param string $path
375
+     * @param bool $recursive
376
+     * @param int $reuse
377
+     * @param int $folderId id for the folder to be scanned
378
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
379
+     * @return int the size of the scanned folder or -1 if the size is unknown at this stage
380
+     */
381
+    protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true) {
382
+        if ($reuse === -1) {
383
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
384
+        }
385
+        $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
386
+        $size = 0;
387
+        if (!is_null($folderId)) {
388
+            $folderId = $this->cache->getId($path);
389
+        }
390
+        $childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
391
+
392
+        foreach ($childQueue as $child => $childId) {
393
+            $childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
394
+            if ($childSize === -1) {
395
+                $size = -1;
396
+            } elseif ($size !== -1) {
397
+                $size += $childSize;
398
+            }
399
+        }
400
+        if ($this->cacheActive) {
401
+            $this->cache->update($folderId, ['size' => $size]);
402
+        }
403
+        $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
404
+        return $size;
405
+    }
406
+
407
+    private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
408
+        // we put this in it's own function so it cleans up the memory before we start recursing
409
+        $existingChildren = $this->getExistingChildren($folderId);
410
+        $newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
411
+
412
+        if ($this->useTransactions) {
413
+            \OC::$server->getDatabaseConnection()->beginTransaction();
414
+        }
415
+
416
+        $exceptionOccurred = false;
417
+        $childQueue = [];
418
+        $newChildNames = [];
419
+        foreach ($newChildren as $fileMeta) {
420
+            $permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions'];
421
+            if ($permissions === 0) {
422
+                continue;
423
+            }
424
+            $originalFile = $fileMeta['name'];
425
+            $file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
426
+            if (trim($originalFile, '/') !== $file) {
427
+                // encoding mismatch, might require compatibility wrapper
428
+                \OC::$server->getLogger()->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
429
+                $this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
430
+                // skip this entry
431
+                continue;
432
+            }
433
+
434
+            $newChildNames[] = $file;
435
+            $child = $path ? $path . '/' . $file : $file;
436
+            try {
437
+                $existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
438
+                $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
439
+                if ($data) {
440
+                    if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE) {
441
+                        $childQueue[$child] = $data['fileid'];
442
+                    } elseif ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE_INCOMPLETE and $data['size'] === -1) {
443
+                        // only recurse into folders which aren't fully scanned
444
+                        $childQueue[$child] = $data['fileid'];
445
+                    } elseif ($data['size'] === -1) {
446
+                        $size = -1;
447
+                    } elseif ($size !== -1) {
448
+                        $size += $data['size'];
449
+                    }
450
+                }
451
+            } catch (Exception $ex) {
452
+                // might happen if inserting duplicate while a scanning
453
+                // process is running in parallel
454
+                // log and ignore
455
+                if ($this->useTransactions) {
456
+                    \OC::$server->getDatabaseConnection()->rollback();
457
+                    \OC::$server->getDatabaseConnection()->beginTransaction();
458
+                }
459
+                \OC::$server->getLogger()->logException($ex, [
460
+                    'message' => 'Exception while scanning file "' . $child . '"',
461
+                    'level' => ILogger::DEBUG,
462
+                    'app' => 'core',
463
+                ]);
464
+                $exceptionOccurred = true;
465
+            } catch (\OCP\Lock\LockedException $e) {
466
+                if ($this->useTransactions) {
467
+                    \OC::$server->getDatabaseConnection()->rollback();
468
+                }
469
+                throw $e;
470
+            }
471
+        }
472
+        $removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
473
+        foreach ($removedChildren as $childName) {
474
+            $child = $path ? $path . '/' . $childName : $childName;
475
+            $this->removeFromCache($child);
476
+        }
477
+        if ($this->useTransactions) {
478
+            \OC::$server->getDatabaseConnection()->commit();
479
+        }
480
+        if ($exceptionOccurred) {
481
+            // It might happen that the parallel scan process has already
482
+            // inserted mimetypes but those weren't available yet inside the transaction
483
+            // To make sure to have the updated mime types in such cases,
484
+            // we reload them here
485
+            \OC::$server->getMimeTypeLoader()->reset();
486
+        }
487
+        return $childQueue;
488
+    }
489
+
490
+    /**
491
+     * check if the file should be ignored when scanning
492
+     * NOTE: files with a '.part' extension are ignored as well!
493
+     *       prevents unfinished put requests to be scanned
494
+     *
495
+     * @param string $file
496
+     * @return boolean
497
+     */
498
+    public static function isPartialFile($file) {
499
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
500
+            return true;
501
+        }
502
+        if (strpos($file, '.part/') !== false) {
503
+            return true;
504
+        }
505
+
506
+        return false;
507
+    }
508
+
509
+    /**
510
+     * walk over any folders that are not fully scanned yet and scan them
511
+     */
512
+    public function backgroundScan() {
513
+        if ($this->storage->instanceOfStorage(Jail::class)) {
514
+            // for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
515
+            // this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
516
+            //
517
+            // Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
518
+            // have to be scanned at some point anyway.
519
+            $unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
520
+            $unJailedScanner->backgroundScan();
521
+        } else {
522
+            if (!$this->cache->inCache('')) {
523
+                // if the storage isn't in the cache yet, just scan the root completely
524
+                $this->runBackgroundScanJob(function () {
525
+                    $this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
526
+                }, '');
527
+            } else {
528
+                $lastPath = null;
529
+                // find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
530
+                while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
531
+                    $this->runBackgroundScanJob(function () use ($path) {
532
+                        $this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
533
+                    }, $path);
534
+                    // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
535
+                    // to make this possible
536
+                    $lastPath = $path;
537
+                }
538
+            }
539
+        }
540
+    }
541
+
542
+    private function runBackgroundScanJob(callable $callback, $path) {
543
+        try {
544
+            $callback();
545
+            \OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
546
+            if ($this->cacheActive && $this->cache instanceof Cache) {
547
+                $this->cache->correctFolderSize($path, null, true);
548
+            }
549
+        } catch (\OCP\Files\StorageInvalidException $e) {
550
+            // skip unavailable storages
551
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
552
+            // skip unavailable storages
553
+        } catch (\OCP\Files\ForbiddenException $e) {
554
+            // skip forbidden storages
555
+        } catch (\OCP\Lock\LockedException $e) {
556
+            // skip unavailable storages
557
+        }
558
+    }
559
+
560
+    /**
561
+     * Set whether the cache is affected by scan operations
562
+     *
563
+     * @param boolean $active The active state of the cache
564
+     */
565
+    public function setCacheActive($active) {
566
+        $this->cacheActive = $active;
567
+    }
568 568
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Propagator.php 2 patches
Indentation   +167 added lines, -167 removed lines patch added patch discarded remove patch
@@ -33,171 +33,171 @@
 block discarded – undo
33 33
  * Propagate etags and mtimes within the storage
34 34
  */
35 35
 class Propagator implements IPropagator {
36
-	private $inBatch = false;
37
-
38
-	private $batch = [];
39
-
40
-	/**
41
-	 * @var \OC\Files\Storage\Storage
42
-	 */
43
-	protected $storage;
44
-
45
-	/**
46
-	 * @var IDBConnection
47
-	 */
48
-	private $connection;
49
-
50
-	/**
51
-	 * @var array
52
-	 */
53
-	private $ignore = [];
54
-
55
-	public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
56
-		$this->storage = $storage;
57
-		$this->connection = $connection;
58
-		$this->ignore = $ignore;
59
-	}
60
-
61
-
62
-	/**
63
-	 * @param string $internalPath
64
-	 * @param int $time
65
-	 * @param int $sizeDifference number of bytes the file has grown
66
-	 */
67
-	public function propagateChange($internalPath, $time, $sizeDifference = 0) {
68
-		// Do not propogate changes in ignored paths
69
-		foreach ($this->ignore as $ignore) {
70
-			if (strpos($internalPath, $ignore) === 0) {
71
-				return;
72
-			}
73
-		}
74
-
75
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
76
-
77
-		$parents = $this->getParents($internalPath);
78
-
79
-		if ($this->inBatch) {
80
-			foreach ($parents as $parent) {
81
-				$this->addToBatch($parent, $time, $sizeDifference);
82
-			}
83
-			return;
84
-		}
85
-
86
-		$parentHashes = array_map('md5', $parents);
87
-		$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
88
-
89
-		$builder = $this->connection->getQueryBuilder();
90
-		$hashParams = array_map(function ($hash) use ($builder) {
91
-			return $builder->expr()->literal($hash);
92
-		}, $parentHashes);
93
-
94
-		$builder->update('filecache')
95
-			->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
96
-			->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
97
-			->andWhere($builder->expr()->in('path_hash', $hashParams));
98
-		if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
99
-			$builder->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR));
100
-		}
101
-
102
-		$builder->execute();
103
-
104
-		if ($sizeDifference !== 0) {
105
-			// we need to do size separably so we can ignore entries with uncalculated size
106
-			$builder = $this->connection->getQueryBuilder();
107
-			$builder->update('filecache')
108
-				->set('size', $builder->func()->greatest(
109
-					$builder->func()->add('size', $builder->createNamedParameter($sizeDifference)),
110
-					$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
111
-				))
112
-				->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
113
-				->andWhere($builder->expr()->in('path_hash', $hashParams))
114
-				->andWhere($builder->expr()->gt('size', $builder->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
115
-
116
-			$builder->execute();
117
-		}
118
-	}
119
-
120
-	protected function getParents($path) {
121
-		$parts = explode('/', $path);
122
-		$parent = '';
123
-		$parents = [];
124
-		foreach ($parts as $part) {
125
-			$parents[] = $parent;
126
-			$parent = trim($parent . '/' . $part, '/');
127
-		}
128
-		return $parents;
129
-	}
130
-
131
-	/**
132
-	 * Mark the beginning of a propagation batch
133
-	 *
134
-	 * Note that not all cache setups support propagation in which case this will be a noop
135
-	 *
136
-	 * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
137
-	 * before the batch is committed.
138
-	 */
139
-	public function beginBatch() {
140
-		$this->inBatch = true;
141
-	}
142
-
143
-	private function addToBatch($internalPath, $time, $sizeDifference) {
144
-		if (!isset($this->batch[$internalPath])) {
145
-			$this->batch[$internalPath] = [
146
-				'hash' => md5($internalPath),
147
-				'time' => $time,
148
-				'size' => $sizeDifference,
149
-			];
150
-		} else {
151
-			$this->batch[$internalPath]['size'] += $sizeDifference;
152
-			if ($time > $this->batch[$internalPath]['time']) {
153
-				$this->batch[$internalPath]['time'] = $time;
154
-			}
155
-		}
156
-	}
157
-
158
-	/**
159
-	 * Commit the active propagation batch
160
-	 */
161
-	public function commitBatch() {
162
-		if (!$this->inBatch) {
163
-			throw new \BadMethodCallException('Not in batch');
164
-		}
165
-		$this->inBatch = false;
166
-
167
-		$this->connection->beginTransaction();
168
-
169
-		$query = $this->connection->getQueryBuilder();
170
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
171
-
172
-		$query->update('filecache')
173
-			->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
174
-			->set('etag', $query->expr()->literal(uniqid()))
175
-			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
176
-			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
177
-
178
-		$sizeQuery = $this->connection->getQueryBuilder();
179
-		$sizeQuery->update('filecache')
180
-			->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
181
-			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
182
-			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
183
-			->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
184
-
185
-		foreach ($this->batch as $item) {
186
-			$query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
187
-			$query->setParameter('hash', $item['hash']);
188
-
189
-			$query->execute();
190
-
191
-			if ($item['size']) {
192
-				$sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
193
-				$sizeQuery->setParameter('hash', $item['hash']);
194
-
195
-				$sizeQuery->execute();
196
-			}
197
-		}
198
-
199
-		$this->batch = [];
200
-
201
-		$this->connection->commit();
202
-	}
36
+    private $inBatch = false;
37
+
38
+    private $batch = [];
39
+
40
+    /**
41
+     * @var \OC\Files\Storage\Storage
42
+     */
43
+    protected $storage;
44
+
45
+    /**
46
+     * @var IDBConnection
47
+     */
48
+    private $connection;
49
+
50
+    /**
51
+     * @var array
52
+     */
53
+    private $ignore = [];
54
+
55
+    public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
56
+        $this->storage = $storage;
57
+        $this->connection = $connection;
58
+        $this->ignore = $ignore;
59
+    }
60
+
61
+
62
+    /**
63
+     * @param string $internalPath
64
+     * @param int $time
65
+     * @param int $sizeDifference number of bytes the file has grown
66
+     */
67
+    public function propagateChange($internalPath, $time, $sizeDifference = 0) {
68
+        // Do not propogate changes in ignored paths
69
+        foreach ($this->ignore as $ignore) {
70
+            if (strpos($internalPath, $ignore) === 0) {
71
+                return;
72
+            }
73
+        }
74
+
75
+        $storageId = (int)$this->storage->getStorageCache()->getNumericId();
76
+
77
+        $parents = $this->getParents($internalPath);
78
+
79
+        if ($this->inBatch) {
80
+            foreach ($parents as $parent) {
81
+                $this->addToBatch($parent, $time, $sizeDifference);
82
+            }
83
+            return;
84
+        }
85
+
86
+        $parentHashes = array_map('md5', $parents);
87
+        $etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
88
+
89
+        $builder = $this->connection->getQueryBuilder();
90
+        $hashParams = array_map(function ($hash) use ($builder) {
91
+            return $builder->expr()->literal($hash);
92
+        }, $parentHashes);
93
+
94
+        $builder->update('filecache')
95
+            ->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
96
+            ->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
97
+            ->andWhere($builder->expr()->in('path_hash', $hashParams));
98
+        if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
99
+            $builder->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR));
100
+        }
101
+
102
+        $builder->execute();
103
+
104
+        if ($sizeDifference !== 0) {
105
+            // we need to do size separably so we can ignore entries with uncalculated size
106
+            $builder = $this->connection->getQueryBuilder();
107
+            $builder->update('filecache')
108
+                ->set('size', $builder->func()->greatest(
109
+                    $builder->func()->add('size', $builder->createNamedParameter($sizeDifference)),
110
+                    $builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
111
+                ))
112
+                ->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
113
+                ->andWhere($builder->expr()->in('path_hash', $hashParams))
114
+                ->andWhere($builder->expr()->gt('size', $builder->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
115
+
116
+            $builder->execute();
117
+        }
118
+    }
119
+
120
+    protected function getParents($path) {
121
+        $parts = explode('/', $path);
122
+        $parent = '';
123
+        $parents = [];
124
+        foreach ($parts as $part) {
125
+            $parents[] = $parent;
126
+            $parent = trim($parent . '/' . $part, '/');
127
+        }
128
+        return $parents;
129
+    }
130
+
131
+    /**
132
+     * Mark the beginning of a propagation batch
133
+     *
134
+     * Note that not all cache setups support propagation in which case this will be a noop
135
+     *
136
+     * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
137
+     * before the batch is committed.
138
+     */
139
+    public function beginBatch() {
140
+        $this->inBatch = true;
141
+    }
142
+
143
+    private function addToBatch($internalPath, $time, $sizeDifference) {
144
+        if (!isset($this->batch[$internalPath])) {
145
+            $this->batch[$internalPath] = [
146
+                'hash' => md5($internalPath),
147
+                'time' => $time,
148
+                'size' => $sizeDifference,
149
+            ];
150
+        } else {
151
+            $this->batch[$internalPath]['size'] += $sizeDifference;
152
+            if ($time > $this->batch[$internalPath]['time']) {
153
+                $this->batch[$internalPath]['time'] = $time;
154
+            }
155
+        }
156
+    }
157
+
158
+    /**
159
+     * Commit the active propagation batch
160
+     */
161
+    public function commitBatch() {
162
+        if (!$this->inBatch) {
163
+            throw new \BadMethodCallException('Not in batch');
164
+        }
165
+        $this->inBatch = false;
166
+
167
+        $this->connection->beginTransaction();
168
+
169
+        $query = $this->connection->getQueryBuilder();
170
+        $storageId = (int)$this->storage->getStorageCache()->getNumericId();
171
+
172
+        $query->update('filecache')
173
+            ->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
174
+            ->set('etag', $query->expr()->literal(uniqid()))
175
+            ->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
176
+            ->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
177
+
178
+        $sizeQuery = $this->connection->getQueryBuilder();
179
+        $sizeQuery->update('filecache')
180
+            ->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
181
+            ->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
182
+            ->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
183
+            ->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
184
+
185
+        foreach ($this->batch as $item) {
186
+            $query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
187
+            $query->setParameter('hash', $item['hash']);
188
+
189
+            $query->execute();
190
+
191
+            if ($item['size']) {
192
+                $sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
193
+                $sizeQuery->setParameter('hash', $item['hash']);
194
+
195
+                $sizeQuery->execute();
196
+            }
197
+        }
198
+
199
+        $this->batch = [];
200
+
201
+        $this->connection->commit();
202
+    }
203 203
 }
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -72,7 +72,7 @@  discard block
 block discarded – undo
72 72
 			}
73 73
 		}
74 74
 
75
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
75
+		$storageId = (int) $this->storage->getStorageCache()->getNumericId();
76 76
 
77 77
 		$parents = $this->getParents($internalPath);
78 78
 
@@ -87,12 +87,12 @@  discard block
 block discarded – undo
87 87
 		$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
88 88
 
89 89
 		$builder = $this->connection->getQueryBuilder();
90
-		$hashParams = array_map(function ($hash) use ($builder) {
90
+		$hashParams = array_map(function($hash) use ($builder) {
91 91
 			return $builder->expr()->literal($hash);
92 92
 		}, $parentHashes);
93 93
 
94 94
 		$builder->update('filecache')
95
-			->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
95
+			->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int) $time, IQueryBuilder::PARAM_INT)))
96 96
 			->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
97 97
 			->andWhere($builder->expr()->in('path_hash', $hashParams));
98 98
 		if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
@@ -123,7 +123,7 @@  discard block
 block discarded – undo
123 123
 		$parents = [];
124 124
 		foreach ($parts as $part) {
125 125
 			$parents[] = $parent;
126
-			$parent = trim($parent . '/' . $part, '/');
126
+			$parent = trim($parent.'/'.$part, '/');
127 127
 		}
128 128
 		return $parents;
129 129
 	}
@@ -167,7 +167,7 @@  discard block
 block discarded – undo
167 167
 		$this->connection->beginTransaction();
168 168
 
169 169
 		$query = $this->connection->getQueryBuilder();
170
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
170
+		$storageId = (int) $this->storage->getStorageCache()->getNumericId();
171 171
 
172 172
 		$query->update('filecache')
173 173
 			->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
Please login to merge, or discard this patch.