summaryrefslogtreecommitdiffstats
path: root/documentation/articles/VaadinScalabilityTestingWithAmazonWebServices.asciidoc
blob: 9b230d13a0e08f4095f0ee6cae2f7f6f95d88417 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
---
title: Vaadin Scalability Testing With Amazon Web Services
order: 52
layout: page
---

[[vaadin-scalability-testing-with-amazon-web-services]]
= Vaadin scalability testing with Amazon Web Services

This article explains how you can test the scalability of your
application in the Amazon Web Services (AWS) cloud. The AWS services
used in this article include http://aws.amazon.com/ec2/[Amazon Elastic
Compute Cloud] (EC2) and http://aws.amazon.com/rds/[Amazon Relational
Database Service] (RDS). The use of
http://aws.amazon.com/elasticloadbalancing/[Amazon Elastic Load
Balancing] (ELB) is also briefly discussed. The application under
testing is called QuickTickets, a fictional Vaadin web application that
sells movie tickets to theaters all over the world. See also the
https://vaadin.com/blog/vaadin-scalability-study-quicktickets[blog
post about the experiment and the results].

To fully understand this article and follow through the steps, you
should have some basic knowledge of Amazon Web Services (AWS),
http://jakarta.apache.org/jmeter/[Apache JMeter], MySQL and Linux shell
usage. You will also need to know how to checkout the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/[QuickTickets
project] from SVN and run http://ant.apache.org/[Ant] targets to
generate test database and to package the application as a WAR file.

Please notice, that using the AWS services discussed here will incur
some expenses.

[[setting-up-the-amazon-rds-database]]
1. Setting up the Amazon RDS database
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* Login to http://aws.amazon.com/console/[AWS Management Console] and
select the Amazon RDS tab.

* Click the Launch DB Instance button.

* Select the following properties for the DB instance:
** DB Instance Class: db1.m1.large
** Multi-AZ Deployment: No
** Allocated Storage: 5 GB
** DB Instance Idenfitier: `quicktickets`
** Master User Name: `quicktickets`
** Master User Password: `<your-password>`
* Additional configuration:
** Database Name: `quicktickets`
* Management options:
** Backup Retention Period: 0 (disable backups)

* After the DB instance is started up, connect to the database with the
MySQL client.
** If this is the first time you are using Amazon RDS, you need to setup
the DB Security Groups.
** More information about http://aws.amazon.com/rds/faqs/#31[network
access to you DB instances].

* Once you have connected to the DB, run the following
command: `alter database quicktickets charset=utf8;`

* Note that the following steps might be a bit faster to do in an EC2
instance in the same zone as the RDS database. But you can of course do
these in your local machine as well.

* Take a checkout of the QuickTickets application project from the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/application/QuickTickets/[SVN
repository].

* Create the database schema by running the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/application/QuickTickets/db/createSchema.sql[QuickTickets/db/createSchema.sql]
file to the quicktickets
database.`mysql -uquicktickets -p<your-password> -h<db-instance-endpoint>.rds.amazonaws.com < QuickTickets/db/createSchema.sql`

* Create a huge test data by running Ant target
`create-huge-database-script` of the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/application/QuickTickets/build.xml[QuickTickets/build.xml]
script.`cd QuickTicketsant create-huge-database-script`

* This target will generate a huge SQL file (500MB) into a temporary
directory containing loads of test data. The location of the file is
printed to the console by the Ant target.

* Run the resulting `quickticketsdata.sql` file to the quicktickets
database (this will take quite a while, well over an
hour). `mysql -uquicktickets -p<your-password> -h<db-instance-endpoint>.rds.amazonaws.com < /tmp/quickticketsdata.sql`

[[setting-up-ec2-instances-for-quicktickets]]
2. Setting up EC2 instance(s) for QuickTickets
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* Login to http://aws.amazon.com/console/[AWS Management Console] and
select the Amazon EC2 tab.

* Click the Launch Instance button.

* Select Community AMIs tab and search an AMI with following id:
`ami-fb16f992`

* Launch a large instance of the AMI. Consult the
http://aws.amazon.com/documentation/ec2/[Amazon EC2 documentation] for
more details on how to launch a new instance.

* Login to the started instance as root via SSH.

* Copy and execute the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/installationscripts/webserver-memcached.sh[webserver-memcached.sh]
installation script as the root user. This script will setup
http://memcached.org/[Memcached] and http://tomcat.apache.org/[Apache
Tomcat].

* Repeat the above procedure for all the instances you want to setup.

[[deploying-the-quicktickets-application]]
3. Deploying the QuickTickets application
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* Take a checkout of the QuickTickets application project (if you
haven't already) from:
** http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/application/QuickTickets/

* Add the *Private DNS* of all the instances you have setup to the list
of Memcached servers in the `WebContent/WEB-INF/servers.xml` file with
the default Memcached port 11211. For
example: `<!-- Memcached servers --&gt;<value>ip-11-111-11-111.ec2.internal:11211</value><value>ip-22-222-22-222.ec2.internal:11211</value>...`

* Create a file called `build.properties` to the root directory of the
project (right next to the `QuickTickets/build.xml`). Set the
`productionMode` property to `true` and add your Amazon RDS database
configuration details to the file. For example:

....

# Debug or production mode?
productionMode=true

# Database configuration
database.url=jdbc:mysql:<db-instance-endpoint>.rds.amazonaws.com:3306/quicktickets?characterEncoding=utf8&useCompression=true
database.username=quicktickets
database.password=<your-password-here>
database.driverClassName=com.mysql.jdbc.Driver
....

* Run the `package-war` target of the `build.xml` to compile and package
the WAR file (resulting in the `build/ROOT.war` file).`ant package-war`

* Deploy the WAR file into all EC2 instances you just created by copying
the `ROOT.war` into `/opt/apache-tomcat/webapps` directory.

* Now you should be able to access the application through your web
browser by opening the following
URL: `http://<instance-public-dns>.amazonaws.com:8080/app`

* If you did setup more than one instance, you could create an Amazon
ELB load balancer and attach all instances to that load balancer.
However, this makes the JMeter testing close to impossible as the ELB
doesn't scale to sudden increases in traffic fast enough and starts
dropping connections.
** More information:
https://forums.aws.amazon.com/thread.jspa?messageID=130622&tstart=0
** More information:
https://wiki.apache.org/jmeter/JMeterAndAmazon

* If you still want to try using ELB, you should add
`-Dsun.net.inetaddr.ttl=0` to the JMeter JVM args and use the following
settings with the ELB:
** Port Configuration: 80 forwarding to 8080
** Enable Application Generated Cookie Stickiness for cookie name:
`jsessionid`
** Set the Health Check port to `8080`
** Ping Path: `/VAADIN/ticket.html`

[[setting-up-ec2-instances-for-jmeter]]
4. Setting up EC2 instance(s) for JMeter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* Launch and login to a new EC2 large instance (using the AMI
`ami-fb16f992`). See the first 5 steps of the second chapter.

* Copy and execute the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/installationscripts/jmeter-instance.sh[jmeter-instance.sh]
installation script as the root user.

* Download the
http://dev.vaadin.com/svn/incubator/QuickTickets/trunk/installationscripts/jmeter-test-script.jmx[JMeter
script].
** The script contains prerecorded ticket purchase sequence that lasts
about 2.5 minutes.

* Open the script in JMeter and make sure you configure the following
settings to suit your test:
** HTTP Request Defaults (set the server name)
** Thread Group (thread count, ramp-up, loop count)
** Summary report (result file name)

* Upload the test script to the JMeter instance(s).

* When logged in as root to the JMeter server you can start the test
from command line with the following
command: `~/jakarta-jmeter-2.4/bin/jmeter.sh -n -t ~/jmeter-test-script.jmx`

* After the run is complete you'll have `jmeter-results.jtl` file (or
the filename you used for the report) which you can open in JMeter for
analyzing the results.

[[results]]
5. Results
^^^^^^^^^^

Jump directly to the results:
https://vaadin.com/blog/vaadin-scalability-study-quicktickets[blog
post about the experiment and the results].
6/stable30'>backport/48016/stable30 Nextcloud server, a safe home for all your data: https://github.com/nextcloud/serverwww-data
aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Files/Node/Root.php
blob: 1686051131dace86512e73a792e939762bbee91e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC\Files\Node;

use OC\Files\FileInfo;
use OC\Files\Mount\Manager;
use OC\Files\Mount\MountPoint;
use OC\Files\Utils\PathHelper;
use OC\Files\View;
use OC\Hooks\PublicEmitter;
use OC\User\NoUserException;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Node as INode;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Server;
use Psr\Log\LoggerInterface;

/**
 * Class Root
 *
 * Hooks available in scope \OC\Files
 * - preWrite(\OCP\Files\Node $node)
 * - postWrite(\OCP\Files\Node $node)
 * - preCreate(\OCP\Files\Node $node)
 * - postCreate(\OCP\Files\Node $node)
 * - preDelete(\OCP\Files\Node $node)
 * - postDelete(\OCP\Files\Node $node)
 * - preTouch(\OC\FilesP\Node $node, int $mtime)
 * - postTouch(\OCP\Files\Node $node)
 * - preCopy(\OCP\Files\Node $source, \OCP\Files\Node $target)
 * - postCopy(\OCP\Files\Node $source, \OCP\Files\Node $target)
 * - preRename(\OCP\Files\Node $source, \OCP\Files\Node $target)
 * - postRename(\OCP\Files\Node $source, \OCP\Files\Node $target)
 *
 * @package OC\Files\Node
 */
class Root extends Folder implements IRootFolder {
	private Manager $mountManager;
	private PublicEmitter $emitter;
	private ?IUser $user;
	private CappedMemoryCache $userFolderCache;
	private IUserMountCache $userMountCache;
	private LoggerInterface $logger;
	private IUserManager $userManager;
	private IEventDispatcher $eventDispatcher;
	private ICache $pathByIdCache;

	/**
	 * @param Manager $manager
	 * @param View $view
	 * @param IUser|null $user
	 */
	public function __construct(
		$manager,
		$view,
		$user,
		IUserMountCache $userMountCache,
		LoggerInterface $logger,
		IUserManager $userManager,
		IEventDispatcher $eventDispatcher,
		ICacheFactory $cacheFactory,
	) {
		parent::__construct($this, $view, '');
		$this->mountManager = $manager;
		$this->user = $user;
		$this->emitter = new PublicEmitter();
		$this->userFolderCache = new CappedMemoryCache();
		$this->userMountCache = $userMountCache;
		$this->logger = $logger;
		$this->userManager = $userManager;
		$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
			$this->userFolderCache = new CappedMemoryCache();
		});
		$this->pathByIdCache = $cacheFactory->createLocal('path-by-id');
	}

	/**
	 * Get the user for which the filesystem is setup
	 *
	 * @return \OC\User\User
	 */
	public function getUser() {
		return $this->user;
	}

	/**
	 * @param string $scope
	 * @param string $method
	 * @param callable $callback
	 */
	public function listen($scope, $method, callable $callback) {
		$this->emitter->listen($scope, $method, $callback);
	}

	/**
	 * @param string $scope optional
	 * @param string $method optional
	 * @param callable $callback optional
	 */
	public function removeListener($scope = null, $method = null, ?callable $callback = null) {
		$this->emitter->removeListener($scope, $method, $callback);
	}

	/**
	 * @param string $scope
	 * @param string $method
	 * @param Node[] $arguments
	 */
	public function emit($scope, $method, $arguments = []) {
		$this->emitter->emit($scope, $method, $arguments);
	}

	/**
	 * @param \OC\Files\Storage\Storage $storage
	 * @param string $mountPoint
	 * @param array $arguments
	 */
	public function mount($storage, $mountPoint, $arguments = []) {
		$mount = new MountPoint($storage, $mountPoint, $arguments);
		$this->mountManager->addMount($mount);
	}

	public function getMount(string $mountPoint): IMountPoint {
		return $this->mountManager->find($mountPoint);
	}

	/**
	 * @param string $mountPoint
	 * @return \OC\Files\Mount\MountPoint[]
	 */
	public function getMountsIn(string $mountPoint): array {
		return $this->mountManager->findIn($mountPoint);
	}

	/**
	 * @param string $storageId
	 * @return \OC\Files\Mount\MountPoint[]
	 */
	public function getMountByStorageId($storageId) {
		return $this->mountManager->findByStorageId($storageId);
	}

	/**
	 * @param int $numericId
	 * @return MountPoint[]
	 */
	public function getMountByNumericStorageId($numericId) {
		return $this->mountManager->findByNumericId($numericId);
	}

	/**
	 * @param \OC\Files\Mount\MountPoint $mount
	 */
	public function unMount($mount) {
		$this->mountManager->remove($mount);
	}

	public function get($path) {
		$path = $this->normalizePath($path);
		if ($this->isValidPath($path)) {
			$fullPath = $this->getFullPath($path);
			$fileInfo = $this->view->getFileInfo($fullPath, false);
			if ($fileInfo) {
				return $this->createNode($fullPath, $fileInfo, false);
			} else {
				throw new NotFoundException($path);
			}
		} else {
			throw new NotPermittedException();
		}
	}

	//most operations can't be done on the root

	/**
	 * @param string $targetPath
	 * @return Node
	 * @throws \OCP\Files\NotPermittedException
	 */
	public function rename($targetPath) {
		throw new NotPermittedException();
	}

	public function delete() {
		throw new NotPermittedException();
	}

	/**
	 * @param string $targetPath
	 * @return Node
	 * @throws \OCP\Files\NotPermittedException
	 */
	public function copy($targetPath) {
		throw new NotPermittedException();
	}

	/**
	 * @param int $mtime
	 * @throws \OCP\Files\NotPermittedException
	 */
	public function touch($mtime = null) {
		throw new NotPermittedException();
	}

	/**
	 * @return \OC\Files\Storage\Storage
	 * @throws \OCP\Files\NotFoundException
	 */
	public function getStorage() {
		throw new NotFoundException();
	}

	/**
	 * @return string
	 */
	public function getPath() {
		return '/';
	}

	/**
	 * @return string
	 */
	public function getInternalPath() {
		return '';
	}

	/**
	 * @return int
	 */
	public function getId() {
		return 0;
	}

	/**
	 * @return array
	 */
	public function stat() {
		return [];
	}

	/**
	 * @return int
	 */
	public function getMTime() {
		return 0;
	}

	/**
	 * @param bool $includeMounts
	 * @return int|float
	 */
	public function getSize($includeMounts = true): int|float {
		return 0;
	}

	/**
	 * @return string
	 */
	public function getEtag() {
		return '';
	}

	/**
	 * @return int
	 */
	public function getPermissions() {
		return \OCP\Constants::PERMISSION_CREATE;
	}

	/**
	 * @return bool
	 */
	public function isReadable() {
		return false;
	}

	/**
	 * @return bool
	 */
	public function isUpdateable() {
		return false;
	}

	/**
	 * @return bool
	 */
	public function isDeletable() {
		return false;
	}

	/**
	 * @return bool
	 */
	public function isShareable() {
		return false;
	}

	/**
	 * @throws \OCP\Files\NotFoundException
	 */
	public function getParent(): INode|IRootFolder {
		throw new NotFoundException();
	}

	/**
	 * @return string
	 */
	public function getName() {
		return '';
	}

	/**
	 * Returns a view to user's files folder
	 *
	 * @param string $userId user ID
	 * @return \OCP\Files\Folder
	 * @throws NoUserException
	 * @throws NotPermittedException
	 */
	public function getUserFolder($userId) {
		$userObject = $this->userManager->get($userId);

		if (is_null($userObject)) {
			$e = new NoUserException('Backends provided no user object');
			$this->logger->error(
				sprintf(
					'Backends provided no user object for %s',
					$userId
				),
				[
					'app' => 'files',
					'exception' => $e,
				]
			);
			throw $e;
		}

		$userId = $userObject->getUID();

		if (!$this->userFolderCache->hasKey($userId)) {
			if ($this->mountManager->getSetupManager()->isSetupComplete($userObject)) {
				try {
					$folder = $this->get('/' . $userId . '/files');
					if (!$folder instanceof \OCP\Files\Folder) {
						throw new \Exception("Account folder for \"$userId\" exists as a file");
					}
				} catch (NotFoundException $e) {
					if (!$this->nodeExists('/' . $userId)) {
						$this->newFolder('/' . $userId);
					}
					$folder = $this->newFolder('/' . $userId . '/files');
				}
			} else {
				$folder = new LazyUserFolder($this, $userObject, $this->mountManager);
			}

			$this->userFolderCache->set($userId, $folder);
		}

		return $this->userFolderCache->get($userId);
	}

	public function getUserMountCache() {
		return $this->userMountCache;
	}

	public function getFirstNodeByIdInPath(int $id, string $path): ?INode {
		// scope the cache by user, so we don't return nodes for different users
		if ($this->user) {
			$cachedPath = $this->pathByIdCache->get($this->user->getUID() . '::' . $id);
			if ($cachedPath && str_starts_with($path, $cachedPath)) {
				// getting the node by path is significantly cheaper than finding it by id
				$node = $this->get($cachedPath);
				// by validating that the cached path still has the requested fileid we can work around the need to invalidate the cached path
				// if the cached path is invalid or a different file now we fall back to the uncached logic
				if ($node && $node->getId() === $id) {
					return $node;
				}
			}
		}
		$node = current($this->getByIdInPath($id, $path));
		if (!$node) {
			return null;
		}

		if ($this->user) {
			$this->pathByIdCache->set($this->user->getUID() . '::' . $id, $node->getPath());
		}
		return $node;
	}

	/**
	 * @param int $id
	 * @return Node[]
	 */
	public function getByIdInPath(int $id, string $path): array {
		$mountCache = $this->getUserMountCache();
		if (strpos($path, '/', 1) > 0) {
			[, $user] = explode('/', $path);
		} else {
			$user = null;
		}
		$mountsContainingFile = $mountCache->getMountsForFileId($id, $user);

		// if the mount isn't in the cache yet, perform a setup first, then try again
		if (count($mountsContainingFile) === 0) {
			$this->mountManager->getSetupManager()->setupForPath($path, true);
			$mountsContainingFile = $mountCache->getMountsForFileId($id, $user);
		}

		// when a user has access through the same storage through multiple paths
		// (such as an external storage that is both mounted for a user and shared to the user)
		// the mount cache will only hold a single entry for the storage
		// this can lead to issues as the different ways the user has access to a storage can have different permissions
		//
		// so instead of using the cached entries directly, we instead filter the current mounts by the rootid of the cache entry

		$mountRootIds = array_map(function ($mount) {
			return $mount->getRootId();
		}, $mountsContainingFile);
		$mountRootPaths = array_map(function ($mount) {
			return $mount->getRootInternalPath();
		}, $mountsContainingFile);
		$mountProviders = array_unique(array_map(function ($mount) {
			return $mount->getMountProvider();
		}, $mountsContainingFile));
		$mountRoots = array_combine($mountRootIds, $mountRootPaths);

		$mounts = $this->mountManager->getMountsByMountProvider($path, $mountProviders);

		$mountsContainingFile = array_filter($mounts, function ($mount) use ($mountRoots) {
			return isset($mountRoots[$mount->getStorageRootId()]);
		});

		if (count($mountsContainingFile) === 0) {
			if ($user === $this->getAppDataDirectoryName()) {
				$folder = $this->get($path);
				if ($folder instanceof Folder) {
					return $folder->getByIdInRootMount($id);
				} else {
					throw new \Exception('getByIdInPath with non folder');
				}
			}
			return [];
		}

		$nodes = array_map(function (IMountPoint $mount) use ($id, $mountRoots) {
			$rootInternalPath = $mountRoots[$mount->getStorageRootId()];
			$cacheEntry = $mount->getStorage()->getCache()->get($id);
			if (!$cacheEntry) {
				return null;
			}

			// cache jails will hide the "true" internal path
			$internalPath = ltrim($rootInternalPath . '/' . $cacheEntry->getPath(), '/');
			$pathRelativeToMount = substr($internalPath, strlen($rootInternalPath));
			$pathRelativeToMount = ltrim($pathRelativeToMount, '/');
			$absolutePath = rtrim($mount->getMountPoint() . $pathRelativeToMount, '/');
			$storage = $mount->getStorage();
			if ($storage === null) {
				return null;
			}
			$ownerId = $storage->getOwner($pathRelativeToMount);
			if ($ownerId !== false) {
				$owner = Server::get(IUserManager::class)->get($ownerId);
			} else {
				$owner = null;
			}
			return $this->createNode($absolutePath, new FileInfo(
				$absolutePath,
				$storage,
				$cacheEntry->getPath(),
				$cacheEntry,
				$mount,
				$owner,
			));
		}, $mountsContainingFile);

		$nodes = array_filter($nodes);

		$folders = array_filter($nodes, function (Node $node) use ($path) {
			return PathHelper::getRelativePath($path, $node->getPath()) !== null;
		});
		usort($folders, function ($a, $b) {
			return $b->getPath() <=> $a->getPath();
		});
		return $folders;
	}

	public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode {
		$path = $cacheEntry->getPath();
		$fullPath = $mountPoint->getMountPoint() . $path;
		// todo: LazyNode?
		$info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint);
		$parentPath = dirname($fullPath);
		$parent = new LazyFolder($this, function () use ($parentPath) {
			$parent = $this->get($parentPath);
			if ($parent instanceof \OCP\Files\Folder) {
				return $parent;
			} else {
				throw new \Exception("parent $parentPath is not a folder");
			}
		}, [
			'path' => $parentPath,
		]);
		$isDir = $info->getType() === FileInfo::TYPE_FOLDER;
		$view = new View('');
		if ($isDir) {
			return new Folder($this, $view, $path, $info, $parent);
		} else {
			return new File($this, $view, $path, $info, $parent);
		}
	}
}